Browse Source

NodeEditor: New Version (Rev. 3) (#25692)

* update flow.js rev. 3

* Pass: Added .isPass property.

* NodeEditor: Rev. 3 (WIP)

* fix example custom node "Replace Material by Name"

* fix disconnect resize icon size

* cleanup

* fix gtf loader example

* NodeMaterial: Fix backward compatibility of scene.environment.

* MeshStandardNodeMaterial: Fix specular color node.

* NodeEditor: Move playground to root and revisions.

* Package: Added lint-playground

* cleanup from lint

* update lib

* RangeNode: Node-Based parameters.

* TSL: Added split( node, channels ) as alternative.

* cleanup

* Revision and more two examples

* TextureEditor: Fix const.

* Example particle: Fix rotate.

* Example car: update car color

* cleanup

* NodeEditorLib: getNodeList() -> prevent multiple fetch.

* NodeEditor: Fix linux case-sensitive getting example.

* Added fresnel example

* Rename invert -> oneMinus
sunag 2 years ago
parent
commit
03e4e05557
100 changed files with 6729 additions and 2893 deletions
  1. 0 2
      examples/files.json
  2. BIN
      examples/fonts/tabler-icons/fonts/tabler-icons.eot
  3. BIN
      examples/fonts/tabler-icons/fonts/tabler-icons.ttf
  4. BIN
      examples/fonts/tabler-icons/fonts/tabler-icons.woff
  5. BIN
      examples/fonts/tabler-icons/fonts/tabler-icons.woff2
  6. 0 3
      examples/fonts/tabler-icons/tabler-icons.min.css
  7. 0 971
      examples/jsm/node-editor/NodeEditor.js
  8. 0 12
      examples/jsm/node-editor/NodeEditorUtils.js
  9. 0 14
      examples/jsm/node-editor/accessors/MatcapUVEditor.js
  10. 0 30
      examples/jsm/node-editor/accessors/NormalEditor.js
  11. 0 30
      examples/jsm/node-editor/accessors/PositionEditor.js
  12. 0 25
      examples/jsm/node-editor/accessors/UVEditor.js
  13. 0 59
      examples/jsm/node-editor/core/DataFile.js
  14. 0 20
      examples/jsm/node-editor/core/FileEditor.js
  15. 0 29
      examples/jsm/node-editor/core/FileURLEditor.js
  16. 0 44
      examples/jsm/node-editor/display/BlendEditor.js
  17. 0 49
      examples/jsm/node-editor/display/NormalMapEditor.js
  18. 0 0
      examples/jsm/node-editor/examples/animate-uv.json
  19. 0 0
      examples/jsm/node-editor/examples/fake-top-light.json
  20. 0 0
      examples/jsm/node-editor/examples/matcap.json
  21. 0 0
      examples/jsm/node-editor/examples/oscillator-color.json
  22. 0 0
      examples/jsm/node-editor/examples/rim.json
  23. 0 23
      examples/jsm/node-editor/inputs/FloatEditor.js
  24. 0 28
      examples/jsm/node-editor/inputs/Vector2Editor.js
  25. 0 30
      examples/jsm/node-editor/inputs/Vector3Editor.js
  26. 0 37
      examples/jsm/node-editor/inputs/Vector4Editor.js
  27. 0 17
      examples/jsm/node-editor/materials/MaterialEditor.js
  28. 0 40
      examples/jsm/node-editor/math/AngleEditor.js
  29. 0 35
      examples/jsm/node-editor/math/DotEditor.js
  30. 0 39
      examples/jsm/node-editor/math/InvertEditor.js
  31. 0 62
      examples/jsm/node-editor/math/LimiterEditor.js
  32. 0 28
      examples/jsm/node-editor/math/NormalizeEditor.js
  33. 0 63
      examples/jsm/node-editor/math/OperatorEditor.js
  34. 0 44
      examples/jsm/node-editor/math/PowerEditor.js
  35. 0 45
      examples/jsm/node-editor/math/TrigonometryEditor.js
  36. 0 27
      examples/jsm/node-editor/procedural/CheckerEditor.js
  37. 0 102
      examples/jsm/node-editor/scene/MeshEditor.js
  38. 0 160
      examples/jsm/node-editor/scene/Object3DEditor.js
  39. 0 99
      examples/jsm/node-editor/scene/PointsEditor.js
  40. 0 43
      examples/jsm/node-editor/utils/OscillatorEditor.js
  41. 0 39
      examples/jsm/node-editor/utils/SplitEditor.js
  42. 1 1
      examples/jsm/nodes/code/ScriptableNode.js
  43. 12 4
      examples/jsm/nodes/core/Node.js
  44. 26 18
      examples/jsm/nodes/geometry/RangeNode.js
  45. 1 1
      examples/jsm/nodes/loaders/NodeMaterialLoader.js
  46. 1 0
      examples/jsm/nodes/shadernode/ShaderNode.js
  47. 2 0
      examples/jsm/postprocessing/Pass.js
  48. BIN
      examples/screenshots/webgl_nodes_playground.jpg
  49. BIN
      examples/screenshots/webgpu_nodes_playground.jpg
  50. 0 218
      examples/webgl_nodes_playground.html
  51. 0 220
      examples/webgpu_nodes_playground.html
  52. 2 1
      package.json
  53. 68 57
      playground/BaseNodeEditor.js
  54. 777 0
      playground/NodeEditor.js
  55. 177 0
      playground/NodeEditorLib.js
  56. 421 0
      playground/NodeEditorUtils.js
  57. 2092 0
      playground/Nodes.json
  58. 1 1
      playground/editors/BasicMaterialEditor.js
  59. 10 5
      playground/editors/ColorEditor.js
  60. 94 0
      playground/editors/CustomNodeEditor.js
  61. 70 0
      playground/editors/FileEditor.js
  62. 23 0
      playground/editors/FloatEditor.js
  63. 50 0
      playground/editors/JavaScriptEditor.js
  64. 10 10
      playground/editors/JoinEditor.js
  65. 17 0
      playground/editors/MaterialEditor.js
  66. 208 0
      playground/editors/NodePrototypeEditor.js
  67. 1 1
      playground/editors/PointsMaterialEditor.js
  68. 9 11
      playground/editors/PreviewEditor.js
  69. 480 0
      playground/editors/ScriptableEditor.js
  70. 6 6
      playground/editors/SliderEditor.js
  71. 50 0
      playground/editors/SplitEditor.js
  72. 1 1
      playground/editors/StandardMaterialEditor.js
  73. 35 0
      playground/editors/StringEditor.js
  74. 46 0
      playground/editors/SwizzleEditor.js
  75. 20 55
      playground/editors/TextureEditor.js
  76. 6 6
      playground/editors/TimerEditor.js
  77. 27 0
      playground/editors/UVEditor.js
  78. 24 0
      playground/editors/Vector2Editor.js
  79. 23 0
      playground/editors/Vector3Editor.js
  80. 23 0
      playground/editors/Vector4Editor.js
  81. 98 0
      playground/elements/CodeEditorElement.js
  82. 0 0
      playground/examples/universal/fresnel.json
  83. 0 0
      playground/examples/universal/matcap.json
  84. 0 0
      playground/examples/universal/teapot.json
  85. 0 0
      playground/examples/webgl/car.json
  86. 0 0
      playground/examples/webgpu/particle.json
  87. 0 0
      playground/fonts/open-sans/open-sans-v15-cyrillic-ext_greek_greek-ext_cyrillic_latin_latin-ext_vietnamese-regular.woff
  88. 0 0
      playground/fonts/open-sans/open-sans-v15-cyrillic-ext_greek_greek-ext_cyrillic_latin_latin-ext_vietnamese-regular.woff2
  89. 0 0
      playground/fonts/open-sans/open-sans.css
  90. BIN
      playground/fonts/tabler-icons/fonts/tabler-icons.eot
  91. BIN
      playground/fonts/tabler-icons/fonts/tabler-icons.ttf
  92. BIN
      playground/fonts/tabler-icons/fonts/tabler-icons.woff
  93. BIN
      playground/fonts/tabler-icons/fonts/tabler-icons.woff2
  94. 513 9
      playground/fonts/tabler-icons/tabler-icons.css
  95. 663 6
      playground/fonts/tabler-icons/tabler-icons.html
  96. 3 0
      playground/fonts/tabler-icons/tabler-icons.min.css
  97. 436 11
      playground/fonts/tabler-icons/tabler-icons.scss
  98. 202 0
      playground/index.html
  99. 0 0
      playground/libs/flow.module.js
  100. 0 2
      test/e2e/puppeteer.js

+ 0 - 2
examples/files.json

@@ -245,7 +245,6 @@
 		"webgl_nodes_materials_physical_clearcoat",
 		"webgl_nodes_materials_standard",
 		"webgl_nodes_materialx_noise",
-		"webgl_nodes_playground",
 		"webgl_nodes_points"
 	],
 	"webgl / postprocessing": [
@@ -342,7 +341,6 @@
 		"webgpu_loader_gltf",
 		"webgpu_materials",
 		"webgpu_materials_video",
-		"webgpu_nodes_playground",
 		"webgpu_particles",
 		"webgpu_rtt",
 		"webgpu_sandbox",

BIN
examples/fonts/tabler-icons/fonts/tabler-icons.eot


BIN
examples/fonts/tabler-icons/fonts/tabler-icons.ttf


BIN
examples/fonts/tabler-icons/fonts/tabler-icons.woff


BIN
examples/fonts/tabler-icons/fonts/tabler-icons.woff2


File diff suppressed because it is too large
+ 0 - 3
examples/fonts/tabler-icons/tabler-icons.min.css


+ 0 - 971
examples/jsm/node-editor/NodeEditor.js

@@ -1,971 +0,0 @@
-import { Styles, Canvas, CircleMenu, ButtonInput, StringInput, ContextMenu, Tips, Search, Loader, Node, TreeViewNode, TreeViewInput, Element } from '../libs/flow.module.js';
-import { BasicMaterialEditor } from './materials/BasicMaterialEditor.js';
-import { StandardMaterialEditor } from './materials/StandardMaterialEditor.js';
-import { PointsMaterialEditor } from './materials/PointsMaterialEditor.js';
-import { OperatorEditor } from './math/OperatorEditor.js';
-import { NormalizeEditor } from './math/NormalizeEditor.js';
-import { InvertEditor } from './math/InvertEditor.js';
-import { LimiterEditor } from './math/LimiterEditor.js';
-import { DotEditor } from './math/DotEditor.js';
-import { PowerEditor } from './math/PowerEditor.js';
-import { AngleEditor } from './math/AngleEditor.js';
-import { TrigonometryEditor } from './math/TrigonometryEditor.js';
-import { FloatEditor } from './inputs/FloatEditor.js';
-import { Vector2Editor } from './inputs/Vector2Editor.js';
-import { Vector3Editor } from './inputs/Vector3Editor.js';
-import { Vector4Editor } from './inputs/Vector4Editor.js';
-import { SliderEditor } from './inputs/SliderEditor.js';
-import { ColorEditor } from './inputs/ColorEditor.js';
-import { TextureEditor } from './inputs/TextureEditor.js';
-import { BlendEditor } from './display/BlendEditor.js';
-import { NormalMapEditor } from './display/NormalMapEditor.js';
-import { UVEditor } from './accessors/UVEditor.js';
-import { MatcapUVEditor } from './accessors/MatcapUVEditor.js';
-import { PositionEditor } from './accessors/PositionEditor.js';
-import { NormalEditor } from './accessors/NormalEditor.js';
-import { PreviewEditor } from './utils/PreviewEditor.js';
-import { TimerEditor } from './utils/TimerEditor.js';
-import { OscillatorEditor } from './utils/OscillatorEditor.js';
-import { SplitEditor } from './utils/SplitEditor.js';
-import { JoinEditor } from './utils/JoinEditor.js';
-import { CheckerEditor } from './procedural/CheckerEditor.js';
-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';
-
-export const NodeList = [
-	{
-		name: 'Inputs',
-		icon: 'forms',
-		children: [
-			{
-				name: 'Slider',
-				icon: 'adjustments-horizontal',
-				tags: 'number',
-				nodeClass: SliderEditor
-			},
-			{
-				name: 'Float',
-				icon: 'box-multiple-1',
-				nodeClass: FloatEditor
-			},
-			{
-				name: 'Vector 2',
-				icon: 'box-multiple-2',
-				nodeClass: Vector2Editor
-			},
-			{
-				name: 'Vector 3',
-				icon: 'box-multiple-3',
-				nodeClass: Vector3Editor
-			},
-			{
-				name: 'Vector 4',
-				icon: 'box-multiple-4',
-				nodeClass: Vector4Editor
-			},
-			{
-				name: 'Color',
-				icon: 'palette',
-				nodeClass: ColorEditor
-			},
-			{
-				name: 'Texture',
-				icon: 'photo',
-				nodeClass: TextureEditor
-			},
-			{
-				name: 'File URL',
-				icon: 'cloud-download',
-				nodeClass: FileURLEditor
-			}
-		]
-	},
-	{
-		name: 'Accessors',
-		icon: 'vector-triangle',
-		children: [
-			{
-				name: 'UV',
-				icon: 'details',
-				nodeClass: UVEditor
-			},
-			{
-				name: 'Position',
-				icon: 'hierarchy',
-				nodeClass: PositionEditor
-			},
-			{
-				name: 'Normal',
-				icon: 'fold-up',
-				nodeClass: NormalEditor
-			},
-			{
-				name: 'Matcap UV',
-				icon: 'circle',
-				nodeClass: MatcapUVEditor
-			}
-		]
-	},
-	{
-		name: 'Display',
-		icon: 'brightness',
-		children: [
-			{
-				name: 'Blend',
-				icon: 'layers-subtract',
-				tags: 'mix',
-				nodeClass: BlendEditor
-			},
-			{
-				name: 'Normal Map',
-				icon: 'chart-line',
-				nodeClass: NormalMapEditor
-			}
-		]
-	},
-	{
-		name: 'Math',
-		icon: 'calculator',
-		children: [
-			{
-				name: 'Operator',
-				icon: 'math-symbols',
-				tags: 'addition, subtration, multiplication, division',
-				nodeClass: OperatorEditor
-			},
-			{
-				name: 'Invert',
-				icon: 'flip-vertical',
-				tip: 'Negate',
-				nodeClass: InvertEditor
-			},
-			{
-				name: 'Limiter',
-				icon: 'arrow-bar-to-up',
-				tip: 'Min / Max',
-				nodeClass: LimiterEditor
-			},
-			{
-				name: 'Dot Product',
-				icon: 'arrows-up-left',
-				nodeClass: DotEditor
-			},
-			{
-				name: 'Power',
-				icon: 'arrow-up-right',
-				nodeClass: PowerEditor
-			},
-			{
-				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
-			},
-			{
-				name: 'Normalize',
-				icon: 'fold',
-				nodeClass: NormalizeEditor
-			}
-		]
-	},
-	{
-		name: 'Procedural',
-		icon: 'infinity',
-		children: [
-			{
-				name: 'Checker',
-				icon: 'border-outer',
-				nodeClass: CheckerEditor
-			}
-		]
-	},
-	{
-		name: 'Utils',
-		icon: 'apps',
-		children: [
-			{
-				name: 'Preview',
-				icon: 'square-check',
-				nodeClass: PreviewEditor
-			},
-			{
-				name: 'Timer',
-				icon: 'clock',
-				nodeClass: TimerEditor
-			},
-			{
-				name: 'Oscillator',
-				icon: 'wave-sine',
-				nodeClass: OscillatorEditor
-			},
-			{
-				name: 'Split',
-				icon: 'arrows-split-2',
-				nodeClass: SplitEditor
-			},
-			{
-				name: 'Join',
-				icon: 'arrows-join-2',
-				nodeClass: JoinEditor
-			}
-		]
-	},
-	/*{
-		name: 'Scene',
-		icon: '3d-cube-sphere',
-		children: [
-			{
-				name: 'Mesh',
-				icon: '3d-cube-sphere',
-				nodeClass: MeshEditor
-			}
-		]
-	},*/
-	{
-		name: 'Material',
-		icon: 'circles',
-		children: [
-			{
-				name: 'Basic Material',
-				icon: 'circle',
-				nodeClass: BasicMaterialEditor
-			},
-			{
-				name: 'Standard Material',
-				icon: 'circle',
-				nodeClass: StandardMaterialEditor
-			},
-			{
-				name: 'Points Material',
-				icon: 'circle-dotted',
-				nodeClass: PointsMaterialEditor
-			}
-		]
-	}
-];
-
-export const ClassLib = {
-	BasicMaterialEditor,
-	StandardMaterialEditor,
-	PointsMaterialEditor,
-	PointsEditor,
-	MeshEditor,
-	OperatorEditor,
-	NormalizeEditor,
-	InvertEditor,
-	LimiterEditor,
-	DotEditor,
-	PowerEditor,
-	AngleEditor,
-	TrigonometryEditor,
-	FloatEditor,
-	Vector2Editor,
-	Vector3Editor,
-	Vector4Editor,
-	SliderEditor,
-	ColorEditor,
-	TextureEditor,
-	BlendEditor,
-	NormalMapEditor,
-	UVEditor,
-	MatcapUVEditor,
-	PositionEditor,
-	NormalEditor,
-	TimerEditor,
-	OscillatorEditor,
-	SplitEditor,
-	JoinEditor,
-	CheckerEditor,
-	FileURLEditor
-};
-
-export class NodeEditor extends EventDispatcher {
-
-	constructor( scene = null ) {
-
-		super();
-
-		const domElement = document.createElement( 'flow' );
-		const canvas = new Canvas();
-
-		domElement.append( canvas.dom );
-
-		this.scene = scene;
-
-		this.canvas = canvas;
-		this.domElement = domElement;
-
-		this._preview = false;
-
-		this.search = null;
-
-		this.menu = null;
-		this.previewMenu = null;
-
-		this.nodesContext = null;
-		this.examplesContext = null;
-
-		this._initUpload();
-		this._initTips();
-		this._initMenu();
-		this._initSearch();
-		this._initNodesContext();
-		this._initExamplesContext();
-
-	}
-
-	setSize( width, height ) {
-
-		this.canvas.setSize( width, height );
-
-		return this;
-
-	}
-
-	centralizeNode( node ) {
-
-		const canvas = this.canvas;
-		const nodeRect = node.dom.getBoundingClientRect();
-
-		node.setPosition(
-			( ( canvas.width / 2 ) - canvas.scrollLeft ) - nodeRect.width,
-			( ( canvas.height / 2 ) - canvas.scrollTop ) - nodeRect.height
-		);
-
-		return this;
-
-	}
-
-	add( node ) {
-
-		const onRemove = () => {
-
-			node.removeEventListener( 'remove', onRemove );
-
-			node.setEditor( null );
-
-		};
-
-		node.setEditor( this );
-		node.addEventListener( 'remove', onRemove );
-
-		this.canvas.add( node );
-
-		this.dispatchEvent( { type: 'add', node } );
-
-		return this;
-
-	}
-
-	get nodes() {
-
-		return this.canvas.nodes;
-
-	}
-
-	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() {
-
-		const canvas = this.canvas;
-		canvas.clear();
-		canvas.scrollLeft = 0;
-		canvas.scrollTop = 0;
-		canvas.zoom = 1;
-
-		this.dispatchEvent( { type: 'new' } );
-
-	}
-
-	loadJSON( json ) {
-
-		const canvas = this.canvas;
-
-		canvas.clear();
-
-		canvas.deserialize( json );
-
-		for ( const node of canvas.nodes ) {
-
-			this.add( node );
-
-		}
-
-		this.dispatchEvent( { type: 'load' } );
-
-	}
-
-	_initUpload() {
-
-		const canvas = this.canvas;
-
-		canvas.onDrop( () => {
-
-			for ( const item of canvas.droppedItems ) {
-
-				if ( /^image\//.test( item.type ) === true ) {
-
-					const { relativeClientX, relativeClientY } = canvas;
-
-					const file = item.getAsFile();
-					const fileEditor = new FileEditor( file );
-
-					fileEditor.setPosition(
-						relativeClientX - ( fileEditor.getWidth() / 2 ),
-						relativeClientY - 20
-					);
-
-					this.add( fileEditor );
-
-				}
-
-			}
-
-		} );
-
-	}
-
-	_initTips() {
-
-		this.tips = new Tips();
-
-		this.domElement.append( this.tips.dom );
-
-	}
-
-	_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() );
-
-		newButton.onClick( () => {
-
-			if ( confirm( 'Are you sure?' ) === true ) {
-
-				this.newProject();
-
-			}
-
-		} );
-
-		openButton.onClick( () => {
-
-			const input = document.createElement( 'input' );
-			input.type = 'file';
-
-			input.onchange = e => {
-
-				const file = e.target.files[ 0 ];
-
-				const reader = new FileReader();
-				reader.readAsText( file, 'UTF-8' );
-
-				reader.onload = readerEvent => {
-
-					const loader = new Loader( Loader.OBJECTS );
-					const json = loader.parse( JSON.parse( readerEvent.target.result ), ClassLib );
-
-					this.loadJSON( json );
-
-				};
-
-			};
-
-			input.click();
-
-		} );
-
-		saveButton.onClick( () => {
-
-			exportJSON( this.canvas.toJSON(), 'node_editor' );
-
-		} );
-
-		menu.add( previewButton )
-			.add( newButton )
-			.add( examplesButton )
-			.add( openButton )
-			.add( saveButton )
-			.add( menuButton );
-
-		previewMenu.add( editorButton );
-
-		this.domElement.append( menu.dom );
-
-		this.menu = menu;
-		this.previewMenu = previewMenu;
-
-	}
-
-	_initExamplesContext() {
-
-		const context = new ContextMenu();
-
-		//**************//
-		// MAIN
-		//**************//
-
-		const onClickExample = async ( button ) => {
-
-			this.examplesContext.hide();
-
-			const filename = button.getExtra();
-
-			const loader = new Loader( Loader.OBJECTS );
-			const json = await loader.load( `./jsm/node-editor/examples/${filename}.json`, ClassLib );
-
-			this.loadJSON( json );
-
-		};
-
-		const addExample = ( context, name, filename = null ) => {
-
-			filename = filename || name.replaceAll( ' ', '-' ).toLowerCase();
-
-			context.add( new ButtonInput( name )
-				.setIcon( 'ti ti-file-symlink' )
-				.onClick( onClickExample )
-				.setExtra( filename )
-			);
-
-		};
-
-		//**************//
-		// EXAMPLES
-		//**************//
-
-		const basicContext = new ContextMenu();
-		const advancedContext = new ContextMenu();
-
-		addExample( basicContext, 'Animate UV' );
-		addExample( basicContext, 'Fake top light' );
-		addExample( basicContext, 'Oscillator color' );
-
-		addExample( advancedContext, 'Rim' );
-
-		//**************//
-		// MAIN
-		//**************//
-
-		context.add( new ButtonInput( 'Basic' ), basicContext );
-		context.add( new ButtonInput( 'Advanced' ), advancedContext );
-
-		this.examplesContext = context;
-
-	}
-
-	_initSearch() {
-
-		const traverseNodeEditors = ( item ) => {
-
-			if ( item.nodeClass ) {
-
-				const button = new ButtonInput( item.name );
-				button.setIcon( `ti ti-${item.icon}` );
-				button.addEventListener( 'complete', () => {
-
-					const node = new item.nodeClass();
-
-					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 ) {
-
-				for ( const subItem of item.children ) {
-
-					traverseNodeEditors( subItem );
-
-				}
-
-			}
-
-		};
-
-		const search = new Search();
-		search.forceAutoComplete = true;
-
-		search.onFilter( () => {
-
-			search.clear();
-
-			for ( const item of NodeList ) {
-
-				traverseNodeEditors( item );
-
-			}
-
-			const object3d = this.scene;
-
-			if ( object3d !== null ) {
-
-				object3d.traverse( ( obj3d ) => {
-
-					if ( obj3d.isMesh === true || obj3d.isPoints === true ) {
-
-						let prefix = null;
-						let icon = null;
-						let editorClass = null;
-
-						if ( obj3d.isMesh === true ) {
-
-							prefix = 'Mesh';
-							icon = 'ti ti-3d-cube-sphere';
-							editorClass = MeshEditor;
-
-						} else if ( obj3d.isPoints === true ) {
-
-							prefix = 'Points';
-							icon = 'ti ti-border-none';
-							editorClass = PointsEditor;
-
-						}
-
-						const button = new ButtonInput( `${prefix} - ${obj3d.name}` );
-						button.setIcon( icon );
-						button.addEventListener( 'complete', () => {
-
-							for ( const node of this.canvas.nodes ) {
-
-								if ( node.value === obj3d ) {
-
-									// prevent duplicated node
-
-									this.canvas.select( node );
-
-									return;
-
-								}
-
-							}
-
-							const node = new editorClass( obj3d );
-
-							this.add( node );
-
-							this.centralizeNode( node );
-							this.canvas.select( node );
-
-						} );
-
-						search.add( button );
-
-					}
-
-				} );
-
-			}
-
-		} );
-
-		search.onSubmit( () => {
-
-			if ( search.currentFiltered !== null ) {
-
-				search.currentFiltered.button.dispatchEvent( new Event( 'complete' ) );
-
-			}
-
-		} );
-
-		this.search = search;
-
-		this.domElement.append( search.dom );
-
-	}
-
-	_initNodesContext() {
-
-		const context = new ContextMenu( this.canvas.canvas ).setWidth( 300 );
-
-		let isContext = false;
-		const contextPosition = {};
-
-		const add = ( node ) => {
-
-			if ( isContext ) {
-
-				node.setPosition(
-					Math.round( contextPosition.x ),
-					Math.round( contextPosition.y )
-				);
-
-			} else {
-
-				this.centralizeNode( node );
-				this.canvas.select( node );
-
-			}
-
-			context.hide();
-
-			this.add( node );
-
-			this.canvas.select( node );
-
-			isContext = false;
-
-		};
-
-		context.onContext( () => {
-
-			isContext = true;
-
-			const { relativeClientX, relativeClientY } = this.canvas;
-
-			contextPosition.x = Math.round( relativeClientX );
-			contextPosition.y = Math.round( relativeClientY );
-
-		} );
-
-		context.addEventListener( 'show', () => {
-
-			reset();
-			focus();
-
-		} );
-
-		//**************//
-		// INPUTS
-		//**************//
-
-		const nodeButtons = [];
-
-		let nodeButtonsVisible = [];
-		let nodeButtonsIndex = - 1;
-
-		const focus = () => requestAnimationFrame( () => search.inputDOM.focus() );
-		const reset = () => {
-
-			search.setValue( '' );
-
-			for ( const button of nodeButtons ) {
-
-				button.setOpened( false ).setVisible( true ).setSelected( false );
-
-			}
-
-		};
-
-		const node = new Node();
-		context.add( node );
-
-		const search = new StringInput().setPlaceHolder( 'Search...' ).setIcon( 'ti ti-list-search' );
-
-		search.inputDOM.addEventListener( 'keydown', e => {
-
-			const key = e.key;
-
-			if ( key === 'ArrowDown' ) {
-
-				const previous = nodeButtonsVisible[ nodeButtonsIndex ];
-				if ( previous ) previous.setSelected( false );
-
-				const current = nodeButtonsVisible[ nodeButtonsIndex = ( nodeButtonsIndex + 1 ) % nodeButtonsVisible.length ];
-				if ( current ) current.setSelected( true );
-
-				e.preventDefault();
-				e.stopImmediatePropagation();
-
-			} else if ( key === 'ArrowUp' ) {
-
-				const previous = nodeButtonsVisible[ nodeButtonsIndex ];
-				if ( previous ) previous.setSelected( false );
-
-				const current = nodeButtonsVisible[ nodeButtonsIndex > 0 ? -- nodeButtonsIndex : ( nodeButtonsIndex = nodeButtonsVisible.length - 1 ) ];
-				if ( current ) current.setSelected( true );
-
-				e.preventDefault();
-				e.stopImmediatePropagation();
-
-			} else if ( key === 'Enter' ) {
-
-				if ( nodeButtonsVisible[ nodeButtonsIndex ] !== undefined ) {
-
-					nodeButtonsVisible[ nodeButtonsIndex ].dom.click();
-
-					e.preventDefault();
-					e.stopImmediatePropagation();
-
-				}
-
-			}
-
-		} );
-
-		search.onChange( () => {
-
-			const value = search.getValue().toLowerCase();
-
-			if ( value.length === 0 ) return reset();
-
-			nodeButtonsVisible = [];
-			nodeButtonsIndex = 0;
-
-			for ( const button of nodeButtons ) {
-
-				const buttonLabel = button.getLabel().toLowerCase();
-
-				button.setVisible( false ).setSelected( false );
-
-				let visible = buttonLabel.indexOf( value ) !== - 1;
-
-				if ( visible && button.parent !== null ) {
-
-					nodeButtonsVisible.push( button );
-
-				}
-
-			}
-
-			for ( const button of nodeButtonsVisible ) {
-
-				let parent = button;
-
-				while ( parent !== null ) {
-
-					parent.setOpened( true ).setVisible( true );
-
-					parent = parent.parent;
-
-				}
-
-			}
-
-			if ( nodeButtonsVisible[ nodeButtonsIndex ] !== undefined ) {
-
-				nodeButtonsVisible[ nodeButtonsIndex ].setSelected( true );
-
-			}
-
-		} );
-
-		const treeView = new TreeViewInput();
-		node.add( new Element().setHeight( 30 ).add( search ) );
-		node.add( new Element().setHeight( 200 ).add( treeView ) );
-
-		const createButtonMenu = ( item ) => {
-
-			const button = new TreeViewNode( item.name );
-			button.setIcon( `ti ti-${item.icon}` );
-
-			if ( item.nodeClass ) {
-
-				button.onClick( () => add( new item.nodeClass() ) );
-
-			}
-
-			if ( item.tip ) {
-
-				//button.setToolTip( item.tip );
-
-			}
-
-			nodeButtons.push( button );
-
-			if ( item.children ) {
-
-				for ( const subItem of item.children ) {
-
-					const subButton = createButtonMenu( subItem );
-
-					button.add( subButton );
-
-				}
-
-			}
-
-			return button;
-
-		};
-
-		for ( const item of NodeList ) {
-
-			const button = createButtonMenu( item );
-
-			treeView.add( button );
-
-		}
-
-		this.nodesContext = context;
-
-	}
-
-}

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

@@ -1,12 +0,0 @@
-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();
-
-};

+ 0 - 14
examples/jsm/node-editor/accessors/MatcapUVEditor.js

@@ -1,14 +0,0 @@
-import { BaseNode } from '../core/BaseNode.js';
-import { MatcapUVNode } from 'three/nodes';
-
-export class MatcapUVEditor extends BaseNode {
-
-	constructor() {
-
-		const node = new MatcapUVNode();
-
-		super( 'Matcap UV', 2, node, 200 );
-
-	}
-
-}

+ 0 - 30
examples/jsm/node-editor/accessors/NormalEditor.js

@@ -1,30 +0,0 @@
-import { SelectInput, Element } from '../../libs/flow.module.js';
-import { BaseNode } from '../core/BaseNode.js';
-import { NormalNode } from 'three/nodes';
-
-export class NormalEditor extends BaseNode {
-
-	constructor() {
-
-		const node = new NormalNode();
-
-		super( 'Normal', 3, node, 200 );
-
-		const optionsField = new SelectInput( [
-			{ name: 'Local', value: NormalNode.LOCAL },
-			{ name: 'World', value: NormalNode.WORLD },
-			{ name: 'View', value: NormalNode.VIEW },
-			{ name: 'Geometry', value: NormalNode.GEOMETRY }
-		], NormalNode.LOCAL ).onChange( () => {
-
-			node.scope = optionsField.getValue();
-
-			this.invalidate();
-
-		} );
-
-		this.add( new Element().add( optionsField ) );
-
-	}
-
-}

+ 0 - 30
examples/jsm/node-editor/accessors/PositionEditor.js

@@ -1,30 +0,0 @@
-import { SelectInput, Element } from '../../libs/flow.module.js';
-import { BaseNode } from '../core/BaseNode.js';
-import { PositionNode } from 'three/nodes';
-
-export class PositionEditor extends BaseNode {
-
-	constructor() {
-
-		const node = new PositionNode();
-
-		super( 'Position', 3, node, 200 );
-
-		const optionsField = new SelectInput( [
-			{ name: 'Local', value: PositionNode.LOCAL },
-			{ name: 'World', value: PositionNode.WORLD },
-			{ name: 'View', value: PositionNode.VIEW },
-			{ name: 'View Direction', value: PositionNode.VIEW_DIRECTION }
-		], PositionNode.LOCAL ).onChange( () => {
-
-			node.scope = optionsField.getValue();
-
-			this.invalidate();
-
-		} );
-
-		this.add( new Element().add( optionsField ) );
-
-	}
-
-}

+ 0 - 25
examples/jsm/node-editor/accessors/UVEditor.js

@@ -1,25 +0,0 @@
-import { SelectInput, LabelElement } from '../../libs/flow.module.js';
-import { BaseNode } from '../core/BaseNode.js';
-import { UVNode } from 'three/nodes';
-
-export class UVEditor extends BaseNode {
-
-	constructor() {
-
-		const node = new UVNode();
-
-		super( 'UV', 2, node, 200 );
-
-		const optionsField = new SelectInput( [ '1', '2' ], 0 ).onChange( () => {
-
-			node.index = Number( optionsField.getValue() );
-
-			this.invalidate();
-
-		} );
-
-		this.add( new LabelElement( 'Channel' ).add( optionsField ) );
-
-	}
-
-}

+ 0 - 59
examples/jsm/node-editor/core/DataFile.js

@@ -1,59 +0,0 @@
-export class DataFile {
-
-	constructor( value ) {
-
-		this.isDataFile = true;
-
-		this.value = value;
-		this.url = null;
-
-	}
-
-	setValue( value ) {
-
-		this.value = value;
-		this.url = null;
-
-	}
-
-	isURL( uri ) {
-
-		const pattern = new RegExp( '^((ft|htt)ps?:\\/\\/)?' + // protocol
-			'((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|' + // domain name and extension
-			'((\\d{1,3}\\.){3}\\d{1,3}))' + // OR ip (v4) address
-			'(\\:\\d+)?' + // port
-			'(\\/[-a-z\\d%@_.~+&:]*)*' + // path
-			'(\\?[;&a-z\\d%@_.,~+&:=-]*)?' + // query string
-			'(\\#[-a-z\\d_]*)?$', 'i' ); // fragment locator
-
-		return pattern.test( uri );
-
-	}
-
-	getURL() {
-
-		let url = this.url;
-
-		if ( url === null ) {
-
-			const value = this.value;
-
-			if ( value instanceof File ) {
-
-				url = URL.createObjectURL( value );
-
-			} else {
-
-				url = value;
-
-			}
-
-			this.url = this.isURL( url ) ? url : null;
-
-		}
-
-		return url;
-
-	}
-
-}

+ 0 - 20
examples/jsm/node-editor/core/FileEditor.js

@@ -1,20 +0,0 @@
-import { StringInput, Element } from '../../libs/flow.module.js';
-import { BaseNode } from './BaseNode.js';
-import { DataFile } from './DataFile.js';
-
-export class FileEditor extends BaseNode {
-
-	constructor( file ) {
-
-		const dataFile = new DataFile( file );
-
-		super( 'File', 1, dataFile, 250 );
-
-		this.file = file;
-		this.nameInput = new StringInput( file.name ).setReadOnly( true );
-
-		this.add( new Element().add( this.nameInput ) );
-
-	}
-
-}

+ 0 - 29
examples/jsm/node-editor/core/FileURLEditor.js

@@ -1,29 +0,0 @@
-import { StringInput, Element } from '../../libs/flow.module.js';
-import { BaseNode } from './BaseNode.js';
-import { DataFile } from './DataFile.js';
-
-export class FileURLEditor extends BaseNode {
-
-	constructor() {
-
-		const dataFile = new DataFile();
-
-		super( 'File URL', 1, dataFile, 250 );
-
-		const urlInput = new StringInput().onChange( () => {
-
-			if ( urlInput.getValue() !== dataFile.getURL() ) {
-
-				dataFile.setValue( urlInput.getValue() );
-
-				this.invalidate();
-
-			}
-
-		} );
-
-		this.add( new Element().add( urlInput ) );
-
-	}
-
-}

+ 0 - 44
examples/jsm/node-editor/display/BlendEditor.js

@@ -1,44 +0,0 @@
-import { LabelElement } from '../../libs/flow.module.js';
-import { BaseNode } from '../core/BaseNode.js';
-import { MathNode, UniformNode } from 'three/nodes';
-
-const NULL_VALUE = new UniformNode( 0 );
-const ONE_VALUE = new UniformNode( 1 );
-
-export class BlendEditor extends BaseNode {
-
-	constructor() {
-
-		const node = new MathNode( MathNode.MIX, NULL_VALUE, NULL_VALUE, ONE_VALUE );
-
-		super( 'Blend', 3, node, 200 );
-
-		const aElement = new LabelElement( 'Base' ).setInput( 3 );
-		const bElement = new LabelElement( 'Blend' ).setInput( 3 );
-		const cElement = new LabelElement( 'Opacity' ).setInput( 1 );
-
-		aElement.onConnect( () => {
-
-			node.aNode = aElement.getLinkedObject() || NULL_VALUE;
-
-		} );
-
-		bElement.onConnect( () => {
-
-			node.bNode = bElement.getLinkedObject() || NULL_VALUE;
-
-		} );
-
-		cElement.onConnect( () => {
-
-			node.cNode = cElement.getLinkedObject() || ONE_VALUE;
-
-		} );
-
-		this.add( aElement )
-			.add( bElement )
-			.add( cElement );
-
-	}
-
-}

+ 0 - 49
examples/jsm/node-editor/display/NormalMapEditor.js

@@ -1,49 +0,0 @@
-import { SelectInput, Element, LabelElement } from '../../libs/flow.module.js';
-import { BaseNode } from '../core/BaseNode.js';
-import { NormalMapNode, ConstNode } from 'three/nodes';
-import { TangentSpaceNormalMap, ObjectSpaceNormalMap } from 'three';
-
-const nullValue = new ConstNode( 0 );
-
-export class NormalMapEditor extends BaseNode {
-
-	constructor() {
-
-		const node = new NormalMapNode( nullValue );
-
-		super( 'Normal Map', 3, node, 175 );
-
-		const source = new LabelElement( 'Source' ).setInput( 3 ).onConnect( () => {
-
-			node.node = source.getLinkedObject() || nullValue;
-
-			this.invalidate();
-
-		} );
-
-		const scale = new LabelElement( 'Scale' ).setInput( 3 ).onConnect( () => {
-
-			node.scaleNode = scale.getLinkedObject();
-
-			this.invalidate();
-
-		} );
-
-		const optionsField = new SelectInput( [
-			{ name: 'Tangent Space', value: TangentSpaceNormalMap },
-			{ name: 'Object Space', value: ObjectSpaceNormalMap }
-		], TangentSpaceNormalMap ).onChange( () => {
-
-			node.normalMapType = Number( optionsField.getValue() );
-
-			this.invalidate();
-
-		} );
-
-		this.add( new Element().add( optionsField ) )
-			.add( source )
-			.add( scale );
-
-	}
-
-}

File diff suppressed because it is too large
+ 0 - 0
examples/jsm/node-editor/examples/animate-uv.json


File diff suppressed because it is too large
+ 0 - 0
examples/jsm/node-editor/examples/fake-top-light.json


File diff suppressed because it is too large
+ 0 - 0
examples/jsm/node-editor/examples/matcap.json


File diff suppressed because it is too large
+ 0 - 0
examples/jsm/node-editor/examples/oscillator-color.json


File diff suppressed because it is too large
+ 0 - 0
examples/jsm/node-editor/examples/rim.json


+ 0 - 23
examples/jsm/node-editor/inputs/FloatEditor.js

@@ -1,23 +0,0 @@
-import { NumberInput, Element } from '../../libs/flow.module.js';
-import { BaseNode } from '../core/BaseNode.js';
-import { UniformNode } from 'three/nodes';
-
-export class FloatEditor extends BaseNode {
-
-	constructor() {
-
-		const node = new UniformNode( 0 );
-
-		super( 'Float', 1, node, 150 );
-
-		const field = new NumberInput().setTagColor( 'red' ).onChange( () => {
-
-			node.value = field.getValue();
-
-		} );
-
-		this.add( new Element().add( field ) );
-
-	}
-
-}

+ 0 - 28
examples/jsm/node-editor/inputs/Vector2Editor.js

@@ -1,28 +0,0 @@
-import { NumberInput, LabelElement } from '../../libs/flow.module.js';
-import { BaseNode } from '../core/BaseNode.js';
-import { Vector2 } from 'three';
-import { UniformNode } from 'three/nodes';
-
-export class Vector2Editor extends BaseNode {
-
-	constructor() {
-
-		const node = new UniformNode( new Vector2() );
-
-		super( 'Vector 2', 2, node );
-
-		const onUpdate = () => {
-
-			node.value.x = fieldX.getValue();
-			node.value.y = fieldY.getValue();
-
-		};
-
-		const fieldX = new NumberInput().setTagColor( 'red' ).onChange( onUpdate );
-		const fieldY = new NumberInput().setTagColor( 'green' ).onChange( onUpdate );
-
-		this.add( new LabelElement( 'XY' ).add( fieldX ).add( fieldY ) );
-
-	}
-
-}

+ 0 - 30
examples/jsm/node-editor/inputs/Vector3Editor.js

@@ -1,30 +0,0 @@
-import { NumberInput, LabelElement } from '../../libs/flow.module.js';
-import { BaseNode } from '../core/BaseNode.js';
-import { Vector3 } from 'three';
-import { UniformNode } from 'three/nodes';
-
-export class Vector3Editor extends BaseNode {
-
-	constructor() {
-
-		const node = new UniformNode( new Vector3() );
-
-		super( 'Vector 3', 3, node, 325 );
-
-		const onUpdate = () => {
-
-			node.value.x = fieldX.getValue();
-			node.value.y = fieldY.getValue();
-			node.value.z = fieldZ.getValue();
-
-		};
-
-		const fieldX = new NumberInput().setTagColor( 'red' ).onChange( onUpdate );
-		const fieldY = new NumberInput().setTagColor( 'green' ).onChange( onUpdate );
-		const fieldZ = new NumberInput().setTagColor( 'blue' ).onChange( onUpdate );
-
-		this.add( new LabelElement( 'XYZ' ).add( fieldX ).add( fieldY ).add( fieldZ ) );
-
-	}
-
-}

+ 0 - 37
examples/jsm/node-editor/inputs/Vector4Editor.js

@@ -1,37 +0,0 @@
-import { NumberInput, LabelElement } from '../../libs/flow.module.js';
-import { BaseNode } from '../core/BaseNode.js';
-import { Vector4 } from 'three';
-import { UniformNode } from 'three/nodes';
-
-export class Vector4Editor extends BaseNode {
-
-	constructor() {
-
-		const node = new UniformNode( new Vector4() );
-
-		super( 'Vector 4', 4, node, 350 );
-
-		const onUpdate = () => {
-
-			node.value.x = fieldX.getValue();
-			node.value.y = fieldY.getValue();
-			node.value.z = fieldZ.getValue();
-			node.value.w = fieldW.getValue();
-
-		};
-
-		const fieldX = new NumberInput().setTagColor( 'red' ).onChange( onUpdate );
-		const fieldY = new NumberInput().setTagColor( 'green' ).onChange( onUpdate );
-		const fieldZ = new NumberInput().setTagColor( 'blue' ).onChange( onUpdate );
-		const fieldW = new NumberInput( 1 ).setTagColor( 'white' ).onChange( onUpdate );
-
-		this.add( new LabelElement( 'XYZW' )
-			.add( fieldX )
-			.add( fieldY )
-			.add( fieldZ )
-			.add( fieldW )
-		);
-
-	}
-
-}

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

@@ -1,17 +0,0 @@
-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;
-
-	}
-
-}

+ 0 - 40
examples/jsm/node-editor/math/AngleEditor.js

@@ -1,40 +0,0 @@
-import { SelectInput, Element, LabelElement } from '../../libs/flow.module.js';
-import { BaseNode } from '../core/BaseNode.js';
-import { Vector3 } from 'three';
-import { MathNode, UniformNode } from 'three/nodes';
-
-const DEFAULT_VALUE = new UniformNode( new Vector3() );
-
-export class AngleEditor extends BaseNode {
-
-	constructor() {
-
-		const node = new MathNode( MathNode.SIN, DEFAULT_VALUE );
-
-		super( 'Angle', 1, node, 175 );
-
-		const optionsField = new SelectInput( [
-			{ name: 'Degrees to Radians', value: MathNode.RADIANS },
-			{ name: 'Radians to Degrees', value: MathNode.DEGREES }
-		], MathNode.RADIANS ).onChange( () => {
-
-			node.method = optionsField.getValue();
-
-			this.invalidate();
-
-		} );
-
-		const input = new LabelElement( 'A' ).setInput( 1 );
-
-		input.onConnect( () => {
-
-			node.aNode = input.getLinkedObject() || DEFAULT_VALUE;
-
-		} );
-
-		this.add( new Element().add( optionsField ) )
-			.add( input );
-
-	}
-
-}

+ 0 - 35
examples/jsm/node-editor/math/DotEditor.js

@@ -1,35 +0,0 @@
-import { LabelElement } from '../../libs/flow.module.js';
-import { BaseNode } from '../core/BaseNode.js';
-import { MathNode, UniformNode } from 'three/nodes';
-
-const NULL_VALUE = new UniformNode( 0 );
-
-export class DotEditor extends BaseNode {
-
-	constructor() {
-
-		const node = new MathNode( MathNode.DOT, NULL_VALUE, NULL_VALUE );
-
-		super( 'Dot Product', 1, node, 175 );
-
-		const aElement = new LabelElement( 'A' ).setInput( 3 );
-		const bElement = new LabelElement( 'B' ).setInput( 3 );
-
-		aElement.onConnect( () => {
-
-			node.aNode = aElement.getLinkedObject() || NULL_VALUE;
-
-		} );
-
-		bElement.onConnect( () => {
-
-			node.bNode = bElement.getLinkedObject() || NULL_VALUE;
-
-		} );
-
-		this.add( aElement )
-			.add( bElement );
-
-	}
-
-}

+ 0 - 39
examples/jsm/node-editor/math/InvertEditor.js

@@ -1,39 +0,0 @@
-import { SelectInput, Element, LabelElement } from '../../libs/flow.module.js';
-import { BaseNode } from '../core/BaseNode.js';
-import { MathNode, UniformNode } from 'three/nodes';
-
-const DEFAULT_VALUE = new UniformNode( 0 );
-
-export class InvertEditor extends BaseNode {
-
-	constructor() {
-
-		const node = new MathNode( MathNode.INVERT, DEFAULT_VALUE );
-
-		super( 'Invert / Negate', 1, node, 175 );
-
-		const optionsField = new SelectInput( [
-			{ name: 'Invert ( 1 - Source )', value: MathNode.INVERT },
-			{ name: 'Negate ( - Source )', value: MathNode.NEGATE }
-		], MathNode.INVERT ).onChange( () => {
-
-			node.method = optionsField.getValue();
-
-			this.invalidate();
-
-		} );
-
-		const input = new LabelElement( 'Source' ).setInput( 1 );
-
-		input.onConnect( () => {
-
-			node.aNode = input.getLinkedObject() || DEFAULT_VALUE;
-
-		} );
-
-		this.add( new Element().add( optionsField ) )
-			.add( input );
-
-	}
-
-}

+ 0 - 62
examples/jsm/node-editor/math/LimiterEditor.js

@@ -1,62 +0,0 @@
-import { SelectInput, LabelElement, Element, NumberInput } from '../../libs/flow.module.js';
-import { BaseNode } from '../core/BaseNode.js';
-import { MathNode, UniformNode } from 'three/nodes';
-
-export class LimiterEditor extends BaseNode {
-
-	constructor() {
-
-		const NULL_VALUE = new UniformNode( 0 );
-
-		const node = new MathNode( MathNode.MIN, NULL_VALUE, NULL_VALUE );
-
-		super( 'Limiter', 1, node, 175 );
-
-		const methodInput = new SelectInput( [
-			{ name: 'Min', value: MathNode.MIN },
-			{ name: 'Max', value: MathNode.MAX },
-			// { name: 'Clamp', value: MathNode.CLAMP }
-			{ name: 'Saturate', value: MathNode.SATURATE }
-		], MathNode.MIN );
-
-		methodInput.onChange( ( data ) => {
-
-			node.method = data.getValue();
-			bElement.setVisible( data.getValue() !== MathNode.SATURATE );
-
-			this.invalidate();
-
-		} );
-
-		const aElement = new LabelElement( 'A' ).setInput( 1 );
-		const bElement = new LabelElement( 'B' ).setInput( 1 );
-
-		aElement.add( new NumberInput().onChange( ( field ) => {
-
-			node.aNode.value = field.getValue();
-
-		} ) ).onConnect( ( elmt ) => {
-
-			elmt.setEnabledInputs( ! elmt.getLinkedObject() );
-			node.aNode = elmt.getLinkedObject() || NULL_VALUE;
-
-		} );
-
-		bElement.add( new NumberInput().onChange( ( field ) => {
-
-			node.bNode.value = field.getValue();
-
-		} ) ).onConnect( ( elmt ) => {
-
-			elmt.setEnabledInputs( ! elmt.getLinkedObject() );
-			node.bNode = elmt.getLinkedObject() || NULL_VALUE;
-
-		} );
-
-		this.add( new Element().add( methodInput ) )
-			.add( aElement )
-			.add( bElement );
-
-	}
-
-}

+ 0 - 28
examples/jsm/node-editor/math/NormalizeEditor.js

@@ -1,28 +0,0 @@
-import { LabelElement } from '../../libs/flow.module.js';
-import { BaseNode } from '../core/BaseNode.js';
-import { Vector3 } from 'three';
-import { MathNode, UniformNode } from 'three/nodes';
-
-const DEFAULT_VALUE = new UniformNode( new Vector3() );
-
-export class NormalizeEditor extends BaseNode {
-
-	constructor() {
-
-		const node = new MathNode( MathNode.NORMALIZE, DEFAULT_VALUE );
-
-		super( 'Normalize', 3, node, 175 );
-
-		const input = new LabelElement( 'A' ).setInput( 3 );
-
-		input.onConnect( () => {
-
-			node.aNode = input.getLinkedObject() || DEFAULT_VALUE;
-
-		} );
-
-		this.add( input );
-
-	}
-
-}

+ 0 - 63
examples/jsm/node-editor/math/OperatorEditor.js

@@ -1,63 +0,0 @@
-import { Element, LabelElement, NumberInput, SelectInput } from '../../libs/flow.module.js';
-import { UniformNode, OperatorNode } from 'three/nodes';
-import { BaseNode } from '../core/BaseNode.js';
-
-export class OperatorEditor extends BaseNode {
-
-	constructor() {
-
-		const NULL_VALUE = new UniformNode( 0 );
-
-		const node = new OperatorNode( '+', NULL_VALUE, NULL_VALUE );
-
-		super( 'Operator', 1, node, 150 );
-
-		const opInput = new SelectInput( [
-			{ name: 'Addition ( + )', value: '+' },
-			{ name: 'Subtraction ( - )', value: '-' },
-			{ name: 'Multiplication ( * )', value: '*' },
-			{ name: 'Division ( / )', value: '/' }
-		], '+' );
-
-		opInput.onChange( ( data ) => {
-
-			node.op = data.getValue();
-
-			this.invalidate();
-
-		} );
-
-		const aElement = new LabelElement( 'A' ).setInput( 3 );
-		const bElement = new LabelElement( 'B' ).setInput( 3 );
-
-
-		aElement.add( new NumberInput().onChange( ( field ) => {
-
-			node.aNode.value = field.getValue();
-
-		} ) ).onConnect( ( elmt ) => {
-
-			elmt.setEnabledInputs( ! elmt.getLinkedObject() );
-			node.aNode = elmt.getLinkedObject() || NULL_VALUE;
-
-		} );
-
-		bElement.add( new NumberInput().onChange( ( field ) => {
-
-			node.bNode.value = field.getValue();
-
-		} ) ).onConnect( ( elmt ) => {
-
-			elmt.setEnabledInputs( ! elmt.getLinkedObject() );
-			node.bNode = elmt.getLinkedObject() || NULL_VALUE;
-
-		} );
-
-
-		this.add( new Element().add( opInput ) )
-			.add( aElement )
-			.add( bElement );
-
-	}
-
-}

+ 0 - 44
examples/jsm/node-editor/math/PowerEditor.js

@@ -1,44 +0,0 @@
-import { LabelElement, NumberInput } from '../../libs/flow.module.js';
-import { BaseNode } from '../core/BaseNode.js';
-import { MathNode, UniformNode } from 'three/nodes';
-
-export class PowerEditor extends BaseNode {
-
-	constructor() {
-
-		const NULL_VALUE = new UniformNode( 0 );
-		const node = new MathNode( MathNode.POW, NULL_VALUE, NULL_VALUE );
-
-		super( 'Power', 1, node, 175 );
-
-		const aElement = new LabelElement( 'A' ).setInput( 1 );
-		const bElement = new LabelElement( 'B' ).setInput( 1 );
-
-		aElement.add( new NumberInput().onChange( ( field ) => {
-
-			node.aNode.value = field.getValue();
-
-		} ) ).onConnect( ( elmt ) => {
-
-			elmt.setEnabledInputs( ! elmt.getLinkedObject() );
-			node.aNode = elmt.getLinkedObject() || NULL_VALUE;
-
-		} );
-
-		bElement.add( new NumberInput().onChange( ( field ) => {
-
-			node.bNode.value = field.getValue();
-
-		} ) ).onConnect( ( elmt ) => {
-
-			elmt.setEnabledInputs( ! elmt.getLinkedObject() );
-			node.bNode = elmt.getLinkedObject() || NULL_VALUE;
-
-		} );
-
-		this.add( aElement )
-			.add( bElement );
-
-	}
-
-}

+ 0 - 45
examples/jsm/node-editor/math/TrigonometryEditor.js

@@ -1,45 +0,0 @@
-import { SelectInput, Element, LabelElement } from '../../libs/flow.module.js';
-import { BaseNode } from '../core/BaseNode.js';
-import { Vector3 } from 'three';
-import { MathNode, UniformNode } from 'three/nodes';
-
-const DEFAULT_VALUE = new UniformNode( new Vector3() );
-
-export class TrigonometryEditor extends BaseNode {
-
-	constructor() {
-
-		const node = new MathNode( MathNode.SIN, DEFAULT_VALUE );
-
-		super( 'Trigonometry', 1, node, 175 );
-
-		const optionsField = new SelectInput( [
-			{ name: 'Sin', value: MathNode.SIN },
-			{ name: 'Cos', value: MathNode.COS },
-			{ name: 'Tan', value: MathNode.TAN },
-
-			{ name: 'asin', value: MathNode.ASIN },
-			{ name: 'acos', value: MathNode.ACOS },
-			{ name: 'atan', value: MathNode.ATAN }
-		], MathNode.SIN ).onChange( () => {
-
-			node.method = optionsField.getValue();
-
-			this.invalidate();
-
-		} );
-
-		const input = new LabelElement( 'A' ).setInput( 1 );
-
-		input.onConnect( () => {
-
-			node.aNode = input.getLinkedObject() || DEFAULT_VALUE;
-
-		} );
-
-		this.add( new Element().add( optionsField ) )
-			.add( input );
-
-	}
-
-}

+ 0 - 27
examples/jsm/node-editor/procedural/CheckerEditor.js

@@ -1,27 +0,0 @@
-import { LabelElement } from '../../libs/flow.module.js';
-import { BaseNode } from '../core/BaseNode.js';
-import { CheckerNode, UVNode } from 'three/nodes';
-
-const defaultUV = new UVNode();
-
-export class CheckerEditor extends BaseNode {
-
-	constructor() {
-
-		const node = new CheckerNode( defaultUV );
-
-		super( 'Checker', 1, node, 200 );
-
-		const field = new LabelElement( 'UV' ).setInput( 2 );
-
-		field.onConnect( () => {
-
-			node.uvNode = field.getLinkedObject() || defaultUV;
-
-		} );
-
-		this.add( field );
-
-	}
-
-}

+ 0 - 102
examples/jsm/node-editor/scene/MeshEditor.js

@@ -1,102 +0,0 @@
-import { LabelElement } from '../../libs/flow.module.js';
-import { Object3DEditor } from './Object3DEditor.js';
-import { Mesh } from 'three';
-
-export class MeshEditor extends Object3DEditor {
-
-	constructor( mesh = null ) {
-
-		if ( mesh === null ) {
-
-			mesh = new Mesh();
-
-		}
-
-		super( mesh, 'Mesh' );
-
-		this.material = null;
-
-		this.meshMaterial = null;
-		this.defaultMeshMaterial = null;
-
-		this._initMaterial();
-
-		this.updateDefault();
-		this.restoreDefault();
-		this.update();
-
-	}
-
-	get mesh() {
-
-		return this.value;
-
-	}
-
-	_initMaterial() {
-
-		const material = new LabelElement( 'Material' ).setInputColor( 'forestgreen' ).setInput( 1 );
-
-		material.onValid( ( source, target, stage ) => {
-
-			const object = target.getObject();
-
-			if ( object && object.isMaterial !== true ) {
-
-				if ( stage === 'dragged' ) {
-
-					const name = target.node.getName();
-
-					this.editor.tips.error( `"${name}" is not a Material.` );
-
-				}
-
-				return false;
-
-			}
-
-		} ).onConnect( () => {
-
-			this.meshMaterial = material.getLinkedObject() || this.defaultMeshMaterial;
-
-			this.update();
-
-		} );
-
-		this.add( material );
-
-		this.material = material;
-
-	}
-
-	update() {
-
-		super.update();
-
-		const mesh = this.mesh;
-
-		if ( mesh ) {
-
-			mesh.material = this.meshMaterial || this.defaultMeshMaterial;
-
-		}
-
-	}
-
-	updateDefault() {
-
-		super.updateDefault();
-
-		this.defaultMeshMaterial = this.mesh.material;
-
-	}
-
-	restoreDefault() {
-
-		super.restoreDefault();
-
-		this.mesh.material = this.defaultMeshMaterial;
-
-	}
-
-}

+ 0 - 160
examples/jsm/node-editor/scene/Object3DEditor.js

@@ -1,160 +0,0 @@
-import { NumberInput, StringInput, LabelElement } from '../../libs/flow.module.js';
-import { BaseNode } from '../core/BaseNode.js';
-import { Group, MathUtils, Vector3 } from 'three';
-
-export class Object3DEditor extends BaseNode {
-
-	constructor( object3d = null, name = 'Object 3D' ) {
-
-		if ( object3d === null ) {
-
-			object3d = new Group();
-
-		}
-
-		super( name, 1, object3d );
-
-		this.defaultPosition = new Vector3();
-		this.defaultRotation = new Vector3();
-		this.defaultScale = new Vector3( 100, 100, 100 );
-
-		this._initTags();
-		this._initTransform();
-
-		this.onValidElement = () => {};
-
-	}
-
-	setEditor( editor ) {
-
-		if ( this.editor ) {
-
-			this.restoreDefault();
-
-		}
-
-		super.setEditor( editor );
-
-		if ( editor ) {
-
-			const name = this.nameInput.getValue();
-			const object3d = editor.scene.getObjectByName( name );
-
-			this.value = object3d;
-
-			this.updateDefault();
-			this.restoreDefault();
-			this.update();
-
-		}
-
-		return this;
-
-	}
-
-	get object3d() {
-
-		return this.value;
-
-	}
-
-	_initTags() {
-
-		this.nameInput = new StringInput( this.object3d.name ).setReadOnly( true )
-			.onChange( () => this.object3d.name = this.nameInput.getValue() );
-
-		this.add( new LabelElement( 'Name' ).add( this.nameInput ) );
-
-	}
-
-	_initTransform() {
-
-		const update = () => this.update();
-
-		const posX = new NumberInput().setTagColor( 'red' ).onChange( update );
-		const posY = new NumberInput().setTagColor( 'green' ).onChange( update );
-		const posZ = new NumberInput().setTagColor( 'blue' ).onChange( update );
-
-		const rotationStep = 1;
-
-		const rotX = new NumberInput().setTagColor( 'red' ).setStep( rotationStep ).onChange( update );
-		const rotY = new NumberInput().setTagColor( 'green' ).setStep( rotationStep ).onChange( update );
-		const rotZ = new NumberInput().setTagColor( 'blue' ).setStep( rotationStep ).onChange( update );
-
-		const scaleX = new NumberInput( 100 ).setTagColor( 'red' ).setStep( rotationStep ).onChange( update );
-		const scaleY = new NumberInput( 100 ).setTagColor( 'green' ).setStep( rotationStep ).onChange( update );
-		const scaleZ = new NumberInput( 100 ).setTagColor( 'blue' ).setStep( rotationStep ).onChange( update );
-
-		this.add( new LabelElement( 'Position' ).add( posX ).add( posY ).add( posZ ) )
-			.add( new LabelElement( 'Rotation' ).add( rotX ).add( rotY ).add( rotZ ) )
-			.add( new LabelElement( 'Scale' ).add( scaleX ).add( scaleY ).add( scaleZ ) );
-
-		this.posX = posX;
-		this.posY = posY;
-		this.posZ = posZ;
-
-		this.rotX = rotX;
-		this.rotY = rotY;
-		this.rotZ = rotZ;
-
-		this.scaleX = scaleX;
-		this.scaleY = scaleY;
-		this.scaleZ = scaleZ;
-
-	}
-
-	update() {
-
-		const object3d = this.object3d;
-
-		if ( object3d ) {
-
-			const { position, rotation, scale } = object3d;
-
-			position.x = this.posX.getValue();
-			position.y = this.posY.getValue();
-			position.z = this.posZ.getValue();
-
-			rotation.x = MathUtils.degToRad( this.rotX.getValue() );
-			rotation.y = MathUtils.degToRad( this.rotY.getValue() );
-			rotation.z = MathUtils.degToRad( this.rotZ.getValue() );
-
-			scale.x = this.scaleX.getValue() / 100;
-			scale.y = this.scaleY.getValue() / 100;
-			scale.z = this.scaleZ.getValue() / 100;
-
-		}
-
-	}
-
-	updateDefault() {
-
-		const { position, rotation, scale } = this.object3d;
-
-		this.defaultPosition = position.clone();
-		this.defaultRotation = new Vector3( MathUtils.radToDeg( rotation.x ), MathUtils.radToDeg( rotation.y ), MathUtils.radToDeg( rotation.z ) );
-		this.defaultScale = scale.clone().multiplyScalar( 100 );
-
-	}
-
-	restoreDefault() {
-
-		const position = this.defaultPosition;
-		const rotation = this.defaultRotation;
-		const scale = this.defaultScale;
-
-		this.posX.setValue( position.x );
-		this.posY.setValue( position.y );
-		this.posZ.setValue( position.z );
-
-		this.rotX.setValue( rotation.x );
-		this.rotY.setValue( rotation.y );
-		this.rotZ.setValue( rotation.z );
-
-		this.scaleX.setValue( scale.x );
-		this.scaleY.setValue( scale.y );
-		this.scaleZ.setValue( scale.z );
-
-	}
-
-}

+ 0 - 99
examples/jsm/node-editor/scene/PointsEditor.js

@@ -1,99 +0,0 @@
-import { LabelElement } from '../../libs/flow.module.js';
-import { Object3DEditor } from './Object3DEditor.js';
-import { Points } from 'three';
-
-export class PointsEditor extends Object3DEditor {
-
-	constructor( points = null ) {
-
-		if ( points === null ) {
-
-			points = new Points();
-
-		}
-
-		super( points, 'Points' );
-
-		this.material = null;
-
-		this.defaultMaterial = null;
-
-		this._initMaterial();
-
-		this.updateDefault();
-		this.restoreDefault();
-		this.update();
-
-	}
-
-	get points() {
-
-		return this.value;
-
-	}
-
-	_initMaterial() {
-
-		const materialElement = new LabelElement( 'Material' ).setInputColor( 'forestgreen' ).setInput( 1 );
-
-		materialElement.onValid( ( source, target, stage ) => {
-
-			const object = target.getObject();
-
-			if ( object && object.isMaterial !== true ) {
-
-				if ( stage === 'dragged' ) {
-
-					const name = target.node.getName();
-
-					this.editor.tips.error( `"${name}" is not a Material.` );
-
-				}
-
-				return false;
-
-			}
-
-		} ).onConnect( () => {
-
-			this.material = materialElement.getLinkedObject() || this.defaultMaterial;
-
-			this.update();
-
-		} );
-
-		this.add( materialElement );
-
-	}
-
-	update() {
-
-		super.update();
-
-		const points = this.points;
-
-		if ( points ) {
-
-			points.material = this.material || this.defaultMaterial;
-
-		}
-
-	}
-
-	updateDefault() {
-
-		super.updateDefault();
-
-		this.defaultMaterial = this.points.material;
-
-	}
-
-	restoreDefault() {
-
-		super.restoreDefault();
-
-		this.points.material = this.defaultMaterial;
-
-	}
-
-}

+ 0 - 43
examples/jsm/node-editor/utils/OscillatorEditor.js

@@ -1,43 +0,0 @@
-import { SelectInput, LabelElement, Element } from '../../libs/flow.module.js';
-import { BaseNode } from '../core/BaseNode.js';
-import { OscNode, UniformNode } from 'three/nodes';
-
-const NULL_VALUE = new UniformNode( 0 );
-
-export class OscillatorEditor extends BaseNode {
-
-	constructor() {
-
-		const node = new OscNode( OscNode.SINE, NULL_VALUE );
-
-		super( 'Oscillator', 1, node, 175 );
-
-		const methodInput = new SelectInput( [
-			{ name: 'Sine', value: OscNode.SINE },
-			{ name: 'Square', value: OscNode.SQUARE },
-			{ name: 'Triangle', value: OscNode.TRIANGLE },
-			{ name: 'Sawtooth', value: OscNode.SAWTOOTH }
-		], OscNode.SINE );
-
-		methodInput.onChange( () => {
-
-			node.method = methodInput.getValue();
-
-			this.invalidate();
-
-		} );
-
-		const timeElement = new LabelElement( 'Time' ).setInput( 1 );
-
-		timeElement.onConnect( () => {
-
-			node.timeNode = timeElement.getLinkedObject() || NULL_VALUE;
-
-		} );
-
-		this.add( new Element().add( methodInput ) )
-			.add( timeElement );
-
-	}
-
-}

+ 0 - 39
examples/jsm/node-editor/utils/SplitEditor.js

@@ -1,39 +0,0 @@
-import { SelectInput, Element } from '../../libs/flow.module.js';
-import { BaseNode } from '../core/BaseNode.js';
-import { SplitNode, UniformNode } from 'three/nodes';
-
-const NULL_VALUE = new UniformNode( 0 );
-
-export class SplitEditor extends BaseNode {
-
-	constructor() {
-
-		const node = new SplitNode( NULL_VALUE, 'x' );
-
-		super( 'Split', 1, node, 175 );
-
-		const componentsField = new SelectInput( [
-			{ name: 'X | R', value: 'x' },
-			{ name: 'Y | G', value: 'y' },
-			{ name: 'Z | B', value: 'z' },
-			{ name: 'W | A', value: 'w' }
-		], node.components ).onChange( () => {
-
-			node.components = componentsField.getValue();
-
-			this.invalidate();
-
-		} );
-
-		const componentsElement = new Element().add( componentsField ).setInput( 1 )
-			.onConnect( () => {
-
-				node.node = componentsElement.getLinkedObject() || NULL_VALUE;
-
-			} );
-
-		this.add( componentsElement );
-
-	}
-
-}

+ 1 - 1
examples/jsm/nodes/code/ScriptableNode.js

@@ -1,6 +1,6 @@
 import Node, { addNodeClass } from '../core/Node.js';
 import { scriptableValue } from './ScriptableValueNode.js';
-import { addNodeElement, nodeProxy } from '../shadernode/ShaderNode.js';
+import { addNodeElement, nodeProxy, float } from '../shadernode/ShaderNode.js';
 
 class Resources extends Map {
 

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

@@ -42,11 +42,19 @@ class Node {
 
 			if ( index !== undefined ) {
 
-				yield { childNode, replaceNode( node ) { self[ property ][ index ] = node; } };
+				yield { childNode, replaceNode( node ) {
+
+					self[ property ][ index ] = node;
+
+				} };
 
 			} else {
 
-				yield { childNode, replaceNode( node ) { self[ property ] = node; } };
+				yield { childNode, replaceNode( node ) {
+
+					self[ property ] = node;
+
+				} };
 
 			}
 
@@ -306,9 +314,9 @@ class Node {
 					for ( const subProperty in json.inputNodes[ property ] ) {
 
 						const uuid = json.inputNodes[ property ][ subProperty ];
-					
+
 						inputObject[ subProperty ] = nodes[ uuid ];
-					
+
 					}
 
 					this[ property ] = inputObject;

+ 26 - 18
examples/jsm/nodes/geometry/RangeNode.js

@@ -1,50 +1,58 @@
 import Node, { addNodeClass } from '../core/Node.js';
+import { getValueType } from '../core/NodeUtils.js';
 import { attribute } from '../core/AttributeNode.js';
-import { nodeObject, float } from '../shadernode/ShaderNode.js';
+import { nodeProxy, float } from '../shadernode/ShaderNode.js';
 
 import { MathUtils, InstancedBufferAttribute } from 'three';
 
 class RangeNode extends Node {
 
-	constructor( min, max ) {
+	constructor( minNode = float(), maxNode = float() ) {
 
 		super();
 
-		this.min = min;
-		this.max = max;
+		this.minNode = minNode;
+		this.maxNode = maxNode;
 
 	}
 
-	getVectorLength() {
+	getVectorLength( builder ) {
 
-		const min = this.min;
+		const minLength = builder.getTypeLength( getValueType( this.minNode.value ) );
+		const maxLength = builder.getTypeLength( getValueType( this.maxNode.value ) );
 
-		let length = 1;
-
-		if ( min.isVector2 ) length = 2;
-		else if ( min.isVector3 || min.isColor ) length = 3;
-		else if ( min.isVector4 ) length = 4;
-
-		return length;
+		return minLength > maxLength ? minLength : maxLength;
 
 	}
 
 	getNodeType( builder ) {
 
-		return builder.object.isInstancedMesh === true ? builder.getTypeFromLength( this.getVectorLength() ) : 'float';
+		return builder.object.isInstancedMesh === true ? builder.getTypeFromLength( this.getVectorLength( builder ) ) : 'float';
 
 	}
 
 	construct( builder ) {
 
-		const { min, max } = this;
-		const { object, geometry } = builder;
+		const object = builder.object;
 
 		let output = null;
 
 		if ( object.isInstancedMesh === true ) {
 
-			const vectorLength = this.getVectorLength();
+			const geometry = builder.geometry;
+
+			let min = this.minNode.value;
+			let max = this.maxNode.value;
+
+			const minLength = builder.getTypeLength( getValueType( min ) );
+			const maxLength = builder.getTypeLength( getValueType( max ) );
+
+				 if ( minLength > maxLength && maxLength > 1 ) max = new min.constructor().fromArray( min.toArray() );
+			else if ( minLength > maxLength && maxLength === 1 ) max = new min.constructor().setScalar( max );
+			else if ( maxLength > minLength && minLength > 1 ) min = new max.constructor().fromArray( min.toArray() );
+			else if ( maxLength > minLength && minLength === 1 ) min = new max.constructor().setScalar( min );
+
+			const vectorLength = this.getVectorLength( builder );
 			const attributeName = 'node' + this.id;
 
 			const length = vectorLength * object.count;
@@ -109,6 +117,6 @@ class RangeNode extends Node {
 
 export default RangeNode;
 
-export const range = ( min, max ) => nodeObject( new RangeNode( min, max ) );
+export const range = nodeProxy( RangeNode );
 
 addNodeClass( RangeNode );

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

@@ -5,7 +5,7 @@ const superFromTypeFunction = MaterialLoader.createMaterialFromType;
 
 MaterialLoader.createMaterialFromType = function ( type ) {
 
-	const material = createNodeMaterialFromType( type )
+	const material = createNodeMaterialFromType( type );
 
 	if ( material !== undefined ) {
 

+ 1 - 0
examples/jsm/nodes/shadernode/ShaderNode.js

@@ -387,6 +387,7 @@ addNodeElement( 'arrayBuffer', arrayBuffer );
 // HACK - we cannot export them from the corresponding files because of the cyclic dependency
 export const element = nodeProxy( ArrayElementNode );
 export const convert = ( node, types ) => nodeObject( new ConvertNode( nodeObject( node ), types ) );
+export const split = ( node, channels ) => nodeObject( new SplitNode( nodeObject( node ), channels ) );
 
 addNodeElement( 'element', element );
 addNodeElement( 'convert', convert );

+ 2 - 0
examples/jsm/postprocessing/Pass.js

@@ -9,6 +9,8 @@ class Pass {
 
 	constructor() {
 
+		this.isPass = true;
+
 		// if set to true, the pass is processed by the composer
 		this.enabled = true;
 

BIN
examples/screenshots/webgl_nodes_playground.jpg


BIN
examples/screenshots/webgpu_nodes_playground.jpg


+ 0 - 218
examples/webgl_nodes_playground.html

@@ -1,218 +0,0 @@
-<!DOCTYPE html>
-<html lang="en">
-	<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"/>
-		<link rel="stylesheet" href="fonts/tabler-icons/tabler-icons.min.css" type="text/css"/>
-		<link type="text/css" rel="stylesheet" href="main.css">
-		<style>
-			body {
-				overflow: hidden;
-				width: 100%;
-				height: 100%;
-			}
-			.renderer {
-				position: absolute;
-				top: 0;
-				left: 0;
-				height: 100%;
-				width: 100%;
-			}
-			flow {
-				position: absolute;
-				top: 0;
-				left: 0;
-				height: 100%;
-				width: 100%;
-				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>
-
-		<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 type="module">
-
-			import * as THREE from 'three';
-			import { uniform, MeshBasicNodeMaterial, PointsNodeMaterial } from 'three/nodes';
-
-			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 { 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';
-
-			let camera, scene, renderer;
-			let model;
-			let nodeEditor;
-
-			init();
-			animate();
-
-			function init() {
-
-				camera = new THREE.PerspectiveCamera( 40, window.innerWidth / window.innerHeight, 0.01, 100 );
-				camera.position.set( 0.0, 3, 4 * 3 );
-
-				scene = new THREE.Scene();
-				scene.background = new THREE.Color( 0x333333 );
-
-				// Lights
-
-				const topLight = new THREE.PointLight( 0xF4F6F0, 1, 100 );
-				topLight.power = 4500;
-				topLight.position.set( 0, 10, 10 );
-				scene.add( topLight );
-
-				const backLight = new THREE.PointLight( 0x0c1445, 1, 100 );
-				backLight.power = 1000;
-				backLight.position.set( - 1, .2, - 2.6 );
-				scene.add( backLight );
-
-				renderer = new THREE.WebGLRenderer( { antialias: true } );
-				document.body.appendChild( renderer.domElement );
-				renderer.outputEncoding = THREE.sRGBEncoding;
-				renderer.toneMapping = THREE.LinearToneMapping;
-				renderer.toneMappingExposure = 1;
-				renderer.useLegacyLights = false;
-
-				renderer.domElement.className = 'renderer';
-
-				//
-
-				const controls = new OrbitControls( camera, renderer.domElement );
-				controls.minDistance = 5;
-				controls.maxDistance = 30;
-
-				window.addEventListener( 'resize', onWindowResize );
-
-				initEditor();
-
-				onWindowResize();
-
-			}
-
-			function initEditor() {
-
-				nodeEditor = new NodeEditor( scene );
-
-				const reset = () => {
-
-					const meshEditor = new MeshEditor( model );
-					const materialEditor = new StandardMaterialEditor();
-
-					nodeEditor.add( meshEditor );
-					nodeEditor.add( 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 );
-
-				const loaderFBX = new FBXLoader();
-				loaderFBX.load( 'models/fbx/stanford-bunny.fbx', ( object ) => {
-
-					const defaultMaterial = new MeshBasicNodeMaterial();
-					defaultMaterial.colorNode = uniform( 0 );
-
-					const sphere = new THREE.Mesh( new THREE.SphereGeometry( 2, 32, 16 ), defaultMaterial );
-					sphere.name = 'Sphere';
-					sphere.position.set( 5, 0, - 5 );
-					scene.add( sphere );
-
-					const box = new THREE.Mesh( new THREE.BoxGeometry( 2, 2, 2 ), defaultMaterial );
-					box.name = 'Box';
-					box.position.set( - 5, 0, - 5 );
-					scene.add( box );
-
-					const defaultPointsMaterial = new PointsNodeMaterial();
-					defaultPointsMaterial.colorNode = uniform( 0 );
-					defaultPointsMaterial.size = 0.01;
-
-					const torusKnot = new THREE.Points( new THREE.TorusKnotGeometry( 1, .3, 100, 16 ), defaultPointsMaterial );
-					torusKnot.name = 'Torus Knot ( Points )';
-					torusKnot.position.set( 0, 0, - 5 );
-					scene.add( torusKnot );
-
-					model = object.children[ 0 ];
-					model.position.set( 0, 0, .1 );
-					model.scale.setScalar( .01 );
-					model.material = defaultMaterial;
-					scene.add( model );
-
-					reset();
-
-				} );
-
-			}
-
-			function onWindowResize() {
-
-				const width = window.innerWidth;
-				const height = window.innerHeight;
-
-				camera.aspect = width / height;
-				camera.updateProjectionMatrix();
-
-				renderer.setSize( width, height );
-
-				nodeEditor.setSize( width, height );
-
-			}
-
-			//
-
-			function animate() {
-
-				requestAnimationFrame( animate );
-
-				nodeFrame.update();
-
-				render();
-
-			}
-
-			function render() {
-
-				//if ( model ) model.rotation.y = performance.now() / 5000;
-
-				renderer.render( scene, camera );
-
-			}
-
-		</script>
-
-	</body>
-</html>

+ 0 - 220
examples/webgpu_nodes_playground.html

@@ -1,220 +0,0 @@
-<!DOCTYPE html>
-<html lang="en">
-	<head>
-		<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">
-		<link rel="stylesheet" href="fonts/open-sans/open-sans.css" type="text/css"/>
-		<link rel="stylesheet" href="fonts/tabler-icons/tabler-icons.min.css" type="text/css"/>
-		<style>
-			body {
-				overflow: hidden;
-				width: 100%;
-				height: 100%;
-			}
-			.renderer {
-				position: absolute;
-				top: 0;
-				left: 0;
-				height: 100%;
-				width: 100%;
-			}
-			flow {
-				position: absolute;
-				top: 0;
-				left: 0;
-				height: 100%;
-				width: 100%;
-				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>
-
-		<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 type="module">
-
-			import * as THREE from 'three';
-			import { toneMapping, uniform, MeshBasicNodeMaterial, PointsNodeMaterial } from 'three/nodes';
-
-			import WebGPU from 'three/addons/capabilities/WebGPU.js';
-			import WebGPURenderer from 'three/addons/renderers/webgpu/WebGPURenderer.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 { OrbitControls } from 'three/addons/controls/OrbitControls.js';
-			import { FBXLoader } from 'three/addons/loaders/FBXLoader.js';
-
-			// Use PreviewEditor in WebGL for now
-			import { nodeFrame } from 'three/addons/renderers/webgl/nodes/WebGLNodes.js';
-
-			let camera, scene, renderer;
-			let model;
-			let nodeEditor;
-
-			init();
-
-			function init() {
-
-				if ( WebGPU.isAvailable() === false ) {
-
-					document.body.appendChild( WebGPU.getErrorMessage() );
-
-					throw new Error( 'No WebGPU support' );
-
-				}
-
-				camera = new THREE.PerspectiveCamera( 40, window.innerWidth / window.innerHeight, 0.01, 100 );
-				camera.position.set( 0.0, 3, 4 * 3 );
-
-				scene = new THREE.Scene();
-				scene.background = new THREE.Color( 0x333333 );
-
-				// Lights
-
-				const topLight = new THREE.PointLight( 0xF4F6F0, 1, 100 );
-				topLight.power = 4500;
-				topLight.position.set( 0, 10, 10 );
-				scene.add( topLight );
-
-				const backLight = new THREE.PointLight( 0x0c1445, 1, 100 );
-				backLight.power = 1000;
-				backLight.position.set( - 1, .2, - 2.6 );
-				scene.add( backLight );
-
-				renderer = new WebGPURenderer();
-				renderer.setPixelRatio( window.devicePixelRatio );
-				renderer.setSize( window.innerWidth, window.innerHeight );
-				renderer.setAnimationLoop( render );
-				renderer.outputEncoding = THREE.sRGBEncoding;
-				renderer.toneMappingNode = toneMapping( THREE.LinearToneMapping, 1 );
-				document.body.appendChild( renderer.domElement );
-
-				renderer.domElement.className = 'renderer';
-
-				//
-
-				const controls = new OrbitControls( camera, renderer.domElement );
-				controls.minDistance = 5;
-				controls.maxDistance = 30;
-
-				window.addEventListener( 'resize', onWindowResize );
-
-				initEditor();
-
-				onWindowResize();
-
-			}
-
-			function initEditor() {
-
-				nodeEditor = new NodeEditor( scene );
-
-				const reset = () => {
-
-					const meshEditor = new MeshEditor( model );
-					const materialEditor = new StandardMaterialEditor();
-
-					nodeEditor.add( meshEditor );
-					nodeEditor.add( 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 );
-
-				const loaderFBX = new FBXLoader();
-				loaderFBX.load( 'models/fbx/stanford-bunny.fbx', ( object ) => {
-
-					const defaultMaterial = new MeshBasicNodeMaterial();
-					defaultMaterial.colorNode = uniform( 0 );
-
-					const sphere = new THREE.Mesh( new THREE.SphereGeometry( 2, 32, 16 ), defaultMaterial );
-					sphere.name = 'Sphere';
-					sphere.position.set( 5, 0, - 5 );
-					scene.add( sphere );
-
-					const box = new THREE.Mesh( new THREE.BoxGeometry( 2, 2, 2 ), defaultMaterial );
-					box.name = 'Box';
-					box.position.set( - 5, 0, - 5 );
-					scene.add( box );
-
-					const defaultPointsMaterial = new PointsNodeMaterial();
-					defaultPointsMaterial.colorNode = uniform( 0 );
-
-					const torusKnot = new THREE.Points( new THREE.TorusKnotGeometry( 1, .3, 100, 16 ), defaultPointsMaterial );
-					torusKnot.name = 'Torus Knot ( Points )';
-					torusKnot.position.set( 0, 0, - 5 );
-					scene.add( torusKnot );
-
-					model = object.children[ 0 ];
-					model.position.set( 0, 0, .1 );
-					model.scale.setScalar( .01 );
-					model.material = defaultMaterial;
-					scene.add( model );
-
-					reset();
-
-				} );
-
-			}
-
-			function onWindowResize() {
-
-				const width = window.innerWidth;
-				const height = window.innerHeight;
-
-				camera.aspect = width / height;
-				camera.updateProjectionMatrix();
-
-				renderer.setSize( width, height );
-
-				nodeEditor.setSize( width, height );
-
-			}
-
-			//
-
-			function render() {
-
-				//if ( model ) model.rotation.y = performance.now() / 5000;
-
-				nodeFrame.update();
-
-				renderer.render( scene, camera );
-
-			}
-
-		</script>
-	</body>
-</html>

+ 2 - 1
package.json

@@ -52,11 +52,12 @@
     "lint-examples": "eslint examples --ext .html",
     "lint-docs": "eslint docs --ignore-pattern prettify.js",
     "lint-editor": "eslint editor --ignore-pattern libs",
+    "lint-playground": "eslint playground --ignore-pattern libs",
     "lint-manual": "eslint manual --ignore-pattern 3rdparty --ignore-pattern prettify.js --ignore-pattern shapefile.js",
     "lint-test": "eslint test --ignore-pattern vendor",
     "lint-utils": "eslint utils",
     "lint": "npm run lint-core",
-    "lint-fix": "npm run lint-core -- --fix && npm run lint-addons -- --fix && npm run lint-examples -- --fix && npm run lint-docs -- --fix && npm run lint-editor -- --fix && npm run lint-manual -- --fix && npm run lint-test -- --fix && npm run lint-utils -- --fix",
+    "lint-fix": "npm run lint-core -- --fix && npm run lint-addons -- --fix && npm run lint-examples -- --fix && npm run lint-docs -- --fix && npm run lint-editor -- --fix && npm run lint-playground -- --fix && npm run lint-manual -- --fix && npm run lint-test -- --fix && npm run lint-utils -- --fix",
     "test-unit": "qunit -r failonlyreporter -f !-webonly test/unit/three.source.unit.js",
     "test-e2e": "node test/e2e/puppeteer.js",
     "test-e2e-cov": "node test/e2e/check-coverage.js",

+ 68 - 57
examples/jsm/node-editor/core/BaseNode.js → playground/BaseNodeEditor.js

@@ -1,21 +1,9 @@
-import { Node, ButtonInput, TitleElement, ContextMenu } from '../../libs/flow.module.js';
-import { exportJSON } from '../NodeEditorUtils.js';
+import { Node, ButtonInput, TitleElement, ContextMenu } from 'flow';
+import { exportJSON, onValidNode, getColorFromValue, getTypeFromValue, getColorFromType } from './NodeEditorUtils.js';
 
-export const onNodeValidElement = ( inputElement, outputElement ) => {
+export class BaseNodeEditor extends Node {
 
-	const outputObject = outputElement.getObject();
-
-	if ( ! outputObject || ! outputObject.isNode ) {
-
-		return false;
-
-	}
-
-};
-
-export class BaseNode extends Node {
-
-	constructor( name, outputLength, value = null, width = 300 ) {
+	constructor( name, value = null, width = 300 ) {
 
 		super();
 
@@ -27,10 +15,12 @@ export class BaseNode extends Node {
 
 		this.setWidth( width );
 
+		this.outputLength = 1;
+
 		const title = new TitleElement( name )
 			.setObjectCallback( getObjectCallback )
 			.setSerializable( false )
-			.setOutput( outputLength );
+			.setOutput( this.outputLength );
 
 		const contextButton = new ButtonInput().onClick( () => {
 
@@ -42,19 +32,27 @@ export class BaseNode extends Node {
 
 			context.removeEventListener( 'show', onAddButtons );
 
-			if ( this.value && typeof this.value.toJSON === 'function' ) {
+			context.add( new ButtonInput( 'Remove' ).setIcon( 'ti ti-trash' ).onClick( () => {
+
+				this.dispose();
+
+			} ) );
+
+			if ( this.hasJSON() ) {
 
 				this.context.add( new ButtonInput( 'Export' ).setIcon( 'ti ti-download' ).onClick( () => {
 
-					exportJSON( this.value.toJSON(), this.constructor.name );
+					exportJSON( this.exportJSON(), this.constructor.name );
 
 				} ) );
 
 			}
 
-			context.add( new ButtonInput( 'Remove' ).setIcon( 'ti ti-trash' ).onClick( () => {
+			context.add( new ButtonInput( 'Isolate' ).setIcon( 'ti ti-3d-cube-sphere' ).onClick( () => {
 
-				this.dispose();
+				this.context.hide();
+
+				this.title.dom.dispatchEvent( new MouseEvent( 'dblclick' ) );
 
 			} ) );
 
@@ -65,6 +63,8 @@ export class BaseNode extends Node {
 
 		this.title = title;
 
+		if ( this.icon ) this.setIcon( 'ti ti-' + this.icon );
+
 		this.contextButton = contextButton;
 		this.context = context;
 
@@ -72,19 +72,37 @@ export class BaseNode extends Node {
 
 		this.add( title );
 
-		this.setOutputColor( this.getColorValueFromValue( value ) );
-
 		this.editor = null;
 
 		this.value = value;
 
-		this.onValidElement = onNodeValidElement;
+		this.onValidElement = onValidNode;
+
+		this.updateOutputConnection();
+
+	}
+
+	getOutputType() {
+
+		return getTypeFromValue( this.value );
 
 	}
 
 	getColor() {
 
-		return ( this.getColorValueFromValue( this.value ) || '#777777' ) + 'BB';
+		return ( getColorFromType( this.getOutputType() ) || '#777777' ) + 'BB';
+
+	}
+
+	hasJSON() {
+
+		return this.value && typeof this.value.toJSON === 'function';
+
+	}
+
+	exportJSON() {
+
+		return this.value.toJSON();
 
 	}
 
@@ -108,28 +126,9 @@ export class BaseNode extends Node {
 
 		this.editor = value;
 
-		return this;
-
-	}
-
-	getColorValueFromValue( value ) {
-
-		if ( ! value ) return;
-
-		if ( value.isMaterial === true ) {
-
-			//return 'forestgreen';
-			return '#228b22';
-
-		} else if ( value.isObject3D === true ) {
+		this.dispatchEvent( new Event( 'editor' ) );
 
-			return '#ffa500';
-
-		} else if ( value.isDataFile === true ) {
-
-			return '#00ffff';
-
-		}
+		return this;
 
 	}
 
@@ -149,12 +148,30 @@ export class BaseNode extends Node {
 
 	}
 
+	setIcon( value ) {
+
+		this.title.setIcon( 'ti ti-' + value );
+
+		return this;
+
+	}
+
 	getName() {
 
 		return this.title.getTitle();
 
 	}
 
+	setOutputLength( value ) {
+
+		this.outputLength = value;
+
+		this.updateOutputConnection();
+
+		return;
+
+	}
+
 	setObjectCallback( callback ) {
 
 		this.title.setObjectCallback( callback );
@@ -177,19 +194,11 @@ export class BaseNode extends Node {
 
 	}
 
-	setOutputLength( length ) {
+	updateOutputConnection() {
 
-		this.title.setOutput( length );
+		this.title.setOutputColor( getColorFromValue( this.value ) );
 
-		return this;
-
-	}
-
-	setOutputColor( color ) {
-
-		this.title.setOutputColor( color );
-
-		return this;
+		this.title.setOutput( this.value ? this.outputLength : 0 );
 
 	}
 
@@ -201,6 +210,8 @@ export class BaseNode extends Node {
 
 	dispose() {
 
+		this.setEditor( null );
+
 		this.context.hide();
 
 		super.dispose();

+ 777 - 0
playground/NodeEditor.js

@@ -0,0 +1,777 @@
+import * as THREE from 'three';
+import * as Nodes from 'three/nodes';
+import { Canvas, CircleMenu, ButtonInput, StringInput, ContextMenu, Tips, Search, Loader, Node, TreeViewNode, TreeViewInput, Element } from 'flow';
+import { FileEditor } from './editors/FileEditor.js';
+import { exportJSON } from './NodeEditorUtils.js';
+import { init, ClassLib, getNodeEditorClass, getNodeList } from './NodeEditorLib.js';
+
+init();
+
+Element.icons.unlink = 'ti ti-unlink';
+
+export class NodeEditor extends THREE.EventDispatcher {
+
+	constructor( scene = null, renderer = null, composer = null ) {
+
+		super();
+
+		const domElement = document.createElement( 'flow' );
+		const canvas = new Canvas();
+
+		domElement.append( canvas.dom );
+
+		this.scene = scene;
+		this.renderer = renderer;
+
+		const { global } = Nodes;
+
+		global.set( 'THREE', THREE );
+		global.set( 'TSL', Nodes );
+
+		global.set( 'scene', scene );
+		global.set( 'renderer', renderer );
+		global.set( 'composer', composer );
+
+		this.nodeClasses = [];
+
+		this.canvas = canvas;
+		this.domElement = domElement;
+
+		this._preview = false;
+
+		this.search = null;
+
+		this.menu = null;
+		this.previewMenu = null;
+
+		this.nodesContext = null;
+		this.examplesContext = null;
+
+		this._initUpload();
+		this._initTips();
+		this._initMenu();
+		this._initSearch();
+		this._initNodesContext();
+		this._initExamplesContext();
+		this._initShortcuts();
+		this._initParams();
+
+	}
+
+	setSize( width, height ) {
+
+		this.canvas.setSize( width, height );
+
+		return this;
+
+	}
+
+	centralizeNode( node ) {
+
+		const canvas = this.canvas;
+		const nodeRect = node.dom.getBoundingClientRect();
+
+		node.setPosition(
+			( ( canvas.width / 2 ) - canvas.scrollLeft ) - nodeRect.width,
+			( ( canvas.height / 2 ) - canvas.scrollTop ) - nodeRect.height
+		);
+
+		return this;
+
+	}
+
+	add( node ) {
+
+		const onRemove = () => {
+
+			node.removeEventListener( 'remove', onRemove );
+
+			node.setEditor( null );
+
+		};
+
+		node.setEditor( this );
+		node.addEventListener( 'remove', onRemove );
+
+		this.canvas.add( node );
+
+		this.dispatchEvent( { type: 'add', node } );
+
+		return this;
+
+	}
+
+	get nodes() {
+
+		return this.canvas.nodes;
+
+	}
+
+	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() {
+
+		const canvas = this.canvas;
+		canvas.clear();
+		canvas.scrollLeft = 0;
+		canvas.scrollTop = 0;
+		canvas.zoom = 1;
+
+		this.dispatchEvent( { type: 'new' } );
+
+	}
+
+	async loadURL( url ) {
+
+		const loader = new Loader( Loader.OBJECTS );
+		const json = await loader.load( url, ClassLib );
+
+		this.loadJSON( json );
+
+	}
+
+	loadJSON( json ) {
+
+		const canvas = this.canvas;
+
+		canvas.clear();
+
+		canvas.deserialize( json );
+
+		for ( const node of canvas.nodes ) {
+
+			this.add( node );
+
+		}
+
+		this.dispatchEvent( { type: 'load' } );
+
+	}
+
+	_initUpload() {
+
+		const canvas = this.canvas;
+
+		canvas.onDrop( () => {
+
+			for ( const item of canvas.droppedItems ) {
+
+				const { relativeClientX, relativeClientY } = canvas;
+
+				const file = item.getAsFile();
+				const reader = new FileReader();
+
+				reader.onload = () => {
+
+					const fileEditor = new FileEditor( reader.result, file.name );
+
+					fileEditor.setPosition(
+						relativeClientX - ( fileEditor.getWidth() / 2 ),
+						relativeClientY - 20
+					);
+
+					this.add( fileEditor );
+
+				};
+
+				reader.readAsArrayBuffer( file );
+
+			}
+
+		} );
+
+	}
+
+	_initTips() {
+
+		this.tips = new Tips();
+
+		this.domElement.append( this.tips.dom );
+
+	}
+
+	_initMenu() {
+
+		const menu = new CircleMenu();
+		const previewMenu = new CircleMenu();
+
+		menu.setAlign( 'top left' );
+		previewMenu.setAlign( 'top left' );
+
+		const previewButton = new ButtonInput().setIcon( 'ti ti-brand-threejs' ).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() );
+
+		newButton.onClick( () => {
+
+			if ( confirm( 'Are you sure?' ) === true ) {
+
+				this.newProject();
+
+			}
+
+		} );
+
+		openButton.onClick( () => {
+
+			const input = document.createElement( 'input' );
+			input.type = 'file';
+
+			input.onchange = e => {
+
+				const file = e.target.files[ 0 ];
+
+				const reader = new FileReader();
+				reader.readAsText( file, 'UTF-8' );
+
+				reader.onload = readerEvent => {
+
+					const loader = new Loader( Loader.OBJECTS );
+					const json = loader.parse( JSON.parse( readerEvent.target.result ), ClassLib );
+
+					this.loadJSON( json );
+
+				};
+
+			};
+
+			input.click();
+
+		} );
+
+		saveButton.onClick( () => {
+
+			exportJSON( this.canvas.toJSON(), 'node_editor' );
+
+		} );
+
+		menu.add( previewButton )
+			.add( newButton )
+			.add( examplesButton )
+			.add( openButton )
+			.add( saveButton )
+			.add( menuButton );
+
+		previewMenu.add( editorButton );
+
+		this.domElement.append( menu.dom );
+
+		this.menu = menu;
+		this.previewMenu = previewMenu;
+
+	}
+
+	_initExamplesContext() {
+
+		const context = new ContextMenu();
+
+		//**************//
+		// MAIN
+		//**************//
+
+		const onClickExample = async ( button ) => {
+
+			this.examplesContext.hide();
+
+			const filename = button.getExtra();
+
+			this.loadURL( `./examples/${filename}.json` );
+
+		};
+
+		const addExamples = ( category, names ) => {
+
+			const subContext = new ContextMenu();
+
+			for ( const name of names ) {
+
+				const filename = name.replaceAll( ' ', '-' ).toLowerCase();
+
+				subContext.add( new ButtonInput( name )
+					.setIcon( 'ti ti-file-symlink' )
+					.onClick( onClickExample )
+					.setExtra( category.toLowerCase() + '/' + filename )
+				);
+
+			}
+
+			context.add( new ButtonInput( category ), subContext );
+
+			return subContext;
+
+		};
+
+		//**************//
+		// EXAMPLES
+		//**************//
+
+		addExamples( 'Universal', [
+			'Teapot',
+			'Matcap',
+			'Fresnel'
+		] );
+
+		if ( this.renderer.isWebGLRenderer ) {
+
+			addExamples( 'WebGL', [
+				'Car'
+			] );
+
+			context.add( new ButtonInput( 'WebGPU Version' ).onClick( () => {
+
+				if ( confirm( 'Are you sure?' ) === true ) {
+
+					window.location.search = '?backend=webgpu';
+
+				}
+
+			} ) );
+
+		} else if ( this.renderer.isWebGPURenderer ) {
+
+			addExamples( 'WebGPU', [
+				'Particle'
+			] );
+
+			context.add( new ButtonInput( 'WebGL Version' ).onClick( () => {
+
+				if ( confirm( 'Are you sure?' ) === true ) {
+
+					window.location.search = '';
+
+				}
+
+			} ) );
+
+		}
+
+		this.examplesContext = context;
+
+	}
+
+	_initShortcuts() {
+
+		document.addEventListener( 'keydown', ( e ) => {
+
+			if ( e.target === document.body ) {
+
+				const key = e.key;
+
+				if ( key === 'Tab' ) {
+
+					this.search.inputDOM.focus();
+
+					e.preventDefault();
+					e.stopImmediatePropagation();
+
+				} else if ( key === ' ' ) {
+
+					this.preview = ! this.preview;
+
+				}
+
+			}
+
+		} );
+
+	}
+
+	_initParams() {
+
+		const urlParams = new URLSearchParams( window.location.search );
+
+		const example = urlParams.get( 'example' ) || 'universal/teapot';
+
+		this.loadURL( `./examples/${example}.json` );
+
+	}
+
+	addClass( nodeData ) {
+
+		this.removeClass( nodeData );
+
+		this.nodeClasses.push( nodeData );
+
+		ClassLib[ nodeData.name ] = nodeData.nodeClass;
+
+		return this;
+
+	}
+
+	removeClass( nodeData ) {
+
+		const index = this.nodeClasses.indexOf( nodeData );
+
+		if ( index !== - 1 ) {
+
+			this.nodeClasses.splice( index, 1 );
+
+			delete ClassLib[ nodeData.name ];
+
+		}
+
+		return this;
+
+	}
+
+	_initSearch() {
+
+		const traverseNodeEditors = ( item ) => {
+
+			if ( item.children ) {
+
+				for ( const subItem of item.children ) {
+
+					traverseNodeEditors( subItem );
+
+				}
+
+			} else {
+
+				const button = new ButtonInput( item.name );
+				button.setIcon( `ti ti-${item.icon}` );
+				button.addEventListener( 'complete', async () => {
+
+					const nodeClass = await getNodeEditorClass( item );
+
+					const node = new nodeClass();
+
+					this.add( node );
+
+					this.centralizeNode( node );
+					this.canvas.select( node );
+
+				} );
+
+				search.add( button );
+
+				if ( item.tags !== undefined ) {
+
+					search.setTag( button, item.tags );
+
+				}
+
+			}
+
+
+
+		};
+
+		const search = new Search();
+		search.forceAutoComplete = true;
+
+		search.onFilter( async () => {
+
+			search.clear();
+
+			const nodeList = await getNodeList();
+
+			for ( const item of nodeList.nodes ) {
+
+				traverseNodeEditors( item );
+
+			}
+
+			for ( const item of this.nodeClasses ) {
+
+				traverseNodeEditors( item );
+
+			}
+
+		} );
+
+		search.onSubmit( () => {
+
+			if ( search.currentFiltered !== null ) {
+
+				search.currentFiltered.button.dispatchEvent( new Event( 'complete' ) );
+
+			}
+
+		} );
+
+		this.search = search;
+
+		this.domElement.append( search.dom );
+
+	}
+
+	async _initNodesContext() {
+
+		const context = new ContextMenu( this.canvas.canvas ).setWidth( 300 );
+
+		let isContext = false;
+		const contextPosition = {};
+
+		const add = ( node ) => {
+
+			context.hide();
+
+			this.add( node );
+
+			if ( isContext ) {
+
+				node.setPosition(
+					Math.round( contextPosition.x ),
+					Math.round( contextPosition.y )
+				);
+
+			} else {
+
+				this.centralizeNode( node );
+
+			}
+
+			this.canvas.select( node );
+
+			isContext = false;
+
+		};
+
+		context.onContext( () => {
+
+			isContext = true;
+
+			const { relativeClientX, relativeClientY } = this.canvas;
+
+			contextPosition.x = Math.round( relativeClientX );
+			contextPosition.y = Math.round( relativeClientY );
+
+		} );
+
+		context.addEventListener( 'show', () => {
+
+			reset();
+			focus();
+
+		} );
+
+		//**************//
+		// INPUTS
+		//**************//
+
+		const nodeButtons = [];
+
+		let nodeButtonsVisible = [];
+		let nodeButtonsIndex = - 1;
+
+		const focus = () => requestAnimationFrame( () => search.inputDOM.focus() );
+		const reset = () => {
+
+			search.setValue( '', false );
+
+			for ( const button of nodeButtons ) {
+
+				button.setOpened( false ).setVisible( true ).setSelected( false );
+
+			}
+
+		};
+
+		const node = new Node();
+		context.add( node );
+
+		const search = new StringInput().setPlaceHolder( 'Search...' ).setIcon( 'ti ti-list-search' );
+
+		search.inputDOM.addEventListener( 'keydown', e => {
+
+			const key = e.key;
+
+			if ( key === 'ArrowDown' ) {
+
+				const previous = nodeButtonsVisible[ nodeButtonsIndex ];
+				if ( previous ) previous.setSelected( false );
+
+				const current = nodeButtonsVisible[ nodeButtonsIndex = ( nodeButtonsIndex + 1 ) % nodeButtonsVisible.length ];
+				if ( current ) current.setSelected( true );
+
+				e.preventDefault();
+				e.stopImmediatePropagation();
+
+			} else if ( key === 'ArrowUp' ) {
+
+				const previous = nodeButtonsVisible[ nodeButtonsIndex ];
+				if ( previous ) previous.setSelected( false );
+
+				const current = nodeButtonsVisible[ nodeButtonsIndex > 0 ? -- nodeButtonsIndex : ( nodeButtonsIndex = nodeButtonsVisible.length - 1 ) ];
+				if ( current ) current.setSelected( true );
+
+				e.preventDefault();
+				e.stopImmediatePropagation();
+
+			} else if ( key === 'Enter' ) {
+
+				if ( nodeButtonsVisible[ nodeButtonsIndex ] !== undefined ) {
+
+					nodeButtonsVisible[ nodeButtonsIndex ].dom.click();
+
+				} else {
+
+					context.hide();
+
+				}
+
+				e.preventDefault();
+				e.stopImmediatePropagation();
+
+			} else if ( key === 'Escape' ) {
+
+				context.hide();
+
+			}
+
+		} );
+
+		search.onChange( () => {
+
+			const value = search.getValue().toLowerCase();
+
+			if ( value.length === 0 ) return reset();
+
+			nodeButtonsVisible = [];
+			nodeButtonsIndex = 0;
+
+			for ( const button of nodeButtons ) {
+
+				const buttonLabel = button.getLabel().toLowerCase();
+
+				button.setVisible( false ).setSelected( false );
+
+				const visible = buttonLabel.indexOf( value ) !== - 1;
+
+				if ( visible && button.parent !== null ) {
+
+					nodeButtonsVisible.push( button );
+
+				}
+
+			}
+
+			for ( const button of nodeButtonsVisible ) {
+
+				let parent = button;
+
+				while ( parent !== null ) {
+
+					parent.setOpened( true ).setVisible( true );
+
+					parent = parent.parent;
+
+				}
+
+			}
+
+			if ( nodeButtonsVisible[ nodeButtonsIndex ] !== undefined ) {
+
+				nodeButtonsVisible[ nodeButtonsIndex ].setSelected( true );
+
+			}
+
+		} );
+
+		const treeView = new TreeViewInput();
+		node.add( new Element().setHeight( 30 ).add( search ) );
+		node.add( new Element().setHeight( 200 ).add( treeView ) );
+
+		const addNodeEditorElement = ( nodeData ) => {
+
+			const button = new TreeViewNode( nodeData.name );
+			button.setIcon( `ti ti-${nodeData.icon}` );
+
+			if ( nodeData.children === undefined ) {
+
+				button.isNodeClass = true;
+				button.onClick( async () => {
+
+					const nodeClass = await getNodeEditorClass( nodeData );
+
+					add( new nodeClass() );
+
+				} );
+
+			}
+
+			if ( nodeData.tip ) {
+
+				//button.setToolTip( item.tip );
+
+			}
+
+			nodeButtons.push( button );
+
+			if ( nodeData.children ) {
+
+				for ( const subItem of nodeData.children ) {
+
+					const subButton = addNodeEditorElement( subItem );
+
+					button.add( subButton );
+
+				}
+
+			}
+
+			return button;
+
+		};
+
+		//
+
+		const nodeList = await getNodeList();
+
+		for ( const node of nodeList.nodes ) {
+
+			const button = addNodeEditorElement( node );
+
+			treeView.add( button );
+
+		}
+
+		this.nodesContext = context;
+
+	}
+
+}

+ 177 - 0
playground/NodeEditorLib.js

@@ -0,0 +1,177 @@
+import { NodePrototypeEditor } from './editors/NodePrototypeEditor.js';
+import { ScriptableEditor } from './editors/ScriptableEditor.js';
+import { BasicMaterialEditor } from './editors/BasicMaterialEditor.js';
+import { StandardMaterialEditor } from './editors/StandardMaterialEditor.js';
+import { PointsMaterialEditor } from './editors/PointsMaterialEditor.js';
+import { FloatEditor } from './editors/FloatEditor.js';
+import { Vector2Editor } from './editors/Vector2Editor.js';
+import { Vector3Editor } from './editors/Vector3Editor.js';
+import { Vector4Editor } from './editors/Vector4Editor.js';
+import { SliderEditor } from './editors/SliderEditor.js';
+import { ColorEditor } from './editors/ColorEditor.js';
+import { TextureEditor } from './editors/TextureEditor.js';
+import { UVEditor } from './editors/UVEditor.js';
+import { PreviewEditor } from './editors/PreviewEditor.js';
+import { TimerEditor } from './editors/TimerEditor.js';
+import { SplitEditor } from './editors/SplitEditor.js';
+import { SwizzleEditor } from './editors/SwizzleEditor.js';
+import { JoinEditor } from './editors/JoinEditor.js';
+import { StringEditor } from './editors/StringEditor.js';
+import { FileEditor } from './editors/FileEditor.js';
+import { CustomNodeEditor } from './editors/CustomNodeEditor.js';
+
+export const ClassLib = {
+	BasicMaterialEditor,
+	StandardMaterialEditor,
+	PointsMaterialEditor,
+	FloatEditor,
+	Vector2Editor,
+	Vector3Editor,
+	Vector4Editor,
+	SliderEditor,
+	ColorEditor,
+	TextureEditor,
+	UVEditor,
+	TimerEditor,
+	SplitEditor,
+	SwizzleEditor,
+	JoinEditor,
+	StringEditor,
+	FileEditor,
+	ScriptableEditor,
+	PreviewEditor,
+	NodePrototypeEditor
+};
+
+let nodeList = null;
+let nodeListLoading = false;
+
+export const getNodeList = async () => {
+
+	if ( nodeList === null ) {
+
+		if ( nodeListLoading === false ) {
+
+			nodeListLoading = true;
+
+			const response = await fetch( './Nodes.json' );
+			nodeList = await response.json();
+
+		} else {
+
+			await new Promise( res => {
+
+				const verifyNodeList = () => {
+
+					if ( nodeList !== null ) {
+
+						res();
+
+					} else {
+
+						window.requestAnimationFrame( verifyNodeList );
+
+					}
+
+				};
+
+				verifyNodeList();
+
+			} );
+
+		}
+
+	}
+
+	return nodeList;
+
+};
+
+export const init = async () => {
+
+	const nodeList = await getNodeList();
+
+	const traverseNodeEditors = ( list ) => {
+
+		for ( const node of list ) {
+
+			getNodeEditorClass( node );
+
+			if ( Array.isArray( node.children ) ) {
+
+				traverseNodeEditors( node.children );
+
+			}
+
+		}
+
+	};
+
+	traverseNodeEditors( nodeList.nodes );
+
+};
+
+export const getNodeEditorClass = async ( nodeData ) => {
+
+	const editorClass = nodeData.editorClass || nodeData.name.replace( / /g, '' );
+
+	//
+
+	let nodeClass = ClassLib[ editorClass ];
+
+	if ( nodeClass !== undefined ) {
+
+		if ( nodeData.editorClass !== undefined ) {
+
+			nodeClass.prototype.icon = nodeData.icon;
+
+		}
+
+		return nodeClass;
+
+	}
+
+	//
+
+	if ( nodeData.editorURL ) {
+
+		const moduleEditor = await import( nodeData.editorURL );
+		const moduleName = nodeData.editorClass || Object.keys( moduleEditor )[ 0 ];
+
+		nodeClass = moduleEditor[ moduleName ];
+
+	} else if ( nodeData.shaderNode ) {
+
+		const createNodeEditorClass = ( nodeData ) => {
+
+			return class extends CustomNodeEditor {
+
+				constructor() {
+
+					super( nodeData );
+
+				}
+
+				get className() {
+
+					return editorClass;
+
+				}
+
+			};
+
+		};
+
+		nodeClass = createNodeEditorClass( nodeData );
+
+	}
+
+	if ( nodeClass !== null ) {
+
+		ClassLib[ editorClass ] = nodeClass;
+
+	}
+
+	return nodeClass;
+
+};

+ 421 - 0
playground/NodeEditorUtils.js

@@ -0,0 +1,421 @@
+import { StringInput, NumberInput, ColorInput, Element, LabelElement } from 'flow';
+import { string, float, vec2, vec3, vec4, color } from 'three/nodes';
+
+export function 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();
+
+}
+
+export function disposeScene( scene ) {
+
+	scene.traverse( object => {
+
+		if ( ! object.isMesh ) return;
+
+		object.geometry.dispose();
+
+		if ( object.material.isMaterial ) {
+
+			disposeMaterial( object.material );
+
+		} else {
+
+			for ( const material of object.material ) {
+
+				disposeMaterial( material );
+
+			}
+
+		}
+
+	} );
+
+}
+
+export function disposeMaterial( material )	{
+
+	material.dispose();
+
+	for ( const key of Object.keys( material ) ) {
+
+		const value = material[ key ];
+
+		if ( value && typeof value === 'object' && typeof value.dispose === 'function' ) {
+
+			value.dispose();
+
+		}
+
+	}
+
+}
+
+export const colorLib = {
+	// gpu
+	string: '#ff0000',
+	// cpu
+	Material: '#228b22',
+	Object3D: '#00a1ff',
+	CodeNode: '#ff00ff',
+	Texture: '#ffa500',
+	URL: '#ff0080',
+	String: '#ff0000'
+};
+
+export function getColorFromType( type ) {
+
+	return colorLib[ type ];
+
+}
+
+export function getTypeFromValue( value ) {
+
+	if ( value && value.isScriptableValueNode ) value = value.value;
+	if ( ! value ) return;
+
+	if ( value.isNode && value.nodeType === 'string' ) return 'string';
+	if ( value.isNode && value.nodeType === 'ArrayBuffer' ) return 'URL';
+
+	for ( const type of Object.keys( colorLib ).reverse() ) {
+
+		if ( value[ 'is' + type ] === true ) return type;
+
+	}
+
+}
+
+export function getColorFromValue( value ) {
+
+	const type = getTypeFromValue( value );
+
+	return getColorFromType( type );
+
+}
+
+export const createColorInput = ( node, element ) => {
+
+	const input = new ColorInput().onChange( () => {
+
+		node.value.setHex( input.getValue() );
+
+		element.dispatchEvent( new Event( 'changeInput' ) );
+
+	} );
+
+	element.add( input );
+
+};
+
+export const createFloatInput = ( node, element ) => {
+
+	const input = new NumberInput().onChange( () => {
+
+		node.value = input.getValue();
+
+		element.dispatchEvent( new Event( 'changeInput' ) );
+
+	} );
+
+	element.add( input );
+
+};
+
+export const createStringInput = ( node, element, settings = {} ) => {
+
+	const input = new StringInput().onChange( () => {
+
+		let value = input.getValue();
+
+		if ( settings.transform === 'lowercase' ) value = value.toLowerCase();
+		else if ( settings.transform === 'uppercase' ) value = value.toUpperCase();
+
+		node.value = value;
+
+		element.dispatchEvent( new Event( 'changeInput' ) );
+
+	} );
+
+	element.add( input );
+
+	if ( settings.options ) {
+
+		for ( const option of settings.options ) {
+
+			input.addOption( option );
+
+		}
+
+	}
+
+	const field = input.getInput();
+
+	if ( settings.allows ) field.addEventListener( 'input', () => field.value = field.value.replace( new RegExp( '[^\\s' + settings.allows + ']', 'gi' ), '' ) );
+	if ( settings.maxLength ) field.maxLength = settings.maxLength;
+	if ( settings.transform ) field.style[ 'text-transform' ] = settings.transform;
+
+};
+
+export const createVector2Input = ( node, element ) => {
+
+	const onUpdate = () => {
+
+		node.value.x = fieldX.getValue();
+		node.value.y = fieldY.getValue();
+
+		element.dispatchEvent( new Event( 'changeInput' ) );
+
+	};
+
+	const fieldX = new NumberInput().setTagColor( 'red' ).onChange( onUpdate );
+	const fieldY = new NumberInput().setTagColor( 'green' ).onChange( onUpdate );
+
+	element.add( fieldX ).add( fieldY );
+
+};
+
+export const createVector3Input = ( node, element ) => {
+
+	const onUpdate = () => {
+
+		node.value.x = fieldX.getValue();
+		node.value.y = fieldY.getValue();
+		node.value.z = fieldZ.getValue();
+
+		element.dispatchEvent( new Event( 'changeInput' ) );
+
+	};
+
+	const fieldX = new NumberInput().setTagColor( 'red' ).onChange( onUpdate );
+	const fieldY = new NumberInput().setTagColor( 'green' ).onChange( onUpdate );
+	const fieldZ = new NumberInput().setTagColor( 'blue' ).onChange( onUpdate );
+
+	element.add( fieldX ).add( fieldY ).add( fieldZ );
+
+};
+
+export const createVector4Input = ( node, element ) => {
+
+	const onUpdate = () => {
+
+		node.value.x = fieldX.getValue();
+		node.value.y = fieldY.getValue();
+		node.value.z = fieldZ.getValue();
+		node.value.w = fieldZ.getValue();
+
+		element.dispatchEvent( new Event( 'changeInput' ) );
+
+	};
+
+	const fieldX = new NumberInput().setTagColor( 'red' ).onChange( onUpdate );
+	const fieldY = new NumberInput().setTagColor( 'green' ).onChange( onUpdate );
+	const fieldZ = new NumberInput().setTagColor( 'blue' ).onChange( onUpdate );
+	const fieldW = new NumberInput( 1 ).setTagColor( 'white' ).onChange( onUpdate );
+
+	element.add( fieldX ).add( fieldY ).add( fieldZ ).add( fieldW );
+
+};
+
+
+export const createInputLib = {
+	// gpu
+	string: createStringInput,
+	float: createFloatInput,
+	vec2: createVector2Input,
+	vec3: createVector3Input,
+	vec4: createVector4Input,
+	color: createColorInput,
+	// cpu
+	Number: createFloatInput,
+	String: createStringInput,
+	Vector2: createVector2Input,
+	Vector3: createVector3Input,
+	Vector4: createVector4Input,
+	Color: createColorInput
+};
+
+export const inputNodeLib = {
+	// gpu
+	string,
+	float,
+	vec2,
+	vec3,
+	vec4,
+	color,
+	// cpu
+	Number: float,
+	String: string,
+	Vector2: vec2,
+	Vector3: vec3,
+	Vector4: vec4,
+	Color: color
+};
+
+export function createElementFromJSON( json ) {
+
+	const { inputType, outputType, nullable } = json;
+
+	const id = json.id || json.name;
+	const element = json.name ? new LabelElement( json.name ) : new Element();
+	const field = nullable !== true && json.field !== false;
+
+	//
+
+	let inputNode = null;
+
+	if ( nullable !== true && inputNodeLib[ inputType ] !== undefined ) {
+
+		inputNode = inputNodeLib[ inputType ]();
+
+	}
+
+	element.value = inputNode;
+
+	//
+
+	if ( json.height ) element.setHeight( json.height );
+
+	if ( inputType ) {
+
+		if ( field && createInputLib[ inputType ] ) {
+
+			createInputLib[ inputType ]( inputNode, element, json );
+
+		}
+
+		element.onConnect( () => {
+
+			const externalNode = element.getLinkedObject();
+
+			element.setEnabledInputs( externalNode === null );
+
+			element.value = externalNode || inputNode;
+
+		} );
+
+	}
+
+	//
+
+	if ( inputType && json.inputConnection !== false ) {
+
+		element.setInputColor( getColorFromType( inputType ) );
+		//element.setInputStyle( 'dotted' ); // 'border-style: dotted;'
+		element.setInput( 1 );
+
+		element.onValid( onValidType( inputType ) );
+
+	}
+
+	if ( outputType ) {
+
+		element.setInputColor( getColorFromType( outputType ) );
+		//element.setInputStyle( 'dotted' ); // 'border-style: dotted;'
+		element.setOutput( 1 );
+
+	}
+
+	return { id, element, inputNode, inputType, outputType };
+
+}
+
+export function isGPUNode( object ) {
+
+	return object && object.isNode === true && object.isCodeNode !== true && object.nodeType !== 'string' && object.nodeType !== 'ArrayBuffer';
+
+}
+
+export function isValidTypeToType( sourceType, targetType ) {
+
+	if ( sourceType === targetType ) return true;
+
+	return false;
+
+}
+
+export const onValidNode = onValidType();
+
+export function onValidType( types = 'node', node = null ) {
+
+	return ( source, target, stage ) => {
+
+		const targetObject = target.getObject();
+
+		if ( targetObject ) {
+
+			for ( const type of types.split( '|' ) ) {
+
+				let object = targetObject;
+
+				if ( object.isScriptableValueNode ) {
+
+					if ( object.outputType ) {
+
+						if ( isValidTypeToType( object.outputType, type ) ) {
+
+							return true;
+
+						}
+
+					}
+
+					object = object.value;
+
+				}
+
+				if ( object === null || object === undefined ) continue;
+
+				let isValid = false;
+
+				if ( type === 'any' ) {
+
+					isValid = true;
+
+				} else if ( type === 'node' ) {
+
+					isValid = isGPUNode( object );
+
+				} else if ( type === 'string' || type === 'String' ) {
+
+					isValid = object.nodeType === 'string';
+
+				} else if ( type === 'Number' ) {
+
+					isValid = object.isInputNode && typeof object.value === 'number';
+
+				} else if ( type === 'URL' ) {
+
+					isValid = object.nodeType === 'string' || object.nodeType === 'ArrayBuffer';
+
+				} else if ( object[ 'is' + type ] === true ) {
+
+					isValid = true;
+
+				}
+
+				if ( isValid ) return true;
+
+			}
+
+			if ( node !== null && stage === 'dragged' ) {
+
+				const name = target.node.getName();
+
+				node.editor.tips.error( `"${name}" is not a "${types}".` );
+
+			}
+
+			return false;
+
+		}
+
+	};
+
+}

+ 2092 - 0
playground/Nodes.json

@@ -0,0 +1,2092 @@
+{
+	"nodes": [
+		{
+			"name": "Inputs",
+			"icon": "stack-push",
+			"children": [
+				{
+					"name": "Primitives",
+					"icon": "forms",
+					"children": [
+						{
+							"name": "Color",
+							"icon": "palette",
+							"editorClass": "ColorEditor"
+						},
+						{
+							"name": "Float",
+							"icon": "box-multiple-1",
+							"editorClass": "FloatEditor"
+						},
+						{
+							"name": "Slider",
+							"icon": "adjustments-horizontal",
+							"editorClass": "SliderEditor"
+						},
+						{
+							"name": "String",
+							"icon": "forms",
+							"editorClass": "StringEditor"
+						},
+						{
+							"name": "Texture",
+							"icon": "photo",
+							"editorClass": "TextureEditor"
+						},
+						{
+							"name": "Vector 2",
+							"icon": "box-multiple-2",
+							"editorClass": "Vector2Editor"
+						},
+						{
+							"name": "Vector 3",
+							"icon": "box-multiple-3",
+							"editorClass": "Vector3Editor"
+						},
+						{
+							"name": "Vector 4",
+							"icon": "box-multiple-4",
+							"editorClass": "Vector4Editor"
+						}
+					]
+				},
+				{
+					"name": "Camera",
+					"icon": "video",
+					"children": [
+						{
+							"name": "Camera Normal Matrix",
+							"icon": "video",
+							"nodeType": "mat4",
+							"shaderNode": "cameraNormalMatrix"
+						},
+						{
+							"name": "Camera Position",
+							"icon": "video",
+							"description": "Returns the global transform of the camera.",
+							"nodeType": "vec3",
+							"shaderNode": "cameraPosition"
+						},
+						{
+							"name": "Camera Projection Matrix",
+							"icon": "video",
+							"nodeType": "mat4",
+							"description": "Returns the matrix which contains the projection.",
+							"shaderNode": "cameraProjectionMatrix"
+						},
+						{
+							"name": "Camera View Matrix",
+							"icon": "video",
+							"nodeType": "mat4",
+							"shaderNode": "cameraViewMatrix"
+						},
+						{
+							"name": "Camera World Matrix",
+							"icon": "video",
+							"description": "Returns the global transform of the camera.",
+							"nodeType": "mat4",
+							"shaderNode": "cameraWorldMatrix"
+						}
+					]
+				},
+				{
+					"name": "Material Elements",
+					"icon": "circles-filled",
+					"children": [
+						{
+							"name": "Material Alpha Test",
+							"icon": "circle-filled",
+							"description": "Return the `alphaTest` value of the Material.",
+							"nodeType": "float",
+							"shaderNode": "materialAlphaTest"
+						},
+						{
+							"name": "Material Color",
+							"icon": "circle-filled",
+							"description": "Return the `emissive * emissiveMap` value of the Material",
+							"nodeType": "color",
+							"shaderNode": "materialColor"
+						},
+						{
+							"name": "Material Emissive",
+							"icon": "circle-filled",
+							"description": "Return the `emissive * emissiveMap` value of the Material.",
+							"nodeType": "color",
+							"shaderNode": "materialEmissive"
+						},
+						{
+							"name": "Material Metalness",
+							"icon": "circle-filled",
+							"description": "Return the `metalness * metalnessMap.b` value of the Material.",
+							"nodeType": "float",
+							"shaderNode": "materialMetalness"
+						},
+						{
+							"name": "Material Normal",
+							"icon": "circle-filled",
+							"nodeType": "float",
+							"shaderNode": "materialNormal"
+						},
+						{
+							"name": "Material Opacity",
+							"icon": "circle-filled",
+							"description": "Return the `opacity * alphaMap` value of the Material.",
+							"nodeType": "float",
+							"shaderNode": "materialOpacity"
+						},
+						{
+							"name": "Material Reflectivity",
+							"icon": "circle-filled",
+							"nodeType": "float",
+							"shaderNode": "materialReflectivity"
+						},
+						{
+							"name": "Material Rotation",
+							"icon": "circle-filled",
+							"nodeType": "float",
+							"shaderNode": "materialRotation"
+						},
+						{
+							"name": "Material Roughness",
+							"icon": "circle-filled",
+							"description": "Return the `roughness * roughnessMap.g` value of the Material.",
+							"nodeType": "float",
+							"shaderNode": "materialRoughness"
+						},
+						{
+							"name": "Material Shininess",
+							"icon": "circle-filled",
+							"nodeType": "float",
+							"shaderNode": "materialShininess"
+						},
+						{
+							"name": "Material Specular Color",
+							"icon": "circle-filled",
+							"nodeType": "color",
+							"shaderNode": "materialSpecularColor"
+						},
+						{
+							"name": "Material UV",
+							"icon": "circle-filled",
+							"nodeType": "vec2",
+							"shaderNode": "materialUV"
+						}
+					]
+				},
+				{
+					"name": "Normal",
+					"icon": "arrow-bar-up",
+					"children": [
+						{
+							"name": "Normal Geometry",
+							"icon": "arrow-bar-up",
+							"description": "Return the normal vector from the coordinate space Geometry.",
+							"nodeType": "vec3",
+							"shaderNode": "normalGeometry"
+						},
+						{
+							"name": "Normal Local",
+							"icon": "arrow-bar-up",
+							"description": "Return the normal vector from the coordinate space Local.",
+							"nodeType": "vec3",
+							"shaderNode": "normalLocal"
+						},
+						{
+							"name": "Normal View",
+							"icon": "arrow-bar-up",
+							"description": "Return the normal vector from the coordinate space View.",
+							"nodeType": "vec3",
+							"shaderNode": "normalView"
+						},
+						{
+							"name": "Normal World",
+							"icon": "arrow-bar-up",
+							"description": "Return the normal vector from the coordinate space World.",
+							"nodeType": "vec3",
+							"shaderNode": "normalWorld"
+						},
+						{
+							"name": "Transformed Normal View",
+							"icon": "arrow-bar-up",
+							"nodeType": "vec3",
+							"shaderNode": "transformedNormalView"
+						},
+						{
+							"name": "Transformed Normal World",
+							"icon": "arrow-bar-up",
+							"nodeType": "vec3",
+							"shaderNode": "transformedNormalWorld"
+						}
+					]
+				},
+				{
+					"name": "Tangent",
+					"icon": "arrows-up-right",
+					"children": [
+						{
+							"name": "Tangent Geometry",
+							"icon": "arrows-up-right",
+							"nodeType": "vec3",
+							"shaderNode": "tangentGeometry"
+						},
+						{
+							"name": "Tangent Local",
+							"icon": "arrows-up-right",
+							"nodeType": "vec3",
+							"shaderNode": "tangentLocal"
+						},
+						{
+							"name": "Tangent View",
+							"icon": "arrows-up-right",
+							"nodeType": "vec3",
+							"shaderNode": "tangentView"
+						},
+						{
+							"name": "Tangent World",
+							"icon": "arrows-up-right",
+							"nodeType": "vec3",
+							"shaderNode": "tangentWorld"
+						},
+						{
+							"name": "Transformed Tangent View",
+							"icon": "arrows-up-right",
+							"nodeType": "vec3",
+							"shaderNode": "transformedTangentView"
+						},
+						{
+							"name": "Transformed Tangent World",
+							"icon": "arrows-up-right",
+							"nodeType": "vec3",
+							"shaderNode": "transformedTangentWorld"
+						}
+					]
+				},
+				{
+					"name": "Bitangent",
+					"icon": "arrows-up-left",
+					"children": [
+						{
+							"name": "Bitangent Geometry",
+							"icon": "arrows-up-left",
+							"nodeType": "vec3",
+							"shaderNode": "bitangentGeometry"
+						},
+						{
+							"name": "Bitangent Local",
+							"icon": "arrows-up-left",
+							"nodeType": "vec3",
+							"shaderNode": "bitangentLocal"
+						},
+						{
+							"name": "Bitangent View",
+							"icon": "arrow-up-left",
+							"nodeType": "vec3",
+							"shaderNode": "bitangentView"
+						},
+						{
+							"name": "Bitangent World",
+							"icon": "arrows-up-left",
+							"nodeType": "vec3",
+							"shaderNode": "bitangentWorld"
+						},
+						{
+							"name": "Transformed Bitangent View",
+							"icon": "arrows-up-left",
+							"nodeType": "vec3",
+							"shaderNode": "transformedBitangentView"
+						},
+						{
+							"name": "Transformed Bitangent World",
+							"icon": "arrows-up-left",
+							"nodeType": "vec3",
+							"shaderNode": "transformedBitangentWorld"
+						}
+					]
+				},
+				{
+					"name": "Model",
+					"icon": "box",
+					"children": [
+						{
+							"name": "Model Direction",
+							"icon": "box",
+							"nodeType": "vec3",
+							"shaderNode": "modelDirection"
+						},
+						{
+							"name": "Model Normal Matrix",
+							"icon": "box",
+							"nodeType": "vec3",
+							"shaderNode": "modelNormalMatrix"
+						},
+						{
+							"name": "Model Position",
+							"icon": "box",
+							"nodeType": "vec3",
+							"shaderNode": "modelPosition"
+						},
+						{
+							"name": "Model View Matrix",
+							"icon": "box",
+							"nodeType": "vec3",
+							"shaderNode": "modelViewMatrix"
+						},
+						{
+							"name": "Model View Position",
+							"icon": "box",
+							"nodeType": "vec3",
+							"shaderNode": "modelViewPosition"
+						},
+						{
+							"name": "Model World Matrix",
+							"icon": "box",
+							"nodeType": "vec3",
+							"shaderNode": "modelWorldMatrix"
+						}
+					]
+				},
+				{
+					"name": "Object",
+					"icon": "3d-cube-sphere",
+					"children": [
+						{
+							"name": "Object Direction",
+							"icon": "3d-cube-sphere",
+							"shaderNode": "objectDirection",
+							"properties": [
+								{
+									"name": "object3d",
+									"nodeType": "Object3D"
+								}
+							]
+						},
+						{
+							"name": "Object Normal Matrix",
+							"icon": "3d-cube-sphere",
+							"shaderNode": "objectNormalMatrix",
+							"properties": [
+								{
+									"name": "object3d",
+									"nodeType": "Object3D"
+								}
+							]
+						},
+						{
+							"name": "Object Position",
+							"icon": "3d-cube-sphere",
+							"shaderNode": "objectPosition",
+							"properties": [
+								{
+									"name": "object3d",
+									"nodeType": "Object3D"
+								}
+							]
+						},
+						{
+							"name": "Object View Matrix",
+							"icon": "3d-cube-sphere",
+							"shaderNode": "objectViewMatrix",
+							"properties": [
+								{
+									"name": "object3d",
+									"nodeType": "Object3D"
+								}
+							]
+						},
+						{
+							"name": "Object View Position",
+							"icon": "3d-cube-sphere",
+							"shaderNode": "objectViewPosition",
+							"properties": [
+								{
+									"name": "object3d",
+									"nodeType": "Object3D"
+								}
+							]
+						},
+						{
+							"name": "Object World Matrix",
+							"icon": "3d-cube-sphere",
+							"shaderNode": "objectWorldMatrix",
+							"properties": [
+								{
+									"name": "object3d",
+									"nodeType": "Object3D"
+								}
+							]
+						}
+					]
+				},
+				{
+					"name": "Position",
+					"icon": "gizmo",
+					"children": [
+						{
+							"name": "Position Geometry",
+							"icon": "gizmo",
+							"nodeType": "vec3",
+							"shaderNode": "positionGeometry"
+						},
+						{
+							"name": "Position Local",
+							"icon": "gizmo",
+							"nodeType": "mat4",
+							"shaderNode": "positionLocal"
+						},
+						{
+							"name": "Position View",
+							"icon": "gizmo",
+							"nodeType": "vec3",
+							"shaderNode": "positionView"
+						},
+						{
+							"name": "Position View Direction",
+							"icon": "gizmo",
+							"nodeType": "vec3",
+							"shaderNode": "positionViewDirection"
+						},
+						{
+							"name": "Position World",
+							"icon": "gizmo",
+							"nodeType": "mat3",
+							"shaderNode": "positionWorld"
+						},
+						{
+							"name": "Position World Direction",
+							"icon": "gizmo",
+							"nodeType": "mat4",
+							"shaderNode": "positionWorldDirection"
+						}
+					]
+				},
+				{
+					"name": "Viewport",
+					"icon": "device-desktop",
+					"children": [
+						{
+							"name": "Viewport Bottom Left",
+							"icon": "device-desktop",
+							"nodeType": "vec3",
+							"shaderNode": "viewportBottomLeft"
+						},
+						{
+							"name": "Viewport Bottom Right",
+							"icon": "device-desktop",
+							"nodeType": "vec3",
+							"shaderNode": "viewportBottomRight"
+						},
+						{
+							"name": "Viewport Coordinate",
+							"icon": "device-desktop",
+							"nodeType": "vec3",
+							"shaderNode": "viewportCoordinate"
+						},
+						{
+							"name": "Viewport Resolution",
+							"icon": "device-desktop",
+							"nodeType": "vec3",
+							"shaderNode": "viewportResolution"
+						},
+						{
+							"name": "Viewport Top Left",
+							"icon": "device-desktop",
+							"nodeType": "vec3",
+							"shaderNode": "viewportTopLeft"
+						},
+						{
+							"name": "Viewport Top Right",
+							"icon": "device-desktop",
+							"nodeType": "vec3",
+							"shaderNode": "viewportTopRight"
+						}
+					]
+				},
+				{
+					"name": "UV",
+					"icon": "chart-treemap",
+					"children": [
+						{
+							"name": "Matcap UV",
+							"icon": "chart-treemap",
+							"nodeType": "vec1",
+							"shaderNode": "matcapUV"
+						},
+						{
+							"name": "Point UV",
+							"icon": "chart-treemap",
+							"nodeType": "vec1",
+							"shaderNode": "pointUV"
+						},
+						{
+							"name": "UV",
+							"icon": "chart-treemap",
+							"nodeType": "vec2",
+							"editorClass": "UVEditor"
+						}
+					]
+				},
+				{
+					"name": "Geometry",
+					"icon": "world",
+					"children": [
+						{
+							"name": "Attribute",
+							"icon": "book-upload",
+							"shaderNode": "attribute",
+							"nodeType": "bool",
+							"properties": [
+								{
+									"name": "aNode",
+									"nodeType": "node"
+								}
+							]
+						},
+						{
+							"name": "Face Direction",
+							"icon": "brightness",
+							"nodeType": "float",
+							"shaderNode": "faceDirection"
+						},
+						{
+							"name": "Front Facing",
+							"icon": "brightness",
+							"shaderNode": "frontFacing",
+							"nodeType": "bool"
+						},
+						{
+							"name": "Geometry Color",
+							"icon": "palette",
+							"shaderNode": "geometryColor",
+							"nodeType": "color"
+						}
+					]
+				}
+			]
+		},
+		{
+			"name": "Math",
+			"icon": "calculator",
+			"children": [
+				{
+					"name": "Arithmetic Operators",
+					"icon": "math-symbols",
+					"children": [
+						{
+							"name": "Addition",
+							"icon": "plus",
+							"description": "Addition operator.",
+							"shaderNode": "add",
+							"nodeType": "node",
+							"properties": [
+								{
+									"name": "aNode",
+									"nodeType": "node"
+								},
+								{
+									"name": "bNode",
+									"nodeType": "node"
+								}
+							]
+						},
+						{
+							"name": "Division",
+							"icon": "divide",
+							"description": "Division operator.",
+							"shaderNode": "div",
+							"nodeType": "node",
+							"properties": [
+								{
+									"name": "aNode",
+									"nodeType": "node"
+								},
+								{
+									"name": "bNode",
+									"nodeType": "node"
+								}
+							]
+						},
+						{
+							"name": "Multiply",
+							"icon": "x",
+							"tags": "tag1,tag2",
+							"description": "Multiply operator.",
+							"shaderNode": "mul",
+							"nodeType": "node",
+							"renderers": "WebGPU",
+							"properties": [
+								{
+									"name": "aNode",
+									"nodeType": "float"
+								},
+								{
+									"name": "bNode",
+									"nodeType": "float"
+								}
+							]
+						},
+						{
+							"name": "Power",
+							"icon": "arrow-up-right",
+							"description": "Exponentiation operator.",
+							"shaderNode": "pow",
+							"nodeType": "node",
+							"properties": [
+								{
+									"name": "aNode",
+									"nodeType": "node"
+								},
+								{
+									"name": "bNode",
+									"nodeType": "node"
+								}
+							]
+						},
+						{
+							"name": "Remainder",
+							"icon": "percentage",
+							"description": "Remainder operator.",
+							"shaderNode": "remainder",
+							"nodeType": "node",
+							"properties": [
+								{
+									"name": "aNode",
+									"nodeType": "node"
+								},
+								{
+									"name": "bNode",
+									"nodeType": "node"
+								}
+							]
+						},
+						{
+							"name": "Subtraction",
+							"icon": "minus",
+							"description": "Subtraction operator.",
+							"shaderNode": "sub",
+							"nodeType": "node",
+							"properties": [
+								{
+									"name": "aNode",
+									"nodeType": "node"
+								},
+								{
+									"name": "bNode",
+									"nodeType": "node"
+								}
+							]
+						}
+					]
+				},
+				{
+					"name": "Logic Operators",
+					"icon": "math-symbols",
+					"children": [
+						{
+							"name": "Less Than",
+							"icon": "math-lower",
+							"description": "Less than operator..",
+							"shaderNode": "lessThan",
+							"nodeType": "bool",
+							"properties": [
+								{
+									"name": "aNode",
+									"nodeType": "node"
+								},
+								{
+									"name": "bNode",
+									"nodeType": "node"
+								}
+							]
+						},
+						{
+							"name": "Less Than Or Equal",
+							"icon": "math-equal-lower",
+							"description": "Less than or equal operator.",
+							"shaderNode": "lessThanEqual",
+							"nodeType": "bool",
+							"properties": [
+								{
+									"name": "aNode",
+									"nodeType": "node"
+								},
+								{
+									"name": "bNode",
+									"nodeType": "node"
+								}
+							]
+						},
+						{
+							"name": "Greater Than",
+							"icon": "math-greater",
+							"description": "Greater than operator.",
+							"shaderNode": "greaterThan",
+							"nodeType": "bool",
+							"renderers": "WebGPU",
+							"properties": [
+								{
+									"name": "aNode",
+									"nodeType": "node"
+								},
+								{
+									"name": "bNode",
+									"nodeType": "node"
+								}
+							]
+						},
+						{
+							"name": "Greater Than Or Equal",
+							"icon": "math-equal-greater",
+							"description": "Greater than or equal operator.",
+							"shaderNode": "greaterThanEqual",
+							"nodeType": "bool",
+							"properties": [
+								{
+									"name": "aNode",
+									"nodeType": "node"
+								},
+								{
+									"name": "bNode",
+									"nodeType": "node"
+								}
+							]
+						},
+						{
+							"name": "Equality",
+							"icon": "equal-double",
+							"description": "Equality operator.",
+							"shaderNode": "equal",
+							"nodeType": "bool",
+							"properties": [
+								{
+									"name": "aNode",
+									"nodeType": "node"
+								},
+								{
+									"name": "bNode",
+									"nodeType": "node"
+								}
+							]
+						},
+						{
+							"name": "Inequality",
+							"icon": "equal-not",
+							"description": "Inequality operator.",
+							"shaderNode": "notEqual",
+							"nodeType": "bool",
+							"properties": [
+								{
+									"name": "aNode",
+									"nodeType": "node"
+								},
+								{
+									"name": "bNode",
+									"nodeType": "node"
+								}
+							]
+						},
+						{
+							"name": "And",
+							"icon": "ampersand",
+							"description": "Logical AND operator.",
+							"shaderNode": "and",
+							"nodeType": "bool",
+							"properties": [
+								{
+									"name": "aNode",
+									"nodeType": "node"
+								},
+								{
+									"name": "bNode",
+									"nodeType": "node"
+								}
+							]
+						},
+						{
+							"name": "Or",
+							"icon": "switch-horizontal",
+							"description": "Logical OR operator.",
+							"shaderNode": "or",
+							"nodeType": "bool",
+							"properties": [
+								{
+									"name": "aNode",
+									"nodeType": "node"
+								},
+								{
+									"name": "bNode",
+									"nodeType": "node"
+								}
+							]
+						},
+						{
+							"name": "Conditional",
+							"icon": "arrows-split",
+							"description": "Logical OR operator.",
+							"shaderNode": "Conditional",
+							"nodeType": "bool",
+							"properties": [
+								{
+									"name": "condNode",
+									"nodeType": "node"
+								},
+								{
+									"name": "ifNode",
+									"nodeType": "node"
+								},
+								{
+									"name": "elseNode",
+									"nodeType": "node"
+								}
+							]
+						}
+					]
+				},
+				{
+					"name": "Bitwise Operators",
+					"icon": "math-symbols",
+					"children": [
+						{
+							"name": "Bitwise AND",
+							"icon": "binary",
+							"description": "Bitwise AND operator.",
+							"shaderNode": "bitAnd",
+							"nodeType": "node",
+							"properties": [
+								{
+									"name": "aNode",
+									"nodeType": "node"
+								},
+								{
+									"name": "bNode",
+									"nodeType": "node"
+								}
+							]
+						},
+						{
+							"name": "Bitwise OR",
+							"icon": "binary",
+							"description": "Bitwise OR operator.",
+							"shaderNode": "bitOr",
+							"nodeType": "node",
+							"properties": [
+								{
+									"name": "aNode",
+									"nodeType": "node"
+								},
+								{
+									"name": "bNode",
+									"nodeType": "node"
+								}
+							]
+						},
+						{
+							"name": "Bitwise XOR",
+							"icon": "binary",
+							"description": "Bitwise XOR operator.",
+							"shaderNode": "bitXor",
+							"nodeType": "node",
+							"properties": [
+								{
+									"name": "aNode",
+									"nodeType": "node"
+								},
+								{
+									"name": "bNode",
+									"nodeType": "node"
+								}
+							]
+						},
+						{
+							"name": "Shift left",
+							"icon": "binary",
+							"description": "Bitwise left shift operator.",
+							"shaderNode": "shiftLeft",
+							"nodeType": "node",
+							"properties": [
+								{
+									"name": "aNode",
+									"nodeType": "node"
+								},
+								{
+									"name": "bNode",
+									"nodeType": "node"
+								}
+							]
+						},
+						{
+							"name": "Shift right",
+							"icon": "binary",
+							"description": "Bitwise right shift operator.",
+							"shaderNode": "shiftRight",
+							"nodeType": "node",
+							"properties": [
+								{
+									"name": "aNode",
+									"nodeType": "node"
+								},
+								{
+									"name": "bNode",
+									"nodeType": "node"
+								}
+							]
+						},
+						{
+							"name": "XOR",
+							"icon": "binary",
+							"description": "Bitwise XOR operator.",
+							"shaderNode": "xor",
+							"nodeType": "node",
+							"properties": [
+								{
+									"name": "aNode",
+									"nodeType": "node"
+								},
+								{
+									"name": "bNode",
+									"nodeType": "node"
+								}
+							]
+						}
+					]
+				},
+				{
+					"name": "Functions",
+					"icon": "math-function",
+					"children": [
+						{
+							"name": "Abs",
+							"icon": "math-function",
+							"description": "Returns the absolute value of x.",
+							"shaderNode": "abs",
+							"nodeType": "node",
+							"properties": [
+								{
+									"name": "aNode",
+									"nodeType": "node",
+									"label": "x",
+									"description": "Specify the value of which to return the absolute."
+								}
+							]
+						},
+						{
+							"name": "Acos",
+							"icon": "math-function",
+							"description": "Returns the angle whose trigonometric cosine is x.",
+							"shaderNode": "acos",
+							"nodeType": "node",
+							"properties": [
+								{
+									"name": "aNode",
+									"nodeType": "node",
+									"label": "x",
+									"description": "Specify the value whose arccosine to return."
+								}
+							]
+						},
+						{
+							"name": "Asin",
+							"icon": "math-function",
+							"description": "Returns the angle whose trigonometric sine is X.",
+							"shaderNode": "asin",
+							"nodeType": "node",
+							"properties": [
+								{
+									"name": "aNode",
+									"nodeType": "node",
+									"label": "x",
+									"description": "Specify the value whose arcsine to return."
+								}
+							]
+						},
+						{
+							"name": "Atan",
+							"icon": "math-function",
+							"description": "Returns either the angle whose trigonometric arctangent is yx or y_over_x, depending on which overload is invoked. In the first overload, the signs of y and x are used to determine the quadrant that the angle lies in.",
+							"shaderNode": "atan",
+							"nodeType": "node",
+							"properties": [
+								{
+									"name": "aSNode",
+									"nodeType": "node",
+									"label": "y_over_x",
+									"description": "Specify the fraction whose arctangent to return."
+								}
+							]
+						},
+						{
+							"name": "Atan2",
+							"icon": "math-function",
+							"description": "Return the arc-tangent of the parameters",
+							"shaderNode": "atan2",
+							"nodeType": "node",
+							"properties": [
+								{
+									"name": "aNode",
+									"nodeType": "node",
+									"label": "y",
+									"description": "Specify the numerator of the fraction whose arctangent to return."
+								},
+								{
+									"name": "bNode",
+									"nodeType": "node",
+									"label": "x",
+									"description": "Specify the denominator of the fraction whose arctangent to return."
+								}
+							]
+						},
+						{
+							"name": "Ceil",
+							"icon": "math-function",
+							"description": "Returns a value equal to the nearest integer that is greater than or equal to x.",
+							"shaderNode": "ceil",
+							"nodeType": "node",
+							"properties": [
+								{
+									"name": "aNode",
+									"nodeType": "node",
+									"label": "x",
+									"description": "Specify the value to evaluate."
+								}
+							]
+						},
+						{
+							"name": "Clamp",
+							"icon": "math-function",
+							"description": "Constrain a value to lie between two further values",
+							"shaderNode": "clamp",
+							"nodeType": "node",
+							"properties": [
+								{
+									"name": "aNode",
+									"nodeType": "node",
+									"label": "x",
+									"description": "Specify the value to constrain."
+								},
+								{
+									"name": "bNode",
+									"nodeType": "node",
+									"label": "minVal",
+									"description": "Specify the lower end of the range into which to constrain."
+								},
+								{
+									"name": "cNode",
+									"nodeType": "node",
+									"label": "maxVal",
+									"description": "Specify the upper end of the range into which to constrain."
+								}
+							]
+						},
+						{
+							"name": "Cosine",
+							"icon": "math-function",
+							"description": "Returns the trigonometric cosine of angle.",
+							"shaderNode": "cos",
+							"nodeType": "node",
+							"properties": [
+								{
+									"name": "aNode",
+									"nodeType": "node",
+									"label": "angle",
+									"description": "Specify the quantity, in radians, of which to return the cosine."
+								}
+							]
+						},
+						{
+							"name": "Cross",
+							"icon": "math-function",
+							"description": "Calculate the cross product of two vectors",
+							"shaderNode": "cross",
+							"nodeType": "node",
+							"properties": [
+								{
+									"name": "aNode",
+									"nodeType": "node",
+									"label": "x",
+									"description": "Specifies the first of two points."
+								},
+								{
+									"name": "bNode",
+									"nodeType": "node",
+									"label": "y",
+									"description": "Specifies the second of two points."
+								}
+							]
+						},
+						{
+							"name": "Degrees",
+							"icon": "math-function",
+							"description": "Converts a quantity specified in radians into degrees.",
+							"shaderNode": "degrees",
+							"nodeType": "node",
+							"properties": [
+								{
+									"name": "aNode",
+									"nodeType": "node",
+									"label": "radians",
+									"description": "Specify the quantity, in radians, to be converted to degrees."
+								}
+							]
+						},
+						{
+							"name": "Distance",
+							"icon": "math-function",
+							"description": "Calculate the distance between two points",
+							"shaderNode": "distance",
+							"nodeType": "node",
+							"properties": [
+								{
+									"name": "aNode",
+									"nodeType": "node",
+									"label": "p0",
+									"description": "Specifies the first of two points."
+								},
+								{
+									"name": "bNode",
+									"nodeType": "node",
+									"label": "p1",
+									"description": "Specifies the second of two points."
+								}
+							]
+						},
+						{
+							"name": "Dot",
+							"icon": "math-function",
+							"description": "Calculate the dot product of two vectors",
+							"shaderNode": "dot",
+							"nodeType": "node",
+							"properties": [
+								{
+									"name": "aNode",
+									"nodeType": "node",
+									"label": "x",
+									"description": "Specifies the first of two points."
+								},
+								{
+									"name": "bNode",
+									"nodeType": "node",
+									"label": "y",
+									"description": "Specifies the second of two points."
+								}
+							]
+						},
+						{
+							"name": "Exp",
+							"icon": "math-function",
+							"description": "Returns the natural exponentiation of x. i.e., ex.",
+							"shaderNode": "exp",
+							"nodeType": "node",
+							"properties": [
+								{
+									"name": "aNode",
+									"nodeType": "node",
+									"label": "x",
+									"description": "Specify the value to exponentiate."
+								}
+							]
+						},
+						{
+							"name": "Face Forward",
+							"icon": "math-function",
+							"description": "Return a vector pointing in the same direction as another",
+							"shaderNode": "faceForward",
+							"nodeType": "node",
+							"properties": [
+								{
+									"name": "aNode",
+									"nodeType": "node",
+									"label": "n",
+									"description": "Specifies the vector to orient."
+								},
+								{
+									"name": "bNode",
+									"nodeType": "node",
+									"label": "i",
+									"description": "Specifies the incident vector."
+								},
+								{
+									"name": "cNode",
+									"nodeType": "node",
+									"label": "nref",
+									"description": "Specifies the reference vector."
+								}
+							]
+						},
+						{
+							"name": "Floor",
+							"icon": "math-function",
+							"description": "Returns a value equal to the nearest integer that is less than or equal to x.",
+							"shaderNode": "floor",
+							"nodeType": "node",
+							"properties": [
+								{
+									"name": "aNode",
+									"nodeType": "node",
+									"label": "x",
+									"description": "Specify the value to evaluate."
+								}
+							]
+						},
+						{
+							"name": "Fract",
+							"icon": "math-function",
+							"description": "Returns the fractional part of x. This is calculated as x - floor(x).",
+							"shaderNode": "fract",
+							"nodeType": "node",
+							"properties": [
+								{
+									"name": "aNode",
+									"nodeType": "node",
+									"label": "x",
+									"description": "Specify the value to evaluate."
+								}
+							]
+						},
+						{
+							"name": "Inverse Sqrt",
+							"icon": "math-function",
+							"description": "Returns the inverse of the square root of x; i.e. the value 1x√.",
+							"shaderNode": "inversesqrt",
+							"nodeType": "node",
+							"properties": [
+								{
+									"name": "aNode",
+									"nodeType": "node",
+									"label": "x",
+									"description": "Specify the value of which to take the inverse of the square root."
+								}
+							]
+						},
+						{
+							"name": "Length",
+							"icon": "math-function",
+							"description": "Returns the length of the vector, i.e.",
+							"shaderNode": "length",
+							"nodeType": "node",
+							"properties": [
+								{
+									"name": "aNode",
+									"nodeType": "node",
+									"label": "x",
+									"description": "Specify the vector of which to calculate the length."
+								}
+							]
+						},
+						{
+							"name": "Log",
+							"icon": "math-function",
+							"description": "Returns the natural logarithm of x, i.e. the value y which satisfies x=ey.",
+							"shaderNode": "log",
+							"nodeType": "node",
+							"properties": [
+								{
+									"name": "aNode",
+									"nodeType": "node",
+									"label": "x",
+									"description": "Specify the value of which to take the natural logarithm."
+								}
+							]
+						},
+						{
+							"name": "Log2",
+							"icon": "math-function",
+							"description": "Returns the base 2 logarithm of x, i.e. the value y which satisfies x=2y.",
+							"shaderNode": "log2",
+							"nodeType": "node",
+							"properties": [
+								{
+									"name": "aNode",
+									"nodeType": "node",
+									"label": "x",
+									"description": "Specify the value of which to take the base 2 logarithm."
+								}
+							]
+						},
+						{
+							"name": "Max",
+							"icon": "math-function",
+							"description": "Returns the maximun of the two parameters.",
+							"shaderNode": "max",
+							"nodeType": "node",
+							"properties": [
+								{
+									"name": "aNode",
+									"nodeType": "node",
+									"label": "x",
+									"description": "Specify the first value to compare."
+								},
+								{
+									"name": "bNode",
+									"nodeType": "node",
+									"label": "y",
+									"description": "Specify the second value to compare."
+								}
+							]
+						},
+						{
+							"name": "Min",
+							"icon": "math-function",
+							"description": "Returns the minimum of the two parameters.",
+							"shaderNode": "min",
+							"nodeType": "node",
+							"properties": [
+								{
+									"name": "aNode",
+									"nodeType": "node",
+									"label": "x",
+									"description": "Specify the first value to compare."
+								},
+								{
+									"name": "bNode",
+									"nodeType": "node",
+									"label": "y",
+									"description": "Specify the second value to compare."
+								}
+							]
+						},
+						{
+							"name": "Mix",
+							"icon": "math-function",
+							"description": "Linearly interpolate between two values",
+							"shaderNode": "mix",
+							"nodeType": "node",
+							"properties": [
+								{
+									"name": "aNode",
+									"nodeType": "node",
+									"label": "x",
+									"description": "Specify the start of the range in which to interpolate."
+								},
+								{
+									"name": "bNode",
+									"nodeType": "node",
+									"label": "y",
+									"description": "Specify the end of the range in which to interpolate."
+								},
+								{
+									"name": "cNode",
+									"nodeType": "node",
+									"label": "a",
+									"description": "Specify the value to use to interpolate between x and y."
+								}
+							]
+						},
+						{
+							"name": "Modulo",
+							"icon": "math-function",
+							"description": "Returns the value of x modulo y.",
+							"shaderNode": "mod",
+							"nodeType": "node",
+							"properties": [
+								{
+									"name": "aNode",
+									"nodeType": "node",
+									"label": "x",
+									"description": "Specify the value to evaluate."
+								},
+								{
+									"name": "bNode",
+									"nodeType": "float",
+									"label": "y",
+									"description": "Specify the value to evaluate."
+								}
+							]
+						},
+						{
+							"name": "Negate",
+							"icon": "math-function",
+							"description": "Returns the flipped sign value of input In",
+							"shaderNode": "negate",
+							"nodeType": "node",
+							"properties": [
+								{
+									"name": "aNode",
+									"nodeType": "node",
+									"description": "Input value."
+								}
+							]
+						},
+						{
+							"name": "Normalize",
+							"icon": "math-function",
+							"description": "Returns a vector with the same direction as its parameter, v, but with length 1.",
+							"shaderNode": "normalize",
+							"nodeType": "node",
+							"properties": [
+								{
+									"name": "aNode",
+									"nodeType": "node",
+									"label": "v",
+									"description": "Specifies the vector to normalize."
+								}
+							]
+						},
+						{
+							"name": "One Minus",
+							"icon": "math-function",
+							"description": "Returns the result of input `a` subtracted from 1.",
+							"shaderNode": "oneMinus",
+							"nodeType": "node",
+							"properties": [
+								{
+									"name": "aNode",
+									"nodeType": "node",
+									"description": "Input value."
+								}
+							]
+						},
+						{
+							"name": "Pow",
+							"icon": "math-function",
+							"description": "Return the value of the first parameter raised to the power of the second",
+							"shaderNode": "pow",
+							"nodeType": "node",
+							"properties": [
+								{
+									"name": "aNode",
+									"nodeType": "node",
+									"label": "x",
+									"description": "Specify the value to raise to the power y."
+								},
+								{
+									"name": "bNode",
+									"nodeType": "node",
+									"label": "y",
+									"description": "Specify the value to whitch to raise x."
+								}
+							]
+						},
+						{
+							"name": "Radians",
+							"icon": "math-function",
+							"description": "Converts a quantity specified in degrees into radians.",
+							"shaderNode": "radians",
+							"nodeType": "node",
+							"properties": [
+								{
+									"name": "aNode",
+									"nodeType": "node",
+									"label": "degrees",
+									"description": "Specify the quantity, in degrees, to be converted to radians."
+								}
+							]
+						},
+						{
+							"name": "Reciprocal",
+							"icon": "math-function",
+							"description": "Returns the result of dividing 1 by the input.",
+							"shaderNode": "reciprocal",
+							"nodeType": "node",
+							"properties": [
+								{
+									"name": "aNode",
+									"nodeType": "node",
+									"description": "Input value."
+								}
+							]
+						},
+						{
+							"name": "Reflect",
+							"icon": "math-function",
+							"description": "Calculate the reflection direction for an incident vector",
+							"shaderNode": "reflect",
+							"nodeType": "node",
+							"properties": [
+								{
+									"name": "aNode",
+									"nodeType": "node",
+									"label": "i",
+									"description": "Specifies the incident vector."
+								},
+								{
+									"name": "bNode",
+									"nodeType": "node",
+									"label": "n",
+									"description": "Specifies the normal vector."
+								}
+							]
+						},
+						{
+							"name": "Refract",
+							"icon": "math-function",
+							"description": "Calculate the refraction direction for an incident vector",
+							"shaderNode": "refract",
+							"nodeType": "node",
+							"properties": [
+								{
+									"name": "aNode",
+									"nodeType": "node",
+									"label": "i",
+									"description": "Specifies the incident vector."
+								},
+								{
+									"name": "bNode",
+									"nodeType": "node",
+									"label": "n",
+									"description": "Specifies the normal vector."
+								},
+								{
+									"name": "cNode",
+									"nodeType": "node",
+									"label": "eta",
+									"description": "Specifies the ratio of indices of refraction."
+								}
+							]
+						},
+						{
+							"name": "Remap",
+							"icon": "math-function",
+							"nodeType": "node",
+							"shaderNode": "remap",
+							"properties": [
+								{
+									"name": "node",
+									"nodeType": "node"
+								},
+								{
+									"name": "inLowNode",
+									"nodeType": "node"
+								},
+								{
+									"name": "inHighNode",
+									"nodeType": "node"
+								},
+								{
+									"name": "outLowNode",
+									"nodeType": "node"
+								},
+								{
+									"name": "outHighNode",
+									"nodeType": "node"
+								}
+							]
+						},
+						{
+							"name": "Round",
+							"icon": "math-function",
+							"description": "Round returns a value equal to the nearest integer to x.",
+							"shaderNode": "round",
+							"nodeType": "node",
+							"properties": [
+								{
+									"name": "aNode",
+									"nodeType": "node",
+									"label": "x",
+									"description": "Specify the value to evaluate."
+								}
+							]
+						},
+						{
+							"name": "Saturate",
+							"icon": "math-function",
+							"shaderNode": "saturate",
+							"nodeType": "node",
+							"properties": [
+								{
+									"name": "aNode",
+									"nodeType": "node"
+								}
+							]
+						},
+						{
+							"name": "Sign",
+							"icon": "math-function",
+							"description": "Returns -1.0 if x is less than 0.0, 0.0 if x is equal to 0.0, and +1.0 if x is greater than 0.0.",
+							"shaderNode": "sign",
+							"nodeType": "node",
+							"properties": [
+								{
+									"name": "aNode",
+									"nodeType": "node",
+									"label": "x",
+									"description": "Specify the value from wich to extract the sign."
+								}
+							]
+						},
+						{
+							"name": "Sine",
+							"icon": "math-function",
+							"description": "Returns the trigonometric sine of angle.",
+							"shaderNode": "sin",
+							"nodeType": "node",
+							"properties": [
+								{
+									"name": "aNode",
+									"nodeType": "node",
+									"label": "angle",
+									"description": "Specify the quantity, in radians, of which to return the sine."
+								}
+							]
+						},
+						{
+							"name": "Smoothstep",
+							"icon": "math-function",
+							"description": "Perform Hermite interpolation between two values",
+							"shaderNode": "smoothstep",
+							"nodeType": "node",
+							"properties": [
+								{
+									"name": "aNode",
+									"nodeType": "node",
+									"label": "edge0",
+									"description": "Specifies the value of the lower edge of the Hermite function."
+								},
+								{
+									"name": "bNode",
+									"nodeType": "node",
+									"label": "edge1",
+									"description": "Specifies the value of the upper edge of the Hermite function."
+								},
+								{
+									"name": "cNode",
+									"nodeType": "node",
+									"label": "x",
+									"description": "Specifies the value to be used to generate the Hermite function."
+								}
+							]
+						},
+						{
+							"name": "Sqrt",
+							"icon": "math-function",
+							"description": "Returns the square root of x, i.e.",
+							"shaderNode": "sqrt",
+							"nodeType": "node",
+							"properties": [
+								{
+									"name": "aNode",
+									"nodeType": "node",
+									"label": "x",
+									"description": "Specify the value of which to take the square root."
+								}
+							]
+						},
+						{
+							"name": "Tangent",
+							"icon": "math-function",
+							"description": "Returns the trigonometric tangent of angle.",
+							"shaderNode": "tan",
+							"nodeType": "node",
+							"properties": [
+								{
+									"name": "aNode",
+									"nodeType": "node",
+									"label": "angle",
+									"description": "Specify the quantity, in radians, of which to return the tangent."
+								}
+							]
+						},
+						{
+							"name": "Transform Direction",
+							"icon": "math-function",
+							"description": "Transforms the direction of vector by a matrix and then normalizes the result.",
+							"shaderNode": "transformDirection",
+							"nodeType": "node",
+							"properties": [
+								{
+									"name": "aNode",
+									"nodeType": "node",
+									"label": "dir"
+								},
+								{
+									"name": "bNode",
+									"nodeType": "node",
+									"label": "matrix"
+								}
+							]
+						},
+						{
+							"name": "dFdx",
+							"icon": "math-function",
+							"description": "Return the partial derivative of expression p with respect to the window x coordinate.",
+							"shaderNode": "dFdx",
+							"nodeType": "node",
+							"properties": [
+								{
+									"name": "aNode",
+									"nodeType": "node",
+									"label": "p",
+									"description": "Specifies the expression of which to take the partial derivative."
+								}
+							]
+						},
+						{
+							"name": "dFdy",
+							"icon": "math-function",
+							"description": "Return the partial derivative of expression p with respect to the window y coordinate.",
+							"shaderNode": "dFdy",
+							"nodeType": "node",
+							"properties": [
+								{
+									"name": "aNode",
+									"nodeType": "node",
+									"label": "p",
+									"description": "Specifies the expression of which to take the partial derivative."
+								}
+							]
+						},
+						{
+							"name": "Step",
+							"icon": "math-function",
+							"description": "Generates a step function by comparing x to edge.",
+							"shaderNode": "step",
+							"nodeType": "node",
+							"properties": [
+								{
+									"name": "aNode",
+									"nodeType": "node",
+									"label": "edge",
+									"description": "Specifies the location of the edge of the step function."
+								},
+								{
+									"name": "bNode",
+									"nodeType": "node",
+									"label": "x",
+									"description": "Specify the value to be used to generate the step function."
+								}
+							]
+						}
+					]
+				},
+				{
+					"name": "Constants",
+					"icon": "123",
+					"children": [
+						{
+							"name": "Epsilon",
+							"icon": "letter-e",
+							"shaderNode": "EPSILON",
+							"nodeType": "float",
+							"value": 1000000
+						},
+						{
+							"name": "Infinity",
+							"icon": "infinity",
+							"shaderNode": "INFINITY",
+							"nodeType": "float",
+							"value": 0.000001
+						},
+						{
+							"name": "PI",
+							"icon": "math-pi",
+							"shaderNode": "PI",
+							"nodeType": "float",
+							"value": 3.141592653589793
+						}
+					]
+				}
+			]
+		},
+		{
+			"name": "Filters",
+			"icon": "color-filter",
+			"children": [
+				{
+					"name": "Burn",
+					"icon": "color-filter",
+					"nodeType": "color",
+					"shaderNode": "burn",
+					"properties": [
+						{
+							"name": "blendNode",
+							"nodeType": "color"
+						},
+						{
+							"name": "baseNode",
+							"nodeType": "color"
+						}
+					]
+				},
+				{
+					"name": "Difference",
+					"icon": "color-filter",
+					"nodeType": "color",
+					"shaderNode": "difference",
+					"properties": [
+						{
+							"name": "blendNode",
+							"nodeType": "color"
+						},
+						{
+							"name": "baseNode",
+							"nodeType": "color"
+						}
+					]
+				},
+				{
+					"name": "Dodge",
+					"icon": "color-filter",
+					"nodeType": "color",
+					"shaderNode": "dodge",
+					"properties": [
+						{
+							"name": "blendNode",
+							"nodeType": "color"
+						},
+						{
+							"name": "baseNode",
+							"nodeType": "color"
+						}
+					]
+				},
+				{
+					"name": "Hue",
+					"icon": "color-filter",
+					"nodeType": "color",
+					"shaderNode": "hue",
+					"properties": [
+						{
+							"name": "blendNode",
+							"nodeType": "color"
+						},
+						{
+							"name": "baseNode",
+							"nodeType": "color"
+						}
+					]
+				},
+				{
+					"name": "Luminance",
+					"icon": "color-filter",
+					"nodeType": "color",
+					"shaderNode": "luminance",
+					"properties": [
+						{
+							"name": "blendNode",
+							"nodeType": "color"
+						},
+						{
+							"name": "baseNode",
+							"nodeType": "color"
+						}
+					]
+				},
+				{
+					"name": "Overlay",
+					"icon": "color-filter",
+					"nodeType": "color",
+					"shaderNode": "overlay",
+					"properties": [
+						{
+							"name": "blendNode",
+							"nodeType": "color"
+						},
+						{
+							"name": "baseNode",
+							"nodeType": "color"
+						}
+					]
+				},
+				{
+					"name": "Posterize",
+					"icon": "color-filter",
+					"nodeType": "color",
+					"shaderNode": "posterize",
+					"visible": false,
+					"properties": [
+						{
+							"name": "blendNode",
+							"nodeType": "color"
+						},
+						{
+							"name": "baseNode",
+							"nodeType": "color"
+						}
+					]
+				},
+				{
+					"name": "Saturation",
+					"icon": "color-filter",
+					"nodeType": "color",
+					"shaderNode": "saturation",
+					"properties": [
+						{
+							"name": "blendNode",
+							"nodeType": "color"
+						},
+						{
+							"name": "baseNode",
+							"nodeType": "color"
+						}
+					]
+				},
+				{
+					"name": "Screen",
+					"icon": "color-filter",
+					"nodeType": "color",
+					"shaderNode": "screen",
+					"properties": [
+						{
+							"name": "blendNode",
+							"nodeType": "color"
+						},
+						{
+							"name": "baseNode",
+							"nodeType": "color"
+						}
+					]
+				},
+				{
+					"name": "Vibrance",
+					"icon": "color-filter",
+					"nodeType": "color",
+					"shaderNode": "vibrance",
+					"properties": [
+						{
+							"name": "blendNode",
+							"nodeType": "color"
+						},
+						{
+							"name": "baseNode",
+							"nodeType": "color"
+						}
+					]
+				}
+			]
+		},
+		{
+			"name": "Utils",
+			"icon": "apps",
+			"children": [
+				{
+					"name": "Channel",
+					"icon": "server-2",
+					"children": [
+						{
+							"name": "Join",
+							"icon": "arrows-join",
+							"nodeType": "node",
+							"editorClass": "JoinEditor"
+						},
+						{
+							"name": "Split",
+							"icon": "arrows-split",
+							"nodeType": "node",
+							"editorClass": "SplitEditor"
+						},
+						{
+							"name": "Swizzle",
+							"icon": "switch-3",
+							"nodeType": "node",
+							"editorClass": "SwizzleEditor"
+						}
+					]
+				},
+				{
+					"name": "UV",
+					"icon": "chart-treemap",
+					"children": [
+						{
+							"name": "Rotate UV",
+							"icon": "rotate-clockwise-2",
+							"nodeType": "float",
+							"shaderNode": "rotateUV"
+						}
+					]
+				},
+				{
+					"name": "Preview",
+					"icon": "square-check",
+					"nodeType": "float",
+					"editorClass": "PreviewEditor"
+				},
+				{
+					"name": "Timer",
+					"icon": "clock",
+					"editorClass": "TimerEditor"
+				}
+			]
+		},
+		{
+			"name": "Conversions",
+			"icon": "arrows-exchange",
+			"children": [
+				{
+					"name": "Color To Direction",
+					"icon": "arrows-exchange",
+					"nodeType": "color",
+					"shaderNode": "colorToDirection"
+				},
+				{
+					"name": "Direction To Color",
+					"icon": "arrows-exchange",
+					"nodeType": "color",
+					"shaderNode": "directionToColor"
+				}
+			]
+		},
+		{
+			"name": "Procedural",
+			"icon": "binary-tree",
+			"children": [
+				{
+					"name": "Checker",
+					"icon": "border-all",
+					"nodeType": "float",
+					"shaderNode": "checker",
+					"properties": [
+						{
+							"name": "uvNode",
+							"nodeType": "vec2"
+						}
+					]
+				},
+				{
+					"name": "Range",
+					"icon": "sort-ascending-2",
+					"nodeType": "node",
+					"shaderNode": "range",
+					"properties": [
+						{
+							"name": "minNode",
+							"nodeType": "InputNode"
+						},
+						{
+							"name": "maxNode",
+							"nodeType": "InputNode"
+						}
+					]
+				}
+			]
+		},
+		{
+			"name": "Prototype",
+			"icon": "code",
+			"children": [
+				{
+					"name": "Node Prototype",
+					"icon": "components",
+					"editorClass": "NodePrototypeEditor"
+				},
+				{
+					"name": "Scriptable",
+					"icon": "variable",
+					"editorClass": "ScriptableEditor"
+				}
+			]
+		},
+		{
+			"name": "Material",
+			"icon": "circles",
+			"children": [
+				{
+					"name": "Basic Material",
+					"icon": "circle",
+					"nodeType": "float",
+					"editorClass": "BasicMaterialEditor",
+					"editorURL": "./materials/BasicMaterialEditor.js"
+				},
+				{
+					"name": "Points Material",
+					"icon": "circle-dotted",
+					"nodeType": "float",
+					"editorClass": "PointsMaterialEditor"
+				},
+				{
+					"name": "Standard Material",
+					"icon": "inner-shadow-top-left",
+					"nodeType": "float",
+					"editorClass": "StandardMaterialEditor"
+				}
+			]
+		}
+	]
+}

+ 1 - 1
examples/jsm/node-editor/materials/BasicMaterialEditor.js → playground/editors/BasicMaterialEditor.js

@@ -1,4 +1,4 @@
-import { ColorInput, SliderInput, LabelElement } from '../../libs/flow.module.js';
+import { ColorInput, SliderInput, LabelElement } from 'flow';
 import { MaterialEditor } from './MaterialEditor.js';
 import { MeshBasicNodeMaterial } from 'three/nodes';
 

+ 10 - 5
examples/jsm/node-editor/inputs/ColorEditor.js → playground/editors/ColorEditor.js

@@ -1,15 +1,18 @@
-import { ColorInput, StringInput, NumberInput, LabelElement, Element } from '../../libs/flow.module.js';
-import { BaseNode } from '../core/BaseNode.js';
+import { ColorInput, StringInput, NumberInput, LabelElement, Element } from 'flow';
+import { BaseNodeEditor } from '../BaseNodeEditor.js';
 import { Color } from 'three';
 import { UniformNode } from 'three/nodes';
 
-export class ColorEditor extends BaseNode {
+export class ColorEditor extends BaseNodeEditor {
 
 	constructor() {
 
-		const node = new UniformNode( new Color() );
+		const v = new Color();
+		const node = new UniformNode( v );
 
-		super( 'Color', 3, node );
+		super( 'Color', node );
+
+		this.setOutputLength( 3 );
 
 		const updateFields = ( editing ) => {
 
@@ -41,6 +44,8 @@ export class ColorEditor extends BaseNode {
 			fieldG.setTagColor( `#00${hexString.slice( 2, 4 )}00` );
 			fieldB.setTagColor( `#0000${hexString.slice( 4, 6 )}` );
 
+			this.invalidate(); // it's important to scriptable nodes ( cpu nodes needs update )
+
 		};
 
 		const field = new ColorInput( 0xFFFFFF ).onChange( () => {

+ 94 - 0
playground/editors/CustomNodeEditor.js

@@ -0,0 +1,94 @@
+import { LabelElement } from 'flow';
+import { Color, Vector2, Vector3, Vector4 } from 'three';
+import * as Nodes from 'three/nodes';
+import { uniform } from 'three/nodes';
+import { BaseNodeEditor } from '../BaseNodeEditor.js';
+import { createInputLib } from '../NodeEditorUtils.js';
+
+const typeToValue = {
+	'color': Color,
+	'vec2': Vector2,
+	'vec3': Vector3,
+	'vec4': Vector4
+};
+
+const createElementFromProperty = ( node, property ) => {
+
+	const nodeType = property.nodeType;
+	const defaultValue = uniform( typeToValue[ nodeType ] ? new typeToValue[ nodeType ]() : 0 );
+
+	let label = property.label;
+
+	if ( label === undefined ) {
+
+		label = property.name;
+
+		if ( label.endsWith( 'Node' ) === true ) {
+
+			label = label.slice( 0, label.length - 4 );
+
+		}
+
+	}
+
+	node[ property.name ] = defaultValue;
+
+	const element = new LabelElement( label ).setInput( property.defaultLength || 1 );
+
+	if ( createInputLib[ nodeType ] !== undefined ) {
+
+		createInputLib[ nodeType ]( defaultValue, element );
+
+	}
+
+	element.onConnect( ( elmt ) => {
+
+		elmt.setEnabledInputs( ! elmt.getLinkedObject() );
+
+		node[ property.name ] = elmt.getLinkedObject() || defaultValue;
+
+	} );
+
+	return element;
+
+};
+
+export class CustomNodeEditor extends BaseNodeEditor {
+
+	constructor( settings ) {
+
+		const shaderNode = Nodes[ settings.shaderNode ];
+
+		let node = null;
+
+		const elements = [];
+
+		if ( settings.properties !== undefined ) {
+
+			node = shaderNode();
+
+			for ( const property of settings.properties ) {
+
+				elements.push( createElementFromProperty( node, property ) );
+
+			}
+
+		} else {
+
+			node = shaderNode;
+
+		}
+
+		super( settings.name, node, 300 );
+
+		this.title.setIcon( 'ti ti-' + settings.icon );
+
+		for ( const element of elements ) {
+
+			this.add( element );
+
+		}
+
+	}
+
+}

+ 70 - 0
playground/editors/FileEditor.js

@@ -0,0 +1,70 @@
+import { StringInput, Element } from 'flow';
+import { BaseNodeEditor } from '../BaseNodeEditor.js';
+import { arrayBuffer, NodeUtils } from 'three/nodes';
+
+export class FileEditor extends BaseNodeEditor {
+
+	constructor( buffer = null, name = 'File' ) {
+
+		super( 'File', arrayBuffer( buffer ), 250 );
+
+		this.nameInput = new StringInput( name ).setReadOnly( true );
+
+		this.add( new Element().add( this.nameInput ) );
+
+		this.url = null;
+
+	}
+
+	set buffer( arrayBuffer ) {
+
+		if ( this.url !== null ) {
+
+			URL.revokeObjectUR( this.url );
+
+		}
+
+		this.value.value = arrayBuffer;
+		this.url = null;
+
+	}
+
+	get buffer() {
+
+		return this.value.value;
+
+	}
+
+	getURL() {
+
+		if ( this.url === null ) {
+
+			const blob = new Blob( [ this.buffer ], { type: 'application/octet-stream' } );
+
+			this.url = URL.createObjectURL( blob );
+
+		}
+
+		return this.url;
+
+	}
+
+	serialize( data ) {
+
+		super.serialize( data );
+
+		data.buffer = NodeUtils.arrayBufferToBase64( this.buffer );
+		data.name = this.nameInput.getValue();
+
+	}
+
+	deserialize( data ) {
+
+		super.deserialize( data );
+
+		this.buffer = NodeUtils.base64ToArrayBuffer( data.buffer );
+		this.nameInput.setValue( data.name );
+
+	}
+
+}

+ 23 - 0
playground/editors/FloatEditor.js

@@ -0,0 +1,23 @@
+import { BaseNodeEditor } from '../BaseNodeEditor.js';
+import { createElementFromJSON } from '../NodeEditorUtils.js';
+
+export class FloatEditor extends BaseNodeEditor {
+
+	constructor() {
+
+		const { element, inputNode } = createElementFromJSON( {
+			inputType: 'float',
+			inputConnection: false
+		} );
+
+		super( 'Float', inputNode, 150 );
+
+		this.setOutputLength( 1 );
+
+		element.addEventListener( 'changeInput', () => this.invalidate() );
+
+		this.add( element );
+
+	}
+
+}

+ 50 - 0
playground/editors/JavaScriptEditor.js

@@ -0,0 +1,50 @@
+import { BaseNodeEditor } from '../BaseNodeEditor.js';
+import { CodeEditorElement } from '../elements/CodeEditorElement.js';
+import { js } from 'three/nodes';
+
+export class JavaScriptEditor extends BaseNodeEditor {
+
+	constructor( source = '' ) {
+
+		const codeNode = js( source );
+
+		super( 'JavaScript', codeNode, 500 );
+
+		this.setResizable( true );
+
+		//
+
+		this.editorElement = new CodeEditorElement( source );
+		this.editorElement.addEventListener( 'change', () => {
+
+			codeNode.code = this.editorElement.source;
+
+			this.invalidate();
+
+			this.editorElement.focus();
+
+		} );
+
+		this.add( this.editorElement );
+
+	}
+
+	set source( value ) {
+
+		this.codeNode.code = value;
+
+	}
+
+	get source() {
+
+		return this.codeNode.code;
+
+	}
+
+	get codeNode() {
+
+		return this.value;
+
+	}
+
+}

+ 10 - 10
examples/jsm/node-editor/utils/JoinEditor.js → playground/editors/JoinEditor.js

@@ -1,16 +1,16 @@
-import { LabelElement } from '../../libs/flow.module.js';
-import { BaseNode } from '../core/BaseNode.js';
-import { JoinNode, UniformNode } from 'three/nodes';
+import { LabelElement } from 'flow';
+import { BaseNodeEditor } from '../BaseNodeEditor.js';
+import { JoinNode, UniformNode, float } from 'three/nodes';
 
 const NULL_VALUE = new UniformNode( 0 );
 
-export class JoinEditor extends BaseNode {
+export class JoinEditor extends BaseNodeEditor {
 
 	constructor() {
 
 		const node = new JoinNode();
 
-		super( 'Join', 1, node, 175 );
+		super( 'Join', node, 175 );
 
 		const update = () => {
 
@@ -31,7 +31,7 @@ export class JoinEditor extends BaseNode {
 
 			for ( let i = 0; i < length; i ++ ) {
 
-				nodes.push( values[ i ] || NULL_VALUE );
+				nodes.push( float( values[ i ] || NULL_VALUE ) );
 
 			}
 
@@ -41,10 +41,10 @@ export class JoinEditor extends BaseNode {
 
 		};
 
-		const xElement = new LabelElement( 'X | R' ).setInput( 1 ).onConnect( update );
-		const yElement = new LabelElement( 'Y | G' ).setInput( 1 ).onConnect( update );
-		const zElement = new LabelElement( 'Z | B' ).setInput( 1 ).onConnect( update );
-		const wElement = new LabelElement( 'W | A' ).setInput( 1 ).onConnect( update );
+		const xElement = new LabelElement( 'x | r' ).setInput( 1 ).onConnect( update );
+		const yElement = new LabelElement( 'y | g' ).setInput( 1 ).onConnect( update );
+		const zElement = new LabelElement( 'z | b' ).setInput( 1 ).onConnect( update );
+		const wElement = new LabelElement( 'w | a' ).setInput( 1 ).onConnect( update );
 
 		this.add( xElement )
 			.add( yElement )

+ 17 - 0
playground/editors/MaterialEditor.js

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

+ 208 - 0
playground/editors/NodePrototypeEditor.js

@@ -0,0 +1,208 @@
+import { JavaScriptEditor } from './JavaScriptEditor.js';
+import { ScriptableEditor } from './ScriptableEditor.js';
+import { scriptable } from 'three/nodes';
+
+const defaultCode = `// Addition Node Example
+// Enjoy! :)
+
+// layout must be the first variable.
+
+layout = {
+	name: "Custom Addition",
+	outputType: 'node',
+	icon: 'heart-plus',
+	width: 200,
+	elements: [
+		{ name: 'A', inputType: 'node' },
+		{ name: 'B', inputType: 'node' }
+	]
+};
+
+// THREE and TSL (Three.js Shading Language) namespaces are available.
+// main code must be in the output function.
+
+const { add, float } = TSL;
+
+function main() {
+
+	const nodeA = parameters.get( 'A' ) || float();
+	const nodeB = parameters.get( 'B' ) || float();
+
+	return add( nodeA, nodeB );
+
+}
+`;
+
+export class NodePrototypeEditor extends JavaScriptEditor {
+
+	constructor( source = defaultCode ) {
+
+		super( source );
+
+		this.setName( 'Node Prototype' );
+
+		this.nodeClass = new WeakMap();
+		this.scriptableNode = scriptable( this.codeNode );
+
+		this.instances = [];
+
+		this.editorElement.addEventListener( 'change', () => {
+
+			this.updatePrototypes();
+
+		} );
+
+		this._prototype = null;
+
+		this.updatePrototypes();
+
+	}
+
+	serialize( data ) {
+
+		super.serialize( data );
+
+		data.source = this.source;
+
+	}
+
+	deserialize( data ) {
+
+		super.deserialize( data );
+
+		this.source = data.source;
+
+	}
+
+	deserializeLib( data, lib ) {
+
+		super.deserializeLib( data, lib );
+
+		this.source = data.source;
+
+		const nodePrototype = this.createPrototype();
+		lib[ nodePrototype.name ] = nodePrototype.nodeClass;
+
+	}
+
+	setEditor( editor ) {
+
+		super.setEditor( editor );
+
+		if ( editor === null ) {
+
+			for ( const proto of [ ...this.instances ] ) {
+
+				proto.dispose();
+
+			}
+
+			this.instances = [];
+
+		}
+
+		this.updatePrototypes();
+
+	}
+
+	createPrototype() {
+
+		if ( this._prototype !== null ) return this._prototype;
+
+		const nodePrototype = this;
+		const scriptableNode = this.scriptableNode;
+		const editorElement = this.editorElement;
+
+		const nodeClass = class extends ScriptableEditor {
+
+			constructor() {
+
+				super( scriptableNode.codeNode, false );
+
+				this.serializePriority = - 1;
+
+				this.onCode = this.onCode.bind( this );
+
+			}
+
+			onCode() {
+
+				this.update();
+
+			}
+
+			setEditor( editor ) {
+
+				super.setEditor( editor );
+
+				const index = nodePrototype.instances.indexOf( this );
+
+				if ( editor ) {
+
+					if ( index === - 1 ) nodePrototype.instances.push( this );
+
+					editorElement.addEventListener( 'change', this.onCode );
+
+				} else {
+
+					if ( index !== - 1 ) nodePrototype.instances.splice( index, 1 );
+
+					editorElement.removeEventListener( 'change', this.onCode );
+
+				}
+
+			}
+
+			get className() {
+
+				return scriptableNode.getLayout().name;
+
+			}
+
+		};
+
+		this._prototype = {
+			get name() {
+
+				return scriptableNode.getLayout().name;
+
+			},
+			get icon() {
+
+				return scriptableNode.getLayout().icon;
+
+			},
+			nodeClass,
+			reference: this,
+			editor: this.editor
+		};
+
+		return this._prototype;
+
+	}
+
+	updatePrototypes() {
+
+		if ( this._prototype !== null && this._prototype.editor !== null ) {
+
+			this._prototype.editor.removeClass( this._prototype );
+
+		}
+
+		//
+
+		const layout = this.scriptableNode.getLayout();
+
+		if ( layout && layout.name ) {
+
+			if ( this.editor ) {
+
+				this.editor.addClass( this.createPrototype() );
+
+			}
+
+		}
+
+	}
+
+}

+ 1 - 1
examples/jsm/node-editor/materials/PointsMaterialEditor.js → playground/editors/PointsMaterialEditor.js

@@ -1,4 +1,4 @@
-import { ColorInput, ToggleInput, SliderInput, LabelElement } from '../../libs/flow.module.js';
+import { ColorInput, ToggleInput, SliderInput, LabelElement } from 'flow';
 import { MaterialEditor } from './MaterialEditor.js';
 import { PointsNodeMaterial } from 'three/nodes';
 import * as THREE from 'three';

+ 9 - 11
examples/jsm/node-editor/utils/PreviewEditor.js → playground/editors/PreviewEditor.js

@@ -1,12 +1,10 @@
-import { OrbitControls } from '../../controls/OrbitControls.js';
-import { ViewHelper } from '../../helpers/ViewHelper.js';
-import { Element, LabelElement, SelectInput } from '../../libs/flow.module.js';
-import { BaseNode } from '../core/BaseNode.js';
-import { MeshBasicNodeMaterial, ConstNode } from 'three/nodes';
+import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
+import { ViewHelper } from 'three/addons/helpers/ViewHelper.js';
+import { Element, LabelElement, SelectInput } from 'flow';
+import { BaseNodeEditor } from '../BaseNodeEditor.js';
+import { MeshBasicNodeMaterial, float } from 'three/nodes';
 import { WebGLRenderer, PerspectiveCamera, Scene, Mesh, DoubleSide, SphereGeometry, BoxGeometry, PlaneGeometry, TorusKnotGeometry } from 'three';
 
-const nullValue = new ConstNode( 0 );
-
 const sceneDict = {};
 
 const getScene = ( name ) => {
@@ -48,17 +46,17 @@ const getScene = ( name ) => {
 
 };
 
-export class PreviewEditor extends BaseNode {
+export class PreviewEditor extends BaseNodeEditor {
 
 	constructor() {
 
 		const width = 300;
 		const height = 300;
 
-		super( 'Preview', 0, null, height );
+		super( 'Preview', null, height );
 
 		const material = new MeshBasicNodeMaterial();
-		material.colorNode = nullValue;
+		material.colorNode = float();
 		material.side = DoubleSide;
 		material.transparent = true;
 
@@ -78,7 +76,7 @@ export class PreviewEditor extends BaseNode {
 
 		const inputElement = new LabelElement( 'Input' ).setInput( 4 ).onConnect( () => {
 
-			material.colorNode = inputElement.getLinkedObject() || nullValue;
+			material.colorNode = inputElement.getLinkedObject() || float();
 			material.dispose();
 
 		}, true );

+ 480 - 0
playground/editors/ScriptableEditor.js

@@ -0,0 +1,480 @@
+import { BaseNodeEditor } from '../BaseNodeEditor.js';
+import { CodeEditorElement } from '../elements/CodeEditorElement.js';
+import { disposeScene, getColorFromType, createElementFromJSON, isGPUNode, onValidType } from '../NodeEditorUtils.js';
+import { global, scriptable, js, scriptableValue } from 'three/nodes';
+
+const defaultTitle = 'Scriptable';
+const defaultWidth = 500;
+
+export class ScriptableEditor extends BaseNodeEditor {
+
+	constructor( source = null, enableEditor = true ) {
+
+		let codeNode = null;
+		let scriptableNode = null;
+
+		if ( source && source.isCodeNode ) {
+
+			codeNode = source;
+
+		} else {
+
+			codeNode = js( source || '' );
+
+		}
+
+		scriptableNode = scriptable( codeNode );
+
+		super( defaultTitle, scriptableNode, defaultWidth );
+
+		this.scriptableNode = scriptableNode;
+		this.editorCodeNode = codeNode;
+		this.editorOutput = null;
+		this.editorOutputAdded = null;
+
+		this.layout = null;
+		this.editorElement = null;
+
+		this.layoutJSON = '';
+		this.initCacheKey = '';
+		this.initId = 0;
+		this.waitToLayoutJSON = null;
+
+		this.hasInternalEditor = false;
+
+		this._updating = false;
+
+		this.onValidElement = () => {};
+
+		if ( enableEditor ) {
+
+			this.title.setSerializable( true );
+
+			this._initExternalConnection();
+
+			this._toInternal();
+
+		}
+
+		const defaultOutput = this.scriptableNode.getDefaultOutput();
+		defaultOutput.events.addEventListener( 'refresh', () => {
+
+			this.update();
+
+		} );
+
+		this.update();
+
+	}
+
+	getOutputType() {
+
+		return this.layout ? this.layout.outputType : null;
+
+	}
+
+	hasJSON() {
+
+		return true;
+
+	}
+
+	exportJSON() {
+
+		return this.scriptableNode.toJSON();
+
+	}
+
+	setSource( source ) {
+
+		this.editorCodeNode.code = source;
+
+		this.update();
+
+		return this;
+
+	}
+
+	update( force = false ) {
+
+		if ( this._updating === true ) return;
+
+		this._updating = true;
+
+		this.scriptableNode.codeNode = this.codeNode;
+		this.scriptableNode.needsUpdate = true;
+
+		let layout = null;
+		let scriptableValueOutput = null;
+
+		try {
+
+			const object = this.scriptableNode.getObject();
+
+			layout = this.scriptableNode.getLayout();
+
+			this.updateLayout( layout, force );
+
+			scriptableValueOutput = this.scriptableNode.getDefaultOutput();
+
+			const initCacheKey = typeof object.init === 'function' ? object.init.toString() : '';
+
+			if ( initCacheKey !== this.initCacheKey ) {
+
+				this.initCacheKey = initCacheKey;
+
+				const initId = ++ this.initId;
+
+				this.scriptableNode.callAsync( 'init' ).then( () => {
+
+					if ( initId === this.initId ) {
+
+						this.update();
+
+						if ( this.editor ) this.editor.tips.message( 'ScriptEditor: Initialized.' );
+
+					}
+
+				} );
+
+			}
+
+		} catch ( e ) {
+
+			console.error( e );
+
+			if ( this.editor ) this.editor.tips.error( e.message );
+
+		}
+
+		const editorOutput = scriptableValueOutput ? scriptableValueOutput.value : null;
+
+		this.value = isGPUNode( editorOutput ) ? this.scriptableNode : scriptableValueOutput;
+		this.layout = layout;
+		this.editorOutput = editorOutput;
+
+		this.updateOutputInEditor();
+		this.updateOutputConnection();
+
+		this.invalidate();
+
+		this._updating = false;
+
+	}
+
+	updateOutputConnection() {
+
+		const layout = this.layout;
+
+		if ( layout ) {
+
+			const outputType = layout.outputType;
+
+			this.title.setOutputColor( getColorFromType( outputType ) );
+			this.title.setOutput( outputType && outputType !== 'void' ? this.outputLength : 0 );
+
+		} else {
+
+			this.title.setOutput( 0 );
+
+		}
+
+	}
+
+	updateOutputInEditor() {
+
+		const { editor, editorOutput, editorOutputAdded } = this;
+
+		if ( editor && editorOutput === editorOutputAdded ) return;
+
+		const scene = global.get( 'scene' );
+		const composer = global.get( 'composer' );
+
+		if ( editor ) {
+
+			if ( editorOutputAdded && editorOutputAdded.isObject3D === true ) {
+
+				editorOutputAdded.removeFromParent();
+
+				disposeScene( editorOutputAdded );
+
+			} else if ( composer && editorOutputAdded && editorOutputAdded.isPass === true ) {
+
+				composer.removePass( editorOutputAdded );
+
+			}
+
+			if ( editorOutput && editorOutput.isObject3D === true ) {
+
+				scene.add( editorOutput );
+
+			} else if ( composer && editorOutput && editorOutput.isPass === true ) {
+
+				composer.addPass( editorOutput );
+
+			}
+
+			this.editorOutputAdded = editorOutput;
+
+		} else {
+
+			if ( editorOutputAdded && editorOutputAdded.isObject3D === true ) {
+
+				editorOutputAdded.removeFromParent();
+
+				disposeScene( editorOutputAdded );
+
+			} else if ( composer && editorOutputAdded && editorOutputAdded.isPass === true ) {
+
+				composer.removePass( editorOutputAdded );
+
+			}
+
+			this.editorOutputAdded = null;
+
+		}
+
+	}
+
+	setEditor( editor ) {
+
+		super.setEditor( editor );
+
+		this.updateOutputInEditor();
+
+	}
+
+	clearParameters() {
+
+		this.layoutJSON = '';
+
+		this.scriptableNode.clearParameters();
+
+		for ( const element of this.elements.concat() ) {
+
+			if ( element !== this.editorElement && element !== this.title ) {
+
+				this.remove( element );
+
+			}
+
+		}
+
+	}
+
+	addElementFromJSON( json ) {
+
+		const { id, element, inputNode, outputType } = createElementFromJSON( json );
+
+		this.add( element );
+
+		this.scriptableNode.setParameter( id, inputNode );
+
+		if ( outputType ) {
+
+			element.setObjectCallback( () => {
+
+				return this.scriptableNode.getOutput( id );
+
+			} );
+
+		}
+
+		//
+
+		const onUpdate = () => {
+
+			const value = element.value;
+			const paramValue = value && value.isScriptableValueNode ? value : scriptableValue( value );
+
+			this.scriptableNode.setParameter( id, paramValue );
+
+			this.update();
+
+		};
+
+		element.addEventListener( 'changeInput', onUpdate );
+		element.onConnect( onUpdate, true );
+
+		//element.onConnect( () => this.getScriptable().call( 'onDeepChange' ), true );
+
+		return element;
+
+	}
+
+	updateLayout( layout = null, force = false ) {
+
+		const needsUpdateWidth = this.hasExternalEditor || this.editorElement === null;
+
+		if ( this.waitToLayoutJSON !== null ) {
+
+			if ( this.waitToLayoutJSON === JSON.stringify( layout || '{}' ) ) {
+
+				this.waitToLayoutJSON = null;
+
+				if ( needsUpdateWidth ) this.setWidth( layout.width );
+
+			} else {
+
+				return;
+
+			}
+
+		}
+
+		if ( layout ) {
+
+			const layoutCacheKey = JSON.stringify( layout );
+
+			if ( this.layoutJSON !== layoutCacheKey || force === true ) {
+
+				this.clearParameters();
+
+				if ( layout.name ) {
+
+					this.setName( layout.name );
+
+				}
+
+
+				if ( layout.icon ) {
+
+					this.setIcon( layout.icon );
+
+				}
+
+				if ( needsUpdateWidth ) {
+
+					if ( layout.width !== undefined ) {
+
+                		this.setWidth( layout.width );
+
+					} else {
+
+						this.setWidth( defaultWidth );
+
+					}
+
+				}
+
+				if ( layout.elements ) {
+
+					for ( const element of layout.elements ) {
+
+						this.addElementFromJSON( element );
+
+					}
+
+					if ( this.editorElement ) {
+
+						this.remove( this.editorElement );
+						this.add( this.editorElement );
+
+					}
+
+				}
+
+				this.layoutJSON = layoutCacheKey;
+
+			}
+
+		} else {
+
+			this.setName( defaultTitle );
+			this.setIcon( null );
+			this.setWidth( defaultWidth );
+
+			this.clearParameters();
+
+		}
+
+		this.updateOutputConnection();
+
+	}
+
+	get hasExternalEditor() {
+
+		return this.title.getLinkedObject() !== null;
+
+	}
+
+	get codeNode() {
+
+		return this.hasExternalEditor ? this.title.getLinkedObject() : this.editorCodeNode;
+
+	}
+
+	_initExternalConnection() {
+
+		this.title.setInputColor( getColorFromType( 'CodeNode' ) ).setInput( 1 ).onValid( onValidType( 'CodeNode' ) ).onConnect( () => {
+
+			this.hasExternalEditor ? this._toExternal() : this._toInternal();
+
+			this.update();
+
+		}, true );
+
+	}
+
+	_toInternal() {
+
+		if ( this.hasInternalEditor === true ) return;
+
+		if ( this.editorElement === null ) {
+
+			this.editorElement = new CodeEditorElement( this.editorCodeNode.code );
+			this.editorElement.addEventListener( 'change', () => {
+
+				this.setSource( this.editorElement.source );
+
+				this.editorElement.focus();
+
+			} );
+
+			this.add( this.editorElement );
+
+		}
+
+		this.setResizable( true );
+
+		this.editorElement.setVisible( true );
+
+		this.hasInternalEditor = true;
+
+		this.update( /*true*/ );
+
+	}
+
+	_toExternal() {
+
+		if ( this.hasInternalEditor === false ) return;
+
+		this.editorElement.setVisible( false );
+
+		this.setResizable( false );
+
+		this.hasInternalEditor = false;
+
+		this.update( /*true*/ );
+
+	}
+
+	serialize( data ) {
+
+		super.serialize( data );
+
+		data.layoutJSON = this.layoutJSON;
+
+	}
+
+	deserialize( data ) {
+
+		this.updateLayout( JSON.parse( data.layoutJSON || '{}' ), true );
+
+		this.waitToLayoutJSON = data.layoutJSON;
+
+		super.deserialize( data );
+
+	}
+
+}

+ 6 - 6
examples/jsm/node-editor/inputs/SliderEditor.js → playground/editors/SliderEditor.js

@@ -1,14 +1,14 @@
-import { ButtonInput, SliderInput, NumberInput, LabelElement, Element } from '../../libs/flow.module.js';
-import { BaseNode } from '../core/BaseNode.js';
-import { UniformNode } from 'three/nodes';
+import { ButtonInput, SliderInput, NumberInput, LabelElement, Element } from 'flow';
+import { BaseNodeEditor } from '../BaseNodeEditor.js';
+import { float } from 'three/nodes';
 
-export class SliderEditor extends BaseNode {
+export class SliderEditor extends BaseNodeEditor {
 
 	constructor() {
 
-		const node = new UniformNode( 0 );
+		const node = float( 0 );
 
-		super( 'Slider', 1, node );
+		super( 'Slider', node );
 
 		this.collapse = true;
 

+ 50 - 0
playground/editors/SplitEditor.js

@@ -0,0 +1,50 @@
+import { LabelElement } from 'flow';
+import { BaseNodeEditor } from '../BaseNodeEditor.js';
+import { nodeObject, float } from 'three/nodes';
+
+export class SplitEditor extends BaseNodeEditor {
+
+	constructor() {
+
+		super( 'Split', null, 175 );
+
+		let node = null;
+
+		const inputElement = new LabelElement( 'Input' ).setInput( 1 ).onConnect( () => {
+
+			node = inputElement.getLinkedObject();
+
+			if ( node !== null ) {
+
+				xElement.setObject( nodeObject( node ).x );
+				yElement.setObject( nodeObject( node ).y );
+				zElement.setObject( nodeObject( node ).z );
+				wElement.setObject( nodeObject( node ).w );
+
+			} else {
+
+				xElement.setObject( float() );
+				yElement.setObject( float() );
+				zElement.setObject( float() );
+				wElement.setObject( float() );
+
+			}
+
+		} );
+
+		this.add( inputElement );
+
+		const xElement = new LabelElement( 'x | r' ).setOutput( 1 ).setObject( float() );
+		const yElement = new LabelElement( 'y | g' ).setOutput( 1 ).setObject( float() );
+		const zElement = new LabelElement( 'z | b' ).setOutput( 1 ).setObject( float() );
+		const wElement = new LabelElement( 'w | a' ).setOutput( 1 ).setObject( float() );
+
+		this.add( inputElement )
+			.add( xElement )
+			.add( yElement )
+			.add( zElement )
+			.add( wElement );
+
+	}
+
+}

+ 1 - 1
examples/jsm/node-editor/materials/StandardMaterialEditor.js → playground/editors/StandardMaterialEditor.js

@@ -1,4 +1,4 @@
-import { ColorInput, SliderInput, LabelElement } from '../../libs/flow.module.js';
+import { ColorInput, SliderInput, LabelElement } from 'flow';
 import { MaterialEditor } from './MaterialEditor.js';
 import { MeshStandardNodeMaterial } from 'three/nodes';
 

+ 35 - 0
playground/editors/StringEditor.js

@@ -0,0 +1,35 @@
+import { BaseNodeEditor } from '../BaseNodeEditor.js';
+import { createElementFromJSON } from '../NodeEditorUtils.js';
+
+export class StringEditor extends BaseNodeEditor {
+
+	constructor() {
+
+		const { element, inputNode } = createElementFromJSON( {
+			inputType: 'string',
+			inputConnection: false
+		} );
+
+		super( 'String', inputNode, 350 );
+
+		this.setOutputLength( 1 );
+
+		element.addEventListener( 'changeInput', () => this.invalidate() );
+
+		this.add( element );
+
+	}
+
+	get stringNode() {
+
+		return this.value;
+
+	}
+
+	getURL() {
+
+		return this.stringNode.value;
+
+	}
+
+}

+ 46 - 0
playground/editors/SwizzleEditor.js

@@ -0,0 +1,46 @@
+import { LabelElement } from 'flow';
+import { BaseNodeEditor } from '../BaseNodeEditor.js';
+import { createElementFromJSON } from '../NodeEditorUtils.js';
+import { split, float } from 'three/nodes';
+
+export class SwizzleEditor extends BaseNodeEditor {
+
+	constructor() {
+
+		const node = split( float(), 'x' );
+
+		super( 'Swizzle', node, 175 );
+
+		const inputElement = new LabelElement( 'Input' ).setInput( 1 ).onConnect( () => {
+
+			node.node = inputElement.getLinkedObject() || float();
+
+		} );
+
+		this.add( inputElement );
+
+		//
+
+		const { element: componentsElement } = createElementFromJSON( {
+			inputType: 'String',
+			allows: 'xyzwrgba',
+			transform: 'lowercase',
+			options: [ 'x', 'y', 'z', 'w', 'r', 'g', 'b', 'a' ],
+			maxLength: 4
+		} );
+
+		componentsElement.addEventListener( 'changeInput', () => {
+
+			const string = componentsElement.value.value;
+
+			node.components = string || 'x';
+
+			this.invalidate();
+
+		} );
+
+		this.add( componentsElement );
+
+	}
+
+}

+ 20 - 55
examples/jsm/node-editor/inputs/TextureEditor.js → playground/editors/TextureEditor.js

@@ -1,46 +1,29 @@
-import { LabelElement, ToggleInput, SelectInput } from '../../libs/flow.module.js';
-import { BaseNode, onNodeValidElement } from '../core/BaseNode.js';
-import { TextureNode, UVNode } from 'three/nodes';
+import { LabelElement, ToggleInput, SelectInput } from 'flow';
+import { BaseNodeEditor } from '../BaseNodeEditor.js';
+import { onValidNode, onValidType, getColorFromType } from '../NodeEditorUtils.js';
+import { texture, uv } from 'three/nodes';
 import { Texture, TextureLoader, RepeatWrapping, ClampToEdgeWrapping, MirroredRepeatWrapping } from 'three';
 
-const fileTexture = new WeakMap();
-const fileURL = new WeakMap();
 const textureLoader = new TextureLoader();
 const defaultTexture = new Texture();
-const defaultUV = new UVNode();
 
-const getTexture = ( file ) => {
+let defaultUV = null;
 
-	let texture = fileTexture.get( file );
+const getTexture = ( url ) => {
 
-	if ( texture === undefined || file.getURL() !== fileURL.get( file ) ) {
-
-		const url = file.getURL();
-
-		if ( texture !== undefined ) {
-
-			texture.dispose();
-
-		}
-
-		texture = textureLoader.load( url );
-
-		fileTexture.set( file, texture );
-		fileURL.set( file, url );
-
-	}
-
-	return texture;
+	return textureLoader.load( url );
 
 };
 
-export class TextureEditor extends BaseNode {
+export class TextureEditor extends BaseNodeEditor {
 
 	constructor() {
 
-		const node = new TextureNode( defaultTexture );
+		const node = texture( defaultTexture );
 
-		super( 'Texture', 4, node, 250 );
+		super( 'Texture', node, 250 );
+
+		this.setOutputLength( 4 );
 
 		this.texture = null;
 
@@ -53,34 +36,16 @@ export class TextureEditor extends BaseNode {
 
 	_initFile() {
 
-		const fileElement = new LabelElement( 'File' ).setInputColor( 'aqua' ).setInput( 1 );
-
-		fileElement.onValid( ( source, target, stage ) => {
-
-			const object = target.getObject();
-
-			if ( object && object.isDataFile !== true ) {
-
-				if ( stage === 'dragged' ) {
-
-					const name = target.node.getName();
+		const fileElement = new LabelElement( 'File' ).setInputColor( getColorFromType( 'URL' ) ).setInput( 1 );
 
-					this.editor.tips.error( `"${name}" is not a File.` );
+		fileElement.onValid( onValidType( 'URL' ) ).onConnect( () => {
 
-				}
-
-				return false;
-
-			}
-
-		} ).onConnect( () => {
-
-			const file = fileElement.getLinkedObject();
-			const node = this.value;
+			const textureNode = this.value;
+			const fileEditorElement = fileElement.getLinkedElement();
 
-			this.texture = file ? getTexture( file ) : null;
+			this.texture = fileEditorElement ? getTexture( fileEditorElement.node.getURL() ) : null;
 
-			node.value = this.texture || defaultTexture;
+			textureNode.value = this.texture || defaultTexture;
 
 			this.update();
 
@@ -94,11 +59,11 @@ export class TextureEditor extends BaseNode {
 
 		const uvField = new LabelElement( 'UV' ).setInput( 2 );
 
-		uvField.onValid( onNodeValidElement ).onConnect( () => {
+		uvField.onValid( onValidNode ).onConnect( () => {
 
 			const node = this.value;
 
-			node.uvNode = uvField.getLinkedObject() || defaultUV;
+			node.uvNode = uvField.getLinkedObject() || defaultUV || ( defaultUV = uv() );
 
 		} );
 

+ 6 - 6
examples/jsm/node-editor/utils/TimerEditor.js → playground/editors/TimerEditor.js

@@ -1,14 +1,14 @@
-import { NumberInput, LabelElement, Element, ButtonInput } from '../../libs/flow.module.js';
-import { BaseNode } from '../core/BaseNode.js';
-import { TimerNode } from 'three/nodes';
+import { NumberInput, LabelElement, Element, ButtonInput } from 'flow';
+import { BaseNodeEditor } from '../BaseNodeEditor.js';
+import { timerLocal } from 'three/nodes';
 
-export class TimerEditor extends BaseNode {
+export class TimerEditor extends BaseNodeEditor {
 
 	constructor() {
 
-		const node = new TimerNode();
+		const node = timerLocal();
 
-		super( 'Timer', 1, node, 200 );
+		super( 'Timer', node, 200 );
 
 		this.title.setIcon( 'ti ti-clock' );
 

+ 27 - 0
playground/editors/UVEditor.js

@@ -0,0 +1,27 @@
+import { SelectInput, LabelElement } from 'flow';
+import { BaseNodeEditor } from '../BaseNodeEditor.js';
+import { uv } from 'three/nodes';
+
+export class UVEditor extends BaseNodeEditor {
+
+	constructor() {
+
+		const node = uv();
+
+		super( 'UV', node, 200 );
+
+		this.setOutputLength( 2 );
+
+		const optionsField = new SelectInput( [ '1', '2' ], 0 ).onChange( () => {
+
+			node.index = Number( optionsField.getValue() );
+
+			this.invalidate();
+
+		} );
+
+		this.add( new LabelElement( 'Channel' ).add( optionsField ) );
+
+	}
+
+}

+ 24 - 0
playground/editors/Vector2Editor.js

@@ -0,0 +1,24 @@
+import { BaseNodeEditor } from '../BaseNodeEditor.js';
+import { createElementFromJSON } from '../NodeEditorUtils.js';
+
+export class Vector2Editor extends BaseNodeEditor {
+
+	constructor() {
+
+		const { element, inputNode } = createElementFromJSON( {
+			inputType: 'vec2',
+			inputConnection: false
+		} );
+
+		super( 'Vector 2', inputNode );
+
+		this.setOutputLength( 2 );
+
+		element.addEventListener( 'changeInput', () => this.invalidate() );
+
+		this.add( element );
+
+
+	}
+
+}

+ 23 - 0
playground/editors/Vector3Editor.js

@@ -0,0 +1,23 @@
+import { BaseNodeEditor } from '../BaseNodeEditor.js';
+import { createElementFromJSON } from '../NodeEditorUtils.js';
+
+export class Vector3Editor extends BaseNodeEditor {
+
+	constructor() {
+
+		const { element, inputNode } = createElementFromJSON( {
+			inputType: 'vec3',
+			inputConnection: false
+		} );
+
+		super( 'Vector 3', inputNode, 325 );
+
+		this.setOutputLength( 3 );
+
+		element.addEventListener( 'changeInput', () => this.invalidate() );
+
+		this.add( element );
+
+	}
+
+}

+ 23 - 0
playground/editors/Vector4Editor.js

@@ -0,0 +1,23 @@
+import { BaseNodeEditor } from '../BaseNodeEditor.js';
+import { createElementFromJSON } from '../NodeEditorUtils.js';
+
+export class Vector4Editor extends BaseNodeEditor {
+
+	constructor() {
+
+		const { element, inputNode } = createElementFromJSON( {
+			inputType: 'vec4',
+			inputConnection: false
+		} );
+
+		super( 'Vector 4', inputNode, 350 );
+
+		this.setOutputLength( 4 );
+
+		element.addEventListener( 'changeInput', () => this.invalidate() );
+
+		this.add( element );
+
+	}
+
+}

+ 98 - 0
playground/elements/CodeEditorElement.js

@@ -0,0 +1,98 @@
+import { Element, LoaderLib } from 'flow';
+
+export class CodeEditorElement extends Element {
+
+	constructor( source = '' ) {
+
+		super();
+
+		this.updateInterval = 500;
+
+		this._source = source;
+
+		this.dom.style[ 'z-index' ] = - 1;
+		this.dom.classList.add( 'no-zoom' );
+
+		this.setHeight( 500 );
+
+		const editorDOM = document.createElement( 'div' );
+		editorDOM.style.width = '100%';
+		editorDOM.style.height = '100%';
+		this.dom.appendChild( editorDOM );
+
+		this.editor = null; // async
+
+		window.require.config( { paths: { 'vs': 'https://cdn.jsdelivr.net/npm/monaco-editor@latest/min/vs' } } );
+
+		require( [ 'vs/editor/editor.main' ], () => {
+
+			this.editor = window.monaco.editor.create( editorDOM, {
+				value: this.source,
+				language: 'javascript',
+				theme: 'vs-dark',
+				automaticLayout: true
+			} );
+
+			let timeout = null;
+
+			this.editor.getModel().onDidChangeContent( () => {
+
+				this._source = this.editor.getValue();
+
+				if ( timeout ) clearTimeout( timeout );
+
+				timeout = setTimeout( () => {
+
+					this.dispatchEvent( new Event( 'change' ) );
+
+				}, this.updateInterval );
+
+			} );
+
+		} );
+
+	}
+
+	set source( value ) {
+
+		if ( this._source === value ) return;
+
+		this._source = value;
+
+		if ( this.editor ) this.editor.setValue( value );
+
+		this.dispatchEvent( new Event( 'change' ) );
+
+	}
+
+	get source() {
+
+		return this._source;
+
+	}
+
+	focus() {
+
+		if ( this.editor ) this.editor.focus();
+
+	}
+
+	serialize( data ) {
+
+		super.serialize( data );
+
+		data.source = this.source;
+
+	}
+
+	deserialize( data ) {
+
+		super.deserialize( data );
+
+		this.source = data.source || '';
+
+	}
+
+}
+
+LoaderLib[ 'CodeEditorElement' ] = CodeEditorElement;

File diff suppressed because it is too large
+ 0 - 0
playground/examples/universal/fresnel.json


File diff suppressed because it is too large
+ 0 - 0
playground/examples/universal/matcap.json


File diff suppressed because it is too large
+ 0 - 0
playground/examples/universal/teapot.json


File diff suppressed because it is too large
+ 0 - 0
playground/examples/webgl/car.json


File diff suppressed because it is too large
+ 0 - 0
playground/examples/webgpu/particle.json


+ 0 - 0
examples/fonts/open-sans/open-sans-v15-cyrillic-ext_greek_greek-ext_cyrillic_latin_latin-ext_vietnamese-regular.woff → playground/fonts/open-sans/open-sans-v15-cyrillic-ext_greek_greek-ext_cyrillic_latin_latin-ext_vietnamese-regular.woff


+ 0 - 0
examples/fonts/open-sans/open-sans-v15-cyrillic-ext_greek_greek-ext_cyrillic_latin_latin-ext_vietnamese-regular.woff2 → playground/fonts/open-sans/open-sans-v15-cyrillic-ext_greek_greek-ext_cyrillic_latin_latin-ext_vietnamese-regular.woff2


+ 0 - 0
examples/fonts/open-sans/open-sans.css → playground/fonts/open-sans/open-sans.css


BIN
playground/fonts/tabler-icons/fonts/tabler-icons.eot


BIN
playground/fonts/tabler-icons/fonts/tabler-icons.ttf


BIN
playground/fonts/tabler-icons/fonts/tabler-icons.woff


BIN
playground/fonts/tabler-icons/fonts/tabler-icons.woff2


File diff suppressed because it is too large
+ 513 - 9
playground/fonts/tabler-icons/tabler-icons.css


File diff suppressed because it is too large
+ 663 - 6
playground/fonts/tabler-icons/tabler-icons.html


File diff suppressed because it is too large
+ 3 - 0
playground/fonts/tabler-icons/tabler-icons.min.css


File diff suppressed because it is too large
+ 436 - 11
playground/fonts/tabler-icons/tabler-icons.scss


+ 202 - 0
playground/index.html

@@ -0,0 +1,202 @@
+<!DOCTYPE html>
+<html lang="en">
+	<head>
+		<title>three.js - 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"/>
+		<link rel="stylesheet" href="fonts/tabler-icons/tabler-icons.min.css" type="text/css"/>
+		<style>
+
+			body {
+				overflow: hidden;
+				width: 100%;
+				height: 100%;
+				top: 0;
+				left: 0;
+				margin: 0;
+				position: fixed;
+				overscroll-behavior: none;
+			}
+
+			.renderer {
+				position: absolute;
+				top: 0;
+				left: 0;
+				height: 100%;
+				width: 100%;
+			}
+
+			flow {
+				position: absolute;
+				top: 0;
+				left: 0;
+				height: 100%;
+				width: 100%;
+				box-shadow: inset 0 0 20px 0px #000000;
+				pointer-events: none;
+			}
+
+			flow > * {
+				pointer-events: auto;
+			}
+
+			flow f-canvas.focusing {
+				pointer-events: none;
+			}
+
+			flow f-canvas:not(.focusing) {
+				background: #191919ed;
+			}
+
+		</style>
+	</head>
+	<body>
+
+		<script src="https://cdn.jsdelivr.net/npm/monaco-editor@latest/min/vs/loader.min.js"></script>
+
+		<script type="importmap">
+			{
+				"imports": {
+					"three": "../build/three.module.js",
+					"three/addons/": "../examples/jsm/",
+					"three/nodes": "../examples/jsm/nodes/Nodes.js",
+					"flow": "./libs/flow.module.js"
+				}
+			}
+		</script>
+
+		<script type="module">
+
+			import * as THREE from 'three';
+
+			import { nodeFrame } from 'three/addons/renderers/webgl/nodes/WebGLNodes.js';
+
+			import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
+
+			import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
+			import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
+
+			import { NodeEditor } from './NodeEditor.js';
+
+			let camera, scene, renderer, composer;
+			let nodeEditor;
+
+			init();
+
+			async function init() {
+
+				const urlParams = new URLSearchParams( window.location.search );
+				const backend = urlParams.get( 'backend' );
+
+				//
+
+				camera = new THREE.PerspectiveCamera( 40, window.innerWidth / window.innerHeight, 0.5, 60 );
+				camera.position.set( 0.0, 3, 4 * 3 );
+
+				scene = new THREE.Scene();
+				scene.background = new THREE.Color( 0x333333 );
+
+				//
+
+				if ( backend === 'webgpu' ) {
+
+					const { default: WebGPU } = await import( 'three/addons/capabilities/WebGPU.js' );
+					const { default: WebGPURenderer } = await import( 'three/addons/renderers/webgpu/WebGPURenderer.js' );
+
+					renderer = new WebGPURenderer();
+
+					if ( WebGPU.isAvailable() === false ) {
+
+						document.body.appendChild( WebGPU.getErrorMessage() );
+
+						throw new Error( 'No WebGPU support' );
+
+					}
+
+				} else {
+
+					renderer = new THREE.WebGLRenderer( { antialias: true } );
+					renderer.useLegacyLights = false;
+
+					composer = new EffectComposer( renderer );
+					composer.addPass( new RenderPass( scene, camera ) );
+
+				}
+
+				//
+
+				renderer.setAnimationLoop( render );
+				renderer.outputEncoding = THREE.sRGBEncoding;
+				renderer.toneMapping = THREE.LinearToneMapping;
+				renderer.toneMappingExposure = 1;
+				document.body.appendChild( renderer.domElement );
+
+				renderer.domElement.className = 'renderer';
+
+				//
+
+				const controls = new OrbitControls( camera, renderer.domElement );
+				controls.minDistance = 1;
+				controls.maxDistance = 30;
+
+				window.addEventListener( 'resize', onWindowResize );
+
+				initEditor();
+
+				onWindowResize();
+
+			}
+
+			function initEditor() {
+
+				nodeEditor = new NodeEditor( scene, renderer, composer );
+
+				nodeEditor.addEventListener( 'new', () => {
+
+					//renderer.dispose();
+
+				} );
+
+				document.body.appendChild( nodeEditor.domElement );
+
+			}
+
+			function onWindowResize() {
+
+				const width = window.innerWidth;
+				const height = window.innerHeight;
+
+				camera.aspect = width / height;
+				camera.updateProjectionMatrix();
+
+				renderer.setSize( width, height );
+				if ( composer ) composer.setSize( width, height );
+
+				nodeEditor.setSize( width, height );
+
+			}
+
+			//
+
+			function animate() {
+
+				requestAnimationFrame( animate );
+
+				if ( renderer.isWebGLRenderer ) nodeFrame.update();
+
+				render();
+
+			}
+
+			function render() {
+
+				if ( composer && composer.passes.length > 1 ) composer.render();
+				else renderer.render( scene, camera );
+
+			}
+
+		</script>
+
+	</body>
+</html>

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


+ 0 - 2
test/e2e/puppeteer.js

@@ -64,9 +64,7 @@ const exceptionList = [
 	'webgl_loader_pdb',
 	'webgl_multiple_canvases_circle',
 	'webgl_multiple_elements_text',
-	'webgl_nodes_playground',
 	'webgl_shaders_tonemapping',
-	'webgpu_nodes_playground',
 
 	// Unknown
 	// TODO: most of these can be fixed just by increasing idleTime and parseTime

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