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 năm trước cách đây
mục cha
commit
03e4e05557
100 tập tin đã thay đổi với 6729 bổ sung2893 xóa
  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


Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 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 );
-
-	}
-
-}

Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 0 - 0
examples/jsm/node-editor/examples/animate-uv.json


Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 0 - 0
examples/jsm/node-editor/examples/fake-top-light.json


Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 0 - 0
examples/jsm/node-editor/examples/matcap.json


Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 0 - 0
examples/jsm/node-editor/examples/oscillator-color.json


Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 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;

Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 0 - 0
playground/examples/universal/fresnel.json


Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 0 - 0
playground/examples/universal/matcap.json


Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 0 - 0
playground/examples/universal/teapot.json


Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 0 - 0
playground/examples/webgl/car.json


Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 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


Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 513 - 9
playground/fonts/tabler-icons/tabler-icons.css


Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 663 - 6
playground/fonts/tabler-icons/tabler-icons.html


Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 3 - 0
playground/fonts/tabler-icons/tabler-icons.min.css


Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 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>

Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 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

Một số tệp đã không được hiển thị bởi vì quá nhiều tập tin thay đổi trong này khác