Procházet zdrojové kódy

NodeEditor: Adds support for exporting Node Chaining, Materials and Objects3D (individually) (#25553)

* NodeEditor: Adds support for exporting Materials and Objects3D

* cleanup

* fix empty auto-complete

* NodeLoader: Fix deserialization single node.

* Node: Added NodeArray[] auto serialization.

* NodeEditor: Show Export option whenever it is available.

* BaseNode: Slightly safer .toJSON() check.

* NodeUtils: Fix getCacheKey() after getNodeKeys() update.
sunag před 2 roky
rodič
revize
f9585c243a

+ 14 - 12
examples/jsm/node-editor/NodeEditor.js

@@ -33,6 +33,7 @@ import { PointsEditor } from './scene/PointsEditor.js';
 import { MeshEditor } from './scene/MeshEditor.js';
 import { FileEditor } from './core/FileEditor.js';
 import { FileURLEditor } from './core/FileURLEditor.js';
+import { exportJSON } from './NodeEditorUtils.js';
 import { EventDispatcher } from 'three';
 
 Styles.icons.unlink = 'ti ti-unlink';
@@ -540,14 +541,7 @@ export class NodeEditor extends EventDispatcher {
 
 		saveButton.onClick( () => {
 
-			const json = JSON.stringify( this.canvas.toJSON() );
-
-			const a = document.createElement( 'a' );
-			const file = new Blob( [ json ], { type: 'text/plain' } );
-
-			a.href = URL.createObjectURL( file );
-			a.download = 'node_editor.json';
-			a.click();
+			exportJSON( this.canvas.toJSON(), 'node_editor' );
 
 		} );
 
@@ -863,10 +857,14 @@ export class NodeEditor extends EventDispatcher {
 
 			} else if ( key === 'Enter' ) {
 
-				nodeButtonsVisible[ nodeButtonsIndex ].dom.click();
+				if ( nodeButtonsVisible[ nodeButtonsIndex ] !== undefined ) {
 
-				e.preventDefault();
-				e.stopImmediatePropagation();
+					nodeButtonsVisible[ nodeButtonsIndex ].dom.click();
+
+					e.preventDefault();
+					e.stopImmediatePropagation();
+
+				}
 
 			}
 
@@ -911,7 +909,11 @@ export class NodeEditor extends EventDispatcher {
 
 			}
 
-			nodeButtonsVisible[ nodeButtonsIndex ].setSelected( true );
+			if ( nodeButtonsVisible[ nodeButtonsIndex ] !== undefined ) {
+
+				nodeButtonsVisible[ nodeButtonsIndex ].setSelected( true );
+
+			}
 
 		} );
 

+ 12 - 0
examples/jsm/node-editor/NodeEditorUtils.js

@@ -0,0 +1,12 @@
+export const exportJSON = ( object, name ) => {
+
+	const json = JSON.stringify( object );
+
+	const a = document.createElement( 'a' );
+	const file = new Blob( [ json ], { type: 'text/plain' } );
+
+	a.href = URL.createObjectURL( file );
+	a.download = name + '.json';
+	a.click();
+
+};

+ 27 - 7
examples/jsm/node-editor/core/BaseNode.js

@@ -1,4 +1,5 @@
 import { Node, ButtonInput, TitleElement, ContextMenu } from '../../libs/flow.module.js';
+import { exportJSON } from '../NodeEditorUtils.js';
 
 export const onNodeValidElement = ( inputElement, outputElement ) => {
 
@@ -31,24 +32,43 @@ export class BaseNode extends Node {
 			.setSerializable( false )
 			.setOutput( outputLength );
 
-		const closeButton = new ButtonInput().onClick( () => {
+		const contextButton = new ButtonInput().onClick( () => {
 
 			context.open();
 
 		} ).setIcon( 'ti ti-dots' );
 
-		const context = new ContextMenu( this.dom );
-		context.add( new ButtonInput( 'Remove' ).setIcon( 'ti ti-trash' ).onClick( () => {
+		const onAddButtons = () => {
+
+			context.removeEventListener( 'show', onAddButtons );
+
+			if ( this.value && typeof this.value.toJSON === 'function' ) {
+
+				this.context.add( new ButtonInput( 'Export' ).setIcon( 'ti ti-download' ).onClick( () => {
+
+					exportJSON( this.value.toJSON(), this.constructor.name );
+
+				} ) );
+
+			}
 
-			this.dispose();
+			context.add( new ButtonInput( 'Remove' ).setIcon( 'ti ti-trash' ).onClick( () => {
 
-		} ) );
+				this.dispose();
+
+			} ) );
+
+		};
+
+		const context = new ContextMenu( this.dom );
+		context.addEventListener( 'show', onAddButtons );
 
 		this.title = title;
-		this.closeButton = closeButton;
+
+		this.contextButton = contextButton;
 		this.context = context;
 
-		title.addButton( closeButton );
+		title.addButton( contextButton );
 
 		this.add( title );
 

+ 3 - 7
examples/jsm/node-editor/materials/BasicMaterialEditor.js

@@ -1,16 +1,14 @@
 import { ColorInput, SliderInput, LabelElement } from '../../libs/flow.module.js';
-import { BaseNode } from '../core/BaseNode.js';
+import { MaterialEditor } from './MaterialEditor.js';
 import { MeshBasicNodeMaterial } from 'three/nodes';
 
-export class BasicMaterialEditor extends BaseNode {
+export class BasicMaterialEditor extends MaterialEditor {
 
 	constructor() {
 
 		const material = new MeshBasicNodeMaterial();
 
-		super( 'Basic Material', 1, material );
-
-		this.setWidth( 300 );
+		super( 'Basic Material', material );
 
 		const color = new LabelElement( 'color' ).setInput( 3 );
 		const opacity = new LabelElement( 'opacity' ).setInput( 1 );
@@ -42,8 +40,6 @@ export class BasicMaterialEditor extends BaseNode {
 		this.opacity = opacity;
 		this.position = position;
 
-		this.material = material;
-
 		this.update();
 
 	}

+ 17 - 0
examples/jsm/node-editor/materials/MaterialEditor.js

@@ -0,0 +1,17 @@
+import { BaseNode } from '../core/BaseNode.js';
+
+export class MaterialEditor extends BaseNode {
+
+	constructor( name, material, width = 300 ) {
+
+		super( name, 1, material, width );
+
+	}
+
+	get material() {
+
+		return this.value;
+
+	}
+
+}

+ 3 - 7
examples/jsm/node-editor/materials/PointsMaterialEditor.js

@@ -1,17 +1,15 @@
 import { ColorInput, ToggleInput, SliderInput, LabelElement } from '../../libs/flow.module.js';
-import { BaseNode } from '../core/BaseNode.js';
+import { MaterialEditor } from './MaterialEditor.js';
 import { PointsNodeMaterial } from 'three/nodes';
 import * as THREE from 'three';
 
-export class PointsMaterialEditor extends BaseNode {
+export class PointsMaterialEditor extends MaterialEditor {
 
 	constructor() {
 
 		const material = new PointsNodeMaterial();
 
-		super( 'Points Material', 1, material );
-
-		this.setWidth( 300 );
+		super( 'Points Material', material );
 
 		const color = new LabelElement( 'color' ).setInput( 3 );
 		const opacity = new LabelElement( 'opacity' ).setInput( 1 );
@@ -57,8 +55,6 @@ export class PointsMaterialEditor extends BaseNode {
 		this.position = position;
 		this.sizeAttenuation = sizeAttenuation;
 
-		this.material = material;
-
 		this.update();
 
 	}

+ 3 - 7
examples/jsm/node-editor/materials/StandardMaterialEditor.js

@@ -1,16 +1,14 @@
 import { ColorInput, SliderInput, LabelElement } from '../../libs/flow.module.js';
-import { BaseNode } from '../core/BaseNode.js';
+import { MaterialEditor } from './MaterialEditor.js';
 import { MeshStandardNodeMaterial } from 'three/nodes';
 
-export class StandardMaterialEditor extends BaseNode {
+export class StandardMaterialEditor extends MaterialEditor {
 
 	constructor() {
 
 		const material = new MeshStandardNodeMaterial();
 
-		super( 'Standard Material', 1, material );
-
-		this.setWidth( 300 );
+		super( 'Standard Material', material );
 
 		const color = new LabelElement( 'color' ).setInput( 3 );
 		const opacity = new LabelElement( 'opacity' ).setInput( 1 );
@@ -70,8 +68,6 @@ export class StandardMaterialEditor extends BaseNode {
 		this.normal = normal;
 		this.position = position;
 
-		this.material = material;
-
 		this.update();
 
 	}

+ 34 - 4
examples/jsm/nodes/core/Node.js

@@ -273,7 +273,21 @@ class Node {
 
 			for ( const property of nodeKeys ) {
 
-				inputNodes[ property ] = this[ property ].toJSON( json.meta ).uuid;
+				if ( Array.isArray( this[ property ] ) ) {
+
+					inputNodes[ property ] = [];
+
+					for ( const node of this[ property ] ) {
+
+						inputNodes[ property ].push( node.toJSON( json.meta ).uuid );
+
+					}
+
+				} else {
+
+					inputNodes[ property ] = this[ property ].toJSON( json.meta ).uuid;
+
+				}
 
 			}
 
@@ -291,9 +305,25 @@ class Node {
 
 			for ( const property in json.inputNodes ) {
 
-				const uuid = json.inputNodes[ property ];
+				if ( Array.isArray( json.inputNodes[ property ] ) ) {
+
+					const inputArray = [];
 
-				this[ property ] = nodes[ uuid ];
+					for ( const uuid of json.inputNodes[ property ] ) {
+
+						inputArray.push( nodes[ uuid ] );
+
+					}
+
+					this[ property ] = inputArray;
+
+				} else {
+
+					const uuid = json.inputNodes[ property ];
+
+					this[ property ] = nodes[ uuid ];
+
+				}
 
 			}
 
@@ -333,7 +363,7 @@ class Node {
 				}
 			};
 
-			meta.nodes[ data.uuid ] = data;
+			if ( isRoot !== true ) meta.nodes[ data.uuid ] = data;
 
 			this.serialize( data );
 

+ 26 - 2
examples/jsm/nodes/core/NodeUtils.js

@@ -12,7 +12,23 @@ export const getCacheKey = ( object ) => {
 
 	for ( const property of getNodesKeys( object ) ) {
 
-		cacheKey += `${ property }:${ object[ property ].getCacheKey() },`;
+		const node = object[ property ];
+
+		// @TODO: Think about implement NodeArray and NodeObject.
+
+		if ( Array.isArray( node ) ) {
+
+			for ( const subNode of node ) {
+
+				cacheKey += `${ property }:${ subNode.getCacheKey() },`;
+
+			}
+
+		} else {
+
+			cacheKey += `${ property }:${ node.getCacheKey() },`;
+
+		}
 
 	}
 
@@ -30,7 +46,15 @@ export const getNodesKeys = ( object ) => {
 
 		const value = object[ name ];
 
-		if ( value && value.isNode === true ) {
+		if ( Array.isArray( value ) ) {
+
+			if ( value[ 0 ] && value[ 0 ].isNode === true ) {
+
+				props.push( name );
+
+			}
+
+		} else if ( value && value.isNode === true ) {
 
 			props.push( name );
 

+ 1 - 1
examples/jsm/nodes/loaders/NodeLoader.js

@@ -83,7 +83,7 @@ class NodeLoader extends Loader {
 		const node = nodeObject( createNodeFromType( type ) );
 		node.uuid = json.uuid;
 
-		const nodes = this.parseNodes( json.inputNodes );
+		const nodes = this.parseNodes( json.nodes );
 		const meta = { nodes, textures: this.textures };
 
 		json.meta = meta;