Browse Source

NodeEditor: UX Updates (#24573)

* node-editor updates

* update screenshot

* re-screenshot

* cleanup

* fix use of material.opacity on WebGLRenderer & cleanup

* allow view other nodes while dragging in focus mode

* fixes from deepscan

* fix tips

* darkness background

* fix tabs

* improve new project

* update background and screenshot

* new tags

* fix drag canvas using zoom

* show only ghost if dragging a node and fix slider field change event

* zoom: fixes and improvements

* improve connection preview

* fix padding
sunag 2 years ago
parent
commit
338658fc1a

File diff suppressed because it is too large
+ 0 - 0
examples/jsm/libs/flow.module.js


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

@@ -45,6 +45,7 @@ export const NodeList = [
 			{
 				name: 'Slider',
 				icon: 'adjustments-horizontal',
+				tags: 'number',
 				nodeClass: SliderEditor
 			},
 			{
@@ -117,6 +118,7 @@ export const NodeList = [
 			{
 				name: 'Blend',
 				icon: 'layers-subtract',
+				tags: 'mix',
 				nodeClass: BlendEditor
 			},
 			{
@@ -133,6 +135,7 @@ export const NodeList = [
 			{
 				name: 'Operator',
 				icon: 'math-symbols',
+				tags: 'addition, subtration, multiplication, division',
 				nodeClass: OperatorEditor
 			},
 			{
@@ -161,12 +164,14 @@ export const NodeList = [
 				name: 'Trigonometry',
 				icon: 'wave-sine',
 				tip: 'Sin / Cos / Tan / ...',
+				tags: 'sin, cos, tan, asin, acos, atan, sine, cosine, tangent, arcsine, arccosine, arctangent',
 				nodeClass: TrigonometryEditor
 			},
 			{
 				name: 'Angle',
 				icon: 'angle',
 				tip: 'Degress / Radians',
+				tags: 'degress, radians',
 				nodeClass: AngleEditor
 			},
 			{
@@ -303,6 +308,13 @@ export class NodeEditor extends EventDispatcher {
 		this.canvas = canvas;
 		this.domElement = domElement;
 
+		this._preview = false;
+
+		this.search = null;
+
+		this.menu = null;
+		this.previewMenu = null;
+
 		this.nodesContext = null;
 		this.examplesContext = null;
 
@@ -315,21 +327,26 @@ export class NodeEditor extends EventDispatcher {
 
 	}
 
+	setSize( width, height ) {
+
+		this.canvas.setSize( width, height );
+
+		return this;
+
+	}
+
 	centralizeNode( node ) {
 
 		const canvas = this.canvas;
-		const canvasRect = canvas.rect;
-
 		const nodeRect = node.dom.getBoundingClientRect();
 
-		const defaultOffsetX = nodeRect.width;
-		const defaultOffsetY = nodeRect.height;
-
 		node.setPosition(
-			( canvas.relativeX + ( canvasRect.width / 2 ) ) - defaultOffsetX,
-			( canvas.relativeY + ( canvasRect.height / 2 ) ) - defaultOffsetY
+			( ( canvas.width / 2 ) - canvas.scrollLeft ) - nodeRect.width,
+			( ( canvas.height / 2 ) - canvas.scrollTop ) - nodeRect.height
 		);
 
+		return this;
+
 	}
 
 	add( node ) {
@@ -359,9 +376,47 @@ export class NodeEditor extends EventDispatcher {
 
 	}
 
+	set preview( value ) {
+
+		if ( this._preview === value ) return;
+
+		if ( value ) {
+
+			this.menu.dom.remove();
+			this.canvas.dom.remove();
+			this.search.dom.remove();
+
+			this.domElement.append( this.previewMenu.dom );
+
+		} else {
+
+			this.canvas.focusSelected = false;
+
+			this.domElement.append( this.menu.dom );
+			this.domElement.append( this.canvas.dom );
+			this.domElement.append( this.search.dom );
+
+			this.previewMenu.dom.remove();
+
+		}
+
+		this._preview = value;
+
+	}
+
+	get preview() {
+
+		return this._preview;
+
+	}
+
 	newProject() {
 
-		this.canvas.clear();
+		const canvas = this.canvas;
+		canvas.clear();
+		canvas.scrollLeft = 0;
+		canvas.scrollTop = 0;
+		canvas.zoom = 1;
 
 		this.dispatchEvent( { type: 'new' } );
 
@@ -426,13 +481,23 @@ export class NodeEditor extends EventDispatcher {
 	_initMenu() {
 
 		const menu = new CircleMenu();
+		const previewMenu = new CircleMenu();
+
+		menu.setAlign( 'top left' );
+		previewMenu.setAlign( 'top left' );
 
+		const previewButton = new ButtonInput().setIcon( 'ti ti-3d-cube-sphere' ).setToolTip( 'Preview' );
 		const menuButton = new ButtonInput().setIcon( 'ti ti-apps' ).setToolTip( 'Add' );
 		const examplesButton = new ButtonInput().setIcon( 'ti ti-file-symlink' ).setToolTip( 'Examples' );
 		const newButton = new ButtonInput().setIcon( 'ti ti-file' ).setToolTip( 'New' );
 		const openButton = new ButtonInput().setIcon( 'ti ti-upload' ).setToolTip( 'Open' );
 		const saveButton = new ButtonInput().setIcon( 'ti ti-download' ).setToolTip( 'Save' );
 
+		const editorButton = new ButtonInput().setIcon( 'ti ti-subtask' ).setToolTip( 'Editor' );
+
+		previewButton.onClick( () => this.preview = true );
+		editorButton.onClick( () => this.preview = false );
+
 		menuButton.onClick( () => this.nodesContext.open() );
 		examplesButton.onClick( () => this.examplesContext.open() );
 
@@ -486,15 +551,19 @@ export class NodeEditor extends EventDispatcher {
 
 		} );
 
-		menu.add( examplesButton )
-			.add( menuButton )
+		menu.add( previewButton )
 			.add( newButton )
+			.add( examplesButton )
 			.add( openButton )
-			.add( saveButton );
+			.add( saveButton )
+			.add( menuButton );
+
+		previewMenu.add( editorButton );
 
 		this.domElement.append( menu.dom );
 
 		this.menu = menu;
+		this.previewMenu = previewMenu;
 
 	}
 
@@ -541,7 +610,6 @@ export class NodeEditor extends EventDispatcher {
 		addExample( basicContext, 'Animate UV' );
 		addExample( basicContext, 'Fake top light' );
 		addExample( basicContext, 'Oscillator color' );
-		addExample( basicContext, 'Matcap' );
 
 		addExample( advancedContext, 'Rim' );
 
@@ -571,11 +639,18 @@ export class NodeEditor extends EventDispatcher {
 					this.add( node );
 
 					this.centralizeNode( node );
+					this.canvas.select( node );
 
 				} );
 
 				search.add( button );
 
+				if ( item.tags !== undefined ) {
+
+					search.setTag( button, item.tags );
+
+				}
+
 			}
 
 			if ( item.children ) {
@@ -652,6 +727,7 @@ export class NodeEditor extends EventDispatcher {
 							this.add( node );
 
 							this.centralizeNode( node );
+							this.canvas.select( node );
 
 						} );
 
@@ -675,6 +751,8 @@ export class NodeEditor extends EventDispatcher {
 
 		} );
 
+		this.search = search;
+
 		this.domElement.append( search.dom );
 
 	}
@@ -698,6 +776,7 @@ export class NodeEditor extends EventDispatcher {
 			} else {
 
 				this.centralizeNode( node );
+				this.canvas.select( node );
 
 			}
 

+ 10 - 3
examples/jsm/node-editor/core/BaseNode.js

@@ -34,6 +34,12 @@ export class BaseNode extends ObjectNode {
 
 	}
 
+	getColor() {
+
+		return ( this.getColorValueFromValue( this.value ) || '#777777' ) + 'BB';
+
+	}
+
 	serialize( data ) {
 
 		super.serialize( data );
@@ -64,15 +70,16 @@ export class BaseNode extends ObjectNode {
 
 		if ( value.isMaterial === true ) {
 
-			return 'forestgreen';
+			//return 'forestgreen';
+			return '#228b22';
 
 		} else if ( value.isObject3D === true ) {
 
-			return 'orange';
+			return '#ffa500';
 
 		} else if ( value.isDataFile === true ) {
 
-			return 'aqua';
+			return '#00ffff';
 
 		}
 

+ 6 - 9
examples/jsm/node-editor/materials/BasicMaterialEditor.js

@@ -1,7 +1,6 @@
 import { ColorInput, SliderInput, LabelElement } from '../../libs/flow.module.js';
 import { BaseNode } from '../core/BaseNode.js';
 import { MeshBasicNodeMaterial } from 'three/nodes';
-import { MathUtils } from 'three';
 
 export class BasicMaterialEditor extends BaseNode {
 
@@ -65,23 +64,21 @@ export class BasicMaterialEditor extends BaseNode {
 
 		this.updateTransparent();
 
-		// TODO: Fix on NodeMaterial System
-		material.customProgramCacheKey = () => {
-
-			return MathUtils.generateUUID();
-
-		};
-
 	}
 
 	updateTransparent() {
 
 		const { material, opacity } = this;
 
-		material.transparent = opacity.getLinkedObject() || material.opacity < 1 ? true : false;
+		const transparent = opacity.getLinkedObject() || material.opacity < 1 ? true : false;
+		const needsUpdate = transparent !== material.transparent;
+
+		material.transparent = transparent;
 
 		opacity.setIcon( material.transparent ? 'ti ti-layers-intersect' : 'ti ti-layers-subtract' );
 
+		if ( needsUpdate === true ) material.dispose();
+
 	}
 
 }

+ 6 - 9
examples/jsm/node-editor/materials/StandardMaterialEditor.js

@@ -1,7 +1,6 @@
 import { ColorInput, SliderInput, LabelElement } from '../../libs/flow.module.js';
 import { BaseNode } from '../core/BaseNode.js';
 import { MeshStandardNodeMaterial } from 'three/nodes';
-import * as THREE from 'three';
 
 export class StandardMaterialEditor extends BaseNode {
 
@@ -99,23 +98,21 @@ export class StandardMaterialEditor extends BaseNode {
 
 		this.updateTransparent();
 
-		// TODO: Fix on NodeMaterial System
-		material.customProgramCacheKey = () => {
-
-			return THREE.MathUtils.generateUUID();
-
-		};
-
 	}
 
 	updateTransparent() {
 
 		const { material, opacity } = this;
 
-		material.transparent = opacity.getLinkedObject() || material.opacity < 1 ? true : false;
+		const transparent = opacity.getLinkedObject() || material.opacity < 1 ? true : false;
+		const needsUpdate = transparent !== material.transparent;
+
+		material.transparent = transparent;
 
 		opacity.setIcon( material.transparent ? 'ti ti-layers-intersect' : 'ti ti-layers-subtract' );
 
+		if ( needsUpdate === true ) material.dispose();
+
 	}
 
 }

+ 3 - 3
examples/jsm/node-editor/math/AngleEditor.js

@@ -14,9 +14,9 @@ export class AngleEditor extends BaseNode {
 		super( 'Angle', 1, node, 175 );
 
 		const optionsField = new SelectInput( [
-			{ name: 'Degrees to Radians', value: MathNode.RAD },
-			{ name: 'Radians to Degrees', value: MathNode.DEG }
-		], MathNode.RAD ).onChange( () => {
+			{ name: 'Degrees to Radians', value: MathNode.RADIANS },
+			{ name: 'Radians to Degrees', value: MathNode.DEGREES }
+		], MathNode.RADIANS ).onChange( () => {
 
 			node.method = optionsField.getValue();
 

+ 2 - 2
examples/jsm/node-editor/math/InvertEditor.js

@@ -1,4 +1,4 @@
-import { SelectInput, LabelElement } from '../../libs/flow.module.js';
+import { SelectInput, Element, LabelElement } from '../../libs/flow.module.js';
 import { BaseNode } from '../core/BaseNode.js';
 import { MathNode, UniformNode } from 'three/nodes';
 
@@ -31,7 +31,7 @@ export class InvertEditor extends BaseNode {
 
 		} );
 
-		this.add( new LabelElement( 'Method' ).add( optionsField ) )
+		this.add( new Element().add( optionsField ) )
 			.add( input );
 
 	}

+ 11 - 8
examples/jsm/node-editor/scene/MeshEditor.js

@@ -16,7 +16,8 @@ export class MeshEditor extends Object3DEditor {
 
 		this.material = null;
 
-		this.defaultMaterial = null;
+		this.meshMaterial = null;
+		this.defaultMeshMaterial = null;
 
 		this._initMaterial();
 
@@ -34,9 +35,9 @@ export class MeshEditor extends Object3DEditor {
 
 	_initMaterial() {
 
-		const materialElement = new LabelElement( 'Material' ).setInputColor( 'forestgreen' ).setInput( 1 );
+		const material = new LabelElement( 'Material' ).setInputColor( 'forestgreen' ).setInput( 1 );
 
-		materialElement.onValid( ( source, target, stage ) => {
+		material.onValid( ( source, target, stage ) => {
 
 			const object = target.getObject();
 
@@ -56,13 +57,15 @@ export class MeshEditor extends Object3DEditor {
 
 		} ).onConnect( () => {
 
-			this.material = materialElement.getLinkedObject() || this.defaultMaterial;
+			this.meshMaterial = material.getLinkedObject() || this.defaultMeshMaterial;
 
 			this.update();
 
 		} );
 
-		this.add( materialElement );
+		this.add( material );
+
+		this.material = material;
 
 	}
 
@@ -74,7 +77,7 @@ export class MeshEditor extends Object3DEditor {
 
 		if ( mesh ) {
 
-			mesh.material = this.material || this.defaultMaterial;
+			mesh.material = this.meshMaterial || this.defaultMeshMaterial;
 
 		}
 
@@ -84,7 +87,7 @@ export class MeshEditor extends Object3DEditor {
 
 		super.updateDefault();
 
-		this.defaultMaterial = this.mesh.material;
+		this.defaultMeshMaterial = this.mesh.material;
 
 	}
 
@@ -92,7 +95,7 @@ export class MeshEditor extends Object3DEditor {
 
 		super.restoreDefault();
 
-		this.mesh.material = this.defaultMaterial;
+		this.mesh.material = this.defaultMeshMaterial;
 
 	}
 

+ 4 - 0
examples/jsm/node-editor/utils/PreviewEditor.js

@@ -65,6 +65,8 @@ export class PreviewEditor extends BaseNode {
 		const previewElement = new Element();
 		previewElement.dom.style[ 'padding-top' ] = 0;
 		previewElement.dom.style[ 'padding-bottom' ] = 0;
+		previewElement.dom.style[ 'padding-left' ] = 0;
+		previewElement.dom.style[ 'padding-right' ] = '14px';
 
 		const sceneInput = new SelectInput( [
 			{ name: 'Box', value: 'box' },
@@ -86,6 +88,8 @@ export class PreviewEditor extends BaseNode {
 		previewElement.dom.append( canvas );
 		previewElement.setHeight( height );
 
+		previewElement.dom.addEventListener( 'wheel', e => e.stopPropagation() );
+
 		const renderer = new WebGLRenderer( {
 			canvas,
 			alpha: true

+ 17 - 19
examples/jsm/renderers/webgl/nodes/WebGLNodeBuilder.js

@@ -112,9 +112,8 @@ class WebGLNodeBuilder extends NodeBuilder {
 			this.addSlot( 'fragment', new SlotNode( {
 				node: material.colorNode,
 				nodeType: 'vec4',
-				source: getIncludeSnippet( 'color_fragment' ),
-				target: 'diffuseColor = %RESULT%;',
-				inclusionType: 'append'
+				source: 'vec4 diffuseColor = vec4( diffuse, opacity );',
+				target: 'vec4 diffuseColor = %RESULT%;'
 			} ) );
 
 		}
@@ -129,6 +128,10 @@ class WebGLNodeBuilder extends NodeBuilder {
 				inclusionType: 'append'
 			} ) );
 
+		} else {
+
+			this.addCode( 'fragment', getIncludeSnippet( 'alphatest_fragment' ), 'diffuseColor.a = opacity;', this.shader );
+
 		}
 
 		if ( material.normalNode && material.normalNode.isNode ) {
@@ -514,32 +517,32 @@ class WebGLNodeBuilder extends NodeBuilder {
 
 	}
 
-	addCodeAfterCode( shaderStage, snippet, code ) {
+	addCode( shaderStage, source, code, scope = this ) {
 
 		const shaderProperty = getShaderStageProperty( shaderStage );
 
-		let source = this[ shaderProperty ];
+		let snippet = scope[ shaderProperty ];
 
-		const index = source.indexOf( snippet );
+		const index = snippet.indexOf( source );
 
 		if ( index !== - 1 ) {
 
-			const start = source.substring( 0, index + snippet.length );
-			const end = source.substring( index + snippet.length );
+			const start = snippet.substring( 0, index + source.length );
+			const end = snippet.substring( index + source.length );
 
-			source = `${start}\n${code}\n${end}`;
+			snippet = `${start}\n${code}\n${end}`;
 
 		}
 
-		this[ shaderProperty ] = source;
+		scope[ shaderProperty ] = snippet;
 
 	}
 
-	replaceCode( shaderStage, source, target ) {
+	replaceCode( shaderStage, source, target, scope = this ) {
 
 		const shaderProperty = getShaderStageProperty( shaderStage );
 
-		this[ shaderProperty ] = this[ shaderProperty ].replaceAll( source, target );
+		scope[ shaderProperty ] = scope[ shaderProperty ].replaceAll( source, target );
 
 	}
 
@@ -644,11 +647,6 @@ ${this.shader[ getShaderStageProperty( shaderStage ) ]}
 
 			const slots = this.slots[ shaderStage ].sort( ( slotA, slotB ) => {
 
-				if ( sourceCode.indexOf( slotA.source ) == - 1 ) {
-					//console.log( slotA, sourceCode.indexOf( slotA.source ), sourceCode.indexOf( slotB.source ) );
-					//console.log(sourceCode);
-				}
-
 				return sourceCode.indexOf( slotA.source ) > sourceCode.indexOf( slotB.source ) ? 1 : - 1;
 
 			} );
@@ -677,7 +675,7 @@ ${this.shader[ getShaderStageProperty( shaderStage ) ]}
 
 				if ( inclusionType === 'append' ) {
 
-					this.addCodeAfterCode( shaderStage, source, target );
+					this.addCode( shaderStage, source, target );
 
 				} else if ( inclusionType === 'replace' ) {
 
@@ -691,7 +689,7 @@ ${this.shader[ getShaderStageProperty( shaderStage ) ]}
 
 			}
 
-			this.addCodeAfterCode(
+			this.addCode(
 				shaderStage,
 				'main() {',
 				this.flowCode[ shaderStage ]

BIN
examples/screenshots/webgl_nodes_playground.jpg


+ 128 - 122
examples/webgl_nodes_playground.html

@@ -1,7 +1,7 @@
 <!DOCTYPE html>
 <html lang="en">
-  <head>
-		<title>three.js webgl - node-editor playground</title>
+	<head>
+		<title>three.js webgl - node playground</title>
 		<meta charset="utf-8">
 		<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
 		<link rel="stylesheet" href="fonts/open-sans/open-sans.css" type="text/css"/>
@@ -17,193 +17,199 @@
 				position: absolute;
 				top: 0;
 				left: 0;
-				height: 50%;
+				height: 100%;
 				width: 100%;
 			}
 			flow {
 				position: absolute;
-				top: 50%;
+				top: 0;
 				left: 0;
-				height: 50%;
+				height: 100%;
 				width: 100%;
-				background: #222;
 				box-shadow: inset 0 0 20px 0px #000000;
+				pointer-events: none;
+			}
+			flow f-canvas {
+				pointer-events: auto;
+			}
+			flow f-canvas:not(.focusing) {
+				background: #191919ed;
 			}
 		</style>
-  </head>
-  <body>
-
-	<div id="info">
-		<a href="https://threejs.org" target="_blank" rel="noopener">three.js</a> - WebGL - Node Editor ( Playground version )<br />
-	</div>
-
-	<script async src="https://unpkg.com/[email protected]/dist/es-module-shims.js"></script>
-
-	<script type="importmap">
-		{
-			"imports": {
-				"three": "../build/three.module.js",
-				"three/addons/": "./jsm/",
-				"three/nodes": "./jsm/nodes/Nodes.js"
+	</head>
+	<body>
+
+		<script async src="https://unpkg.com/[email protected]/dist/es-module-shims.js"></script>
+
+		<script type="importmap">
+			{
+				"imports": {
+					"three": "../build/three.module.js",
+					"three/addons/": "./jsm/",
+					"three/nodes": "./jsm/nodes/Nodes.js"
+				}
 			}
-		}
-	</script>
+		</script>
 
-	<script type="module">
+		<script type="module">
 
-		import * as THREE from 'three';
-		import * as Nodes from 'three/nodes';
+			import * as THREE from 'three';
+			import * as Nodes from 'three/nodes';
 
-		import { nodeFrame } from 'three/addons/renderers/webgl/nodes/WebGLNodes.js';
+			import { nodeFrame } from 'three/addons/renderers/webgl/nodes/WebGLNodes.js';
 
-		import { NodeEditor } from 'three/addons/node-editor/NodeEditor.js';
-		import { MeshEditor } from 'three/addons/node-editor/scene/MeshEditor.js';
+			import { NodeEditor } from 'three/addons/node-editor/NodeEditor.js';
+			import { MeshEditor } from 'three/addons/node-editor/scene/MeshEditor.js';
+			import { StandardMaterialEditor } from 'three/addons/node-editor/materials/StandardMaterialEditor.js';
 
-		import Stats from 'three/addons/libs/stats.module.js';
+			import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
+			import { FBXLoader } from 'three/addons/loaders/FBXLoader.js';
 
-		import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
-		import { FBXLoader } from 'three/addons/loaders/FBXLoader.js';
+			let camera, scene, renderer;
+			let model;
+			let nodeEditor;
 
-		let stats;
-		let camera, scene, renderer;
-		let model;
+			init();
+			animate();
 
-		init();
-		animate();
+			function init() {
 
-		function init() {
+				camera = new THREE.PerspectiveCamera( 40, window.innerWidth / window.innerHeight, 1, 5000 );
+				camera.position.set( 0.0, 300, 400 * 3 );
 
-			camera = new THREE.PerspectiveCamera( 40, window.innerWidth / window.innerHeight, 1, 5000 );
-			camera.position.set( 0.0, 300, 400 * 3 );
+				scene = new THREE.Scene();
+				scene.background = new THREE.Color( 0x333333 );
 
-			scene = new THREE.Scene();
-			scene.background = new THREE.Color( 0x333333 );
+				// Lights
 
-			// Lights
+				const topLight = new THREE.PointLight( 0xF4F6F0, 1 );
+				topLight.position.set( 0, 1000, 1000 );
+				scene.add( topLight );
 
-			const topLight = new THREE.PointLight( 0xF4F6F0, 1 );
-			topLight.position.set( 0, 1000, 1000 );
-			scene.add( topLight );
+				const backLight = new THREE.PointLight( 0x0c1445, 1 );
+				backLight.position.set( - 100, 20, - 260 );
+				scene.add( backLight );
 
-			const backLight = new THREE.PointLight( 0x0c1445, 1 );
-			backLight.position.set( - 100, 20, - 260 );
-			scene.add( backLight );
+				renderer = new THREE.WebGLRenderer( { antialias: true } );
+				document.body.appendChild( renderer.domElement );
+				renderer.outputEncoding = THREE.sRGBEncoding;
+				renderer.toneMapping = THREE.LinearToneMapping;
+				renderer.toneMappingExposure = 4000;
+				renderer.physicallyCorrectLights = true;
 
-			renderer = new THREE.WebGLRenderer( { antialias: true } );
-			document.body.appendChild( renderer.domElement );
-			renderer.outputEncoding = THREE.sRGBEncoding;
-			renderer.toneMapping = THREE.LinearToneMapping;
-			renderer.toneMappingExposure = 4000;
-			renderer.physicallyCorrectLights = true;
+				renderer.domElement.className = 'renderer';
 
-			renderer.domElement.className = 'renderer';
+				//
 
-			//
+				const controls = new OrbitControls( camera, renderer.domElement );
+				controls.minDistance = 500;
+				controls.maxDistance = 3000;
+
+				window.addEventListener( 'resize', onWindowResize );
 
-			stats = new Stats();
-			document.body.appendChild( stats.dom );
+				initEditor();
 
-			const controls = new OrbitControls( camera, renderer.domElement );
-			controls.minDistance = 500;
-			controls.maxDistance = 3000;
+				onWindowResize();
 
-			window.addEventListener( 'resize', onWindowResize );
+			}
 
-			onWindowResize();
+			function initEditor() {
 
-			initEditor();
+				nodeEditor = new NodeEditor( scene );
 
-		}
+				const reset = () => {
 
-		function initEditor() {
+					const meshEditor = new MeshEditor( model );
+					const materialEditor = new StandardMaterialEditor();
 
-			const nodeEditor = new NodeEditor( scene );
+					nodeEditor.add( meshEditor );
+					nodeEditor.add( materialEditor );
+					nodeEditor.centralizeNode( meshEditor );
 
-			nodeEditor.addEventListener( 'new', () => {
+					const { x, y } = meshEditor.getPosition();
 
-				const materialEditor = new MeshEditor( model );
+					meshEditor.setPosition( x + 250, y );
+					materialEditor.setPosition( x - 250, y );
 
-				nodeEditor.add( materialEditor );
-				nodeEditor.centralizeNode( materialEditor );
+					meshEditor.material.connect( materialEditor );
 
-			} );
+				};
 
-			document.body.appendChild( nodeEditor.domElement );
+				nodeEditor.addEventListener( 'new', reset );
 
-			const loaderFBX = new FBXLoader();
-			loaderFBX.load( 'models/fbx/stanford-bunny.fbx', ( object ) => {
+				document.body.appendChild( nodeEditor.domElement );
 
-				const defaultMaterial = new Nodes.MeshBasicNodeMaterial();
-				defaultMaterial.colorNode = new Nodes.UniformNode( 0 );
+				const loaderFBX = new FBXLoader();
+				loaderFBX.load( 'models/fbx/stanford-bunny.fbx', ( object ) => {
 
-				const sphere = new THREE.Mesh( new THREE.SphereGeometry( 200, 32, 16 ), defaultMaterial );
-				sphere.name = 'Sphere';
-				sphere.position.set( 500, 0, - 500 );
-				scene.add( sphere );
+					const defaultMaterial = new Nodes.MeshBasicNodeMaterial();
+					defaultMaterial.colorNode = new Nodes.UniformNode( 0 );
 
-				const box = new THREE.Mesh( new THREE.BoxGeometry( 200, 200, 200 ), defaultMaterial );
-				box.name = 'Box';
-				box.position.set( - 500, 0, - 500 );
-				scene.add( box );
+					const sphere = new THREE.Mesh( new THREE.SphereGeometry( 200, 32, 16 ), defaultMaterial );
+					sphere.name = 'Sphere';
+					sphere.position.set( 500, 0, - 500 );
+					scene.add( sphere );
 
-				const defaultPointsMaterial = new Nodes.PointsNodeMaterial();
-				defaultPointsMaterial.colorNode = new Nodes.UniformNode( 0 );
+					const box = new THREE.Mesh( new THREE.BoxGeometry( 200, 200, 200 ), defaultMaterial );
+					box.name = 'Box';
+					box.position.set( - 500, 0, - 500 );
+					scene.add( box );
 
-				const torusKnot = new THREE.Points( new THREE.TorusKnotGeometry( 100, 30, 100, 16 ), defaultPointsMaterial );
-				torusKnot.name = 'Torus Knot ( Points )';
-				torusKnot.position.set( 0, 0, - 500 );
-				scene.add( torusKnot );
+					const defaultPointsMaterial = new Nodes.PointsNodeMaterial();
+					defaultPointsMaterial.colorNode = new Nodes.UniformNode( 0 );
 
-				model = object.children[ 0 ];
-				model.position.set( 0, 0, 10 );
-				model.scale.setScalar( 1 );
-				model.material = defaultMaterial;
-				scene.add( model );
+					const torusKnot = new THREE.Points( new THREE.TorusKnotGeometry( 100, 30, 100, 16 ), defaultPointsMaterial );
+					torusKnot.name = 'Torus Knot ( Points )';
+					torusKnot.position.set( 0, 0, - 500 );
+					scene.add( torusKnot );
 
-				const materialEditor = new MeshEditor( model );
+					model = object.children[ 0 ];
+					model.position.set( 0, 0, 10 );
+					model.scale.setScalar( 1 );
+					model.material = defaultMaterial;
+					scene.add( model );
 
-				nodeEditor.add( materialEditor );
-				nodeEditor.centralizeNode( materialEditor );
+					reset();
 
-			} );
+				} );
 
-		}
+			}
 
-		function onWindowResize() {
+			function onWindowResize() {
 
-			const width = window.innerWidth;
-			const height = window.innerHeight / 2;
+				const width = window.innerWidth;
+				const height = window.innerHeight;
 
-			camera.aspect = width / height;
-			camera.updateProjectionMatrix();
+				camera.aspect = width / height;
+				camera.updateProjectionMatrix();
 
-			renderer.setSize( width, height );
+				renderer.setSize( width, height );
 
-		}
+				nodeEditor.setSize( width, height );
 
-		//
+			}
 
-		function animate() {
+			//
 
-			requestAnimationFrame( animate );
+			function animate() {
 
-			nodeFrame.update();
+				requestAnimationFrame( animate );
 
-			render();
+				nodeFrame.update();
 
-			stats.update();
+				render();
 
-		}
+			}
 
-		function render() {
+			function render() {
 
-			//if ( model ) model.rotation.y = performance.now() / 5000;
+				//if ( model ) model.rotation.y = performance.now() / 5000;
 
-			renderer.render( scene, camera );
+				renderer.render( scene, camera );
 
-		}
+			}
 
-	</script>
+		</script>
 
-  </body>
+	</body>
 </html>

+ 35 - 29
examples/webgpu_nodes_playground.html

@@ -1,7 +1,7 @@
 <!DOCTYPE html>
 <html lang="en">
 	<head>
-		<title>three.js - WebGPU - Selective Lights</title>
+		<title>three.js - webgpu - node playground</title>
 		<meta charset="utf-8">
 		<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
 		<link type="text/css" rel="stylesheet" href="main.css">
@@ -17,26 +17,28 @@
 				position: absolute;
 				top: 0;
 				left: 0;
-				height: 50%;
+				height: 100%;
 				width: 100%;
 			}
 			flow {
 				position: absolute;
-				top: 50%;
+				top: 0;
 				left: 0;
-				height: 50%;
+				height: 100%;
 				width: 100%;
-				background: #222;
 				box-shadow: inset 0 0 20px 0px #000000;
+				pointer-events: none;
+			}
+			flow f-canvas {
+				pointer-events: auto;
+			}
+			flow f-canvas:not(.focusing) {
+				background: #191919ed;
 			}
 		</style>
 	</head>
 	<body>
 
-		<div id="info">
-			<a href="https://threejs.org" target="_blank" rel="noopener">three.js</a> - WebGPU - Node Editor ( Playground version )<br />
-		</div>
-
 		<script async src="https://unpkg.com/[email protected]/dist/es-module-shims.js"></script>
 
 		<script type="importmap">
@@ -59,8 +61,7 @@
 
 			import { NodeEditor } from 'three/addons/node-editor/NodeEditor.js';
 			import { MeshEditor } from 'three/addons/node-editor/scene/MeshEditor.js';
-
-			import Stats from 'three/addons/libs/stats.module.js';
+			import { StandardMaterialEditor } from 'three/addons/node-editor/materials/StandardMaterialEditor.js';
 
 			import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
 			import { FBXLoader } from 'three/addons/loaders/FBXLoader.js';
@@ -68,9 +69,9 @@
 			// Use PreviewEditor in WebGL for now
 			import { nodeFrame } from 'three/addons/renderers/webgl/nodes/WebGLNodes.js';
 
-			let stats;
 			let camera, scene, renderer;
 			let model;
+			let nodeEditor;
 
 			init().then( animate ).catch( error => console.error( error ) );
 
@@ -111,35 +112,43 @@
 
 				//
 
-				stats = new Stats();
-				document.body.appendChild( stats.dom );
-
 				const controls = new OrbitControls( camera, renderer.domElement );
 				controls.minDistance = 500;
 				controls.maxDistance = 3000;
 
 				window.addEventListener( 'resize', onWindowResize );
 
-				onWindowResize();
-
 				initEditor();
 
+				onWindowResize();
+
 				return renderer.init();
 
 			}
 
 			function initEditor() {
 
-				const nodeEditor = new NodeEditor( scene );
+				nodeEditor = new NodeEditor( scene );
 
-				nodeEditor.addEventListener( 'new', () => {
+				const reset = () => {
 
-					const materialEditor = new MeshEditor( model );
+					const meshEditor = new MeshEditor( model );
+					const materialEditor = new StandardMaterialEditor();
 
+					nodeEditor.add( meshEditor );
 					nodeEditor.add( materialEditor );
-					nodeEditor.centralizeNode( materialEditor );
+					nodeEditor.centralizeNode( meshEditor );
 
-				} );
+					const { x, y } = meshEditor.getPosition();
+
+					meshEditor.setPosition( x + 250, y );
+					materialEditor.setPosition( x - 250, y );
+
+					meshEditor.material.connect( materialEditor );
+
+				};
+
+				nodeEditor.addEventListener( 'new', reset );
 
 				document.body.appendChild( nodeEditor.domElement );
 
@@ -173,10 +182,7 @@
 					model.material = defaultMaterial;
 					scene.add( model );
 
-					const materialEditor = new MeshEditor( model );
-
-					nodeEditor.add( materialEditor );
-					nodeEditor.centralizeNode( materialEditor );
+					reset();
 
 				} );
 
@@ -185,13 +191,15 @@
 			function onWindowResize() {
 
 				const width = window.innerWidth;
-				const height = window.innerHeight / 2;
+				const height = window.innerHeight;
 
 				camera.aspect = width / height;
 				camera.updateProjectionMatrix();
 
 				renderer.setSize( width, height );
 
+				nodeEditor.setSize( width, height );
+
 			}
 
 			//
@@ -204,8 +212,6 @@
 
 				render();
 
-				stats.update();
-
 			}
 
 			function render() {

Some files were not shown because too many files changed in this diff