NodeEditor.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663
  1. import { Styles, Canvas, CircleMenu, ButtonInput, ContextMenu, Tips, Search, Loader } from '../libs/flow.module.js';
  2. import { StandardMaterialEditor } from './materials/StandardMaterialEditor.js';
  3. import { OperatorEditor } from './math/OperatorEditor.js';
  4. import { NormalizeEditor } from './math/NormalizeEditor.js';
  5. import { InvertEditor } from './math/InvertEditor.js';
  6. import { LimiterEditor } from './math/LimiterEditor.js';
  7. import { DotEditor } from './math/DotEditor.js';
  8. import { PowerEditor } from './math/PowerEditor.js';
  9. import { TrigonometryEditor } from './math/TrigonometryEditor.js';
  10. import { FloatEditor } from './inputs/FloatEditor.js';
  11. import { Vector2Editor } from './inputs/Vector2Editor.js';
  12. import { Vector3Editor } from './inputs/Vector3Editor.js';
  13. import { Vector4Editor } from './inputs/Vector4Editor.js';
  14. import { SliderEditor } from './inputs/SliderEditor.js';
  15. import { ColorEditor } from './inputs/ColorEditor.js';
  16. import { BlendEditor } from './display/BlendEditor.js';
  17. import { UVEditor } from './accessors/UVEditor.js';
  18. import { PositionEditor } from './accessors/PositionEditor.js';
  19. import { NormalEditor } from './accessors/NormalEditor.js';
  20. import { TimerEditor } from './utils/TimerEditor.js';
  21. import { OscillatorEditor } from './utils/OscillatorEditor.js';
  22. import { SplitEditor } from './utils/SplitEditor.js';
  23. import { JoinEditor } from './utils/JoinEditor.js';
  24. import { CheckerEditor } from './procedural/CheckerEditor.js';
  25. import { MeshEditor } from './scene/MeshEditor.js';
  26. import { EventDispatcher } from 'three';
  27. Styles.icons.unlink = 'ti ti-unlink';
  28. export const NodeList = [
  29. {
  30. name: 'Inputs',
  31. icon: 'forms',
  32. children: [
  33. {
  34. name: 'Slider',
  35. icon: 'adjustments-horizontal',
  36. nodeClass: SliderEditor
  37. },
  38. {
  39. name: 'Float',
  40. icon: 'box-multiple-1',
  41. nodeClass: FloatEditor
  42. },
  43. {
  44. name: 'Vector 2',
  45. icon: 'box-multiple-2',
  46. nodeClass: Vector2Editor
  47. },
  48. {
  49. name: 'Vector 3',
  50. icon: 'box-multiple-3',
  51. nodeClass: Vector3Editor
  52. },
  53. {
  54. name: 'Vector 4',
  55. icon: 'box-multiple-4',
  56. nodeClass: Vector4Editor
  57. },
  58. {
  59. name: 'Color',
  60. icon: 'palette',
  61. nodeClass: ColorEditor
  62. }
  63. ]
  64. },
  65. {
  66. name: 'Accessors',
  67. icon: 'vector-triangle',
  68. children: [
  69. {
  70. name: 'UV',
  71. icon: 'details',
  72. nodeClass: UVEditor
  73. },
  74. {
  75. name: 'Position',
  76. icon: 'hierarchy',
  77. nodeClass: PositionEditor
  78. },
  79. {
  80. name: 'Normal',
  81. icon: 'fold-up',
  82. nodeClass: NormalEditor
  83. }
  84. ]
  85. },
  86. {
  87. name: 'Display',
  88. icon: 'brightness',
  89. children: [
  90. {
  91. name: 'Blend',
  92. icon: 'layers-subtract',
  93. nodeClass: BlendEditor
  94. }
  95. ]
  96. },
  97. {
  98. name: 'Math',
  99. icon: 'calculator',
  100. children: [
  101. {
  102. name: 'Operator',
  103. icon: 'math-symbols',
  104. nodeClass: OperatorEditor
  105. },
  106. {
  107. name: 'Invert',
  108. icon: 'flip-vertical',
  109. tip: 'Negate',
  110. nodeClass: OperatorEditor
  111. },
  112. {
  113. name: 'Limiter',
  114. icon: 'arrow-bar-to-up',
  115. tip: 'Min / Max',
  116. nodeClass: LimiterEditor
  117. },
  118. {
  119. name: 'Dot Product',
  120. icon: 'arrows-up-left',
  121. nodeClass: DotEditor
  122. },
  123. {
  124. name: 'Power',
  125. icon: 'arrow-up-right',
  126. nodeClass: PowerEditor
  127. },
  128. {
  129. name: 'Trigonometry',
  130. icon: 'wave-sine',
  131. tip: 'Sin / Cos / Tan',
  132. nodeClass: TrigonometryEditor
  133. },
  134. {
  135. name: 'Normalize',
  136. icon: 'fold',
  137. nodeClass: NormalizeEditor
  138. }
  139. ]
  140. },
  141. {
  142. name: 'Procedural',
  143. icon: 'infinity',
  144. children: [
  145. {
  146. name: 'Checker',
  147. icon: 'border-outer',
  148. nodeClass: CheckerEditor
  149. }
  150. ]
  151. },
  152. {
  153. name: 'Utils',
  154. icon: 'apps',
  155. children: [
  156. {
  157. name: 'Timer',
  158. icon: 'clock',
  159. nodeClass: TimerEditor
  160. },
  161. {
  162. name: 'Oscillator',
  163. icon: 'wave-sine',
  164. nodeClass: OscillatorEditor
  165. },
  166. {
  167. name: 'Split',
  168. icon: 'arrows-split-2',
  169. nodeClass: SplitEditor
  170. },
  171. {
  172. name: 'Join',
  173. icon: 'arrows-join-2',
  174. nodeClass: JoinEditor
  175. }
  176. ]
  177. },
  178. /*{
  179. name: 'Scene',
  180. icon: '3d-cube-sphere',
  181. children: [
  182. {
  183. name: 'Mesh',
  184. icon: '3d-cube-sphere',
  185. nodeClass: MeshEditor
  186. }
  187. ]
  188. },*/
  189. {
  190. name: 'Material',
  191. icon: 'circles',
  192. children: [
  193. {
  194. name: 'Standard Material',
  195. icon: 'circle',
  196. nodeClass: StandardMaterialEditor
  197. }
  198. ]
  199. }
  200. ];
  201. export const ClassLib = {
  202. StandardMaterialEditor,
  203. MeshEditor,
  204. OperatorEditor,
  205. NormalizeEditor,
  206. InvertEditor,
  207. LimiterEditor,
  208. DotEditor,
  209. PowerEditor,
  210. TrigonometryEditor,
  211. FloatEditor,
  212. Vector2Editor,
  213. Vector3Editor,
  214. Vector4Editor,
  215. SliderEditor,
  216. ColorEditor,
  217. BlendEditor,
  218. UVEditor,
  219. PositionEditor,
  220. NormalEditor,
  221. TimerEditor,
  222. OscillatorEditor,
  223. SplitEditor,
  224. JoinEditor,
  225. CheckerEditor
  226. };
  227. export class NodeEditor extends EventDispatcher {
  228. constructor( scene = null ) {
  229. super();
  230. const domElement = document.createElement( 'flow' );
  231. const canvas = new Canvas();
  232. domElement.append( canvas.dom );
  233. this.scene = scene;
  234. this.canvas = canvas;
  235. this.domElement = domElement;
  236. this.nodesContext = null;
  237. this.examplesContext = null;
  238. this._initTips();
  239. this._initMenu();
  240. this._initSearch();
  241. this._initNodesContext();
  242. this._initExamplesContext();
  243. }
  244. centralizeNode( node ) {
  245. const canvas = this.canvas;
  246. const canvasRect = canvas.rect;
  247. const nodeRect = node.dom.getBoundingClientRect();
  248. const defaultOffsetX = nodeRect.width;
  249. const defaultOffsetY = nodeRect.height;
  250. node.setPosition(
  251. ( canvas.relativeX + ( canvasRect.width / 2 ) ) - defaultOffsetX,
  252. ( canvas.relativeY + ( canvasRect.height / 2 ) ) - defaultOffsetY
  253. );
  254. }
  255. add( node ) {
  256. const onRemove = () => {
  257. node.removeEventListener( 'remove', onRemove );
  258. node.setEditor( null );
  259. };
  260. node.setEditor( this );
  261. node.addEventListener( 'remove', onRemove );
  262. this.canvas.add( node );
  263. this.dispatchEvent( { type: 'add', node } );
  264. return this;
  265. }
  266. get nodes() {
  267. return this.canvas.nodes;
  268. }
  269. newProject() {
  270. this.canvas.clear();
  271. this.dispatchEvent( { type: 'new' } );
  272. }
  273. loadJSON( json ) {
  274. this.canvas.clear();
  275. this.canvas.deserialize( json );
  276. for ( const node of this.canvas.nodes ) {
  277. this.add( node );
  278. }
  279. this.dispatchEvent( { type: 'load' } );
  280. }
  281. _initTips() {
  282. this.tips = new Tips();
  283. this.domElement.append( this.tips.dom );
  284. }
  285. _initMenu() {
  286. const menu = new CircleMenu();
  287. const menuButton = new ButtonInput().setIcon( 'ti ti-apps' ).setToolTip( 'Add' );
  288. const examplesButton = new ButtonInput().setIcon( 'ti ti-file-symlink' ).setToolTip( 'Examples' );
  289. const newButton = new ButtonInput().setIcon( 'ti ti-file' ).setToolTip( 'New' );
  290. const openButton = new ButtonInput().setIcon( 'ti ti-upload' ).setToolTip( 'Open' );
  291. const saveButton = new ButtonInput().setIcon( 'ti ti-download' ).setToolTip( 'Save' );
  292. menuButton.onClick( () => this.nodesContext.open() );
  293. examplesButton.onClick( () => this.examplesContext.open() );
  294. newButton.onClick( () => {
  295. if ( confirm( 'are you sure' ) ) this.newProject();
  296. } );
  297. openButton.onClick( () => {
  298. const input = document.createElement( 'input' );
  299. input.type = 'file';
  300. input.onchange = e => {
  301. const file = e.target.files[ 0 ];
  302. const reader = new FileReader();
  303. reader.readAsText( file, 'UTF-8' );
  304. reader.onload = readerEvent => {
  305. const loader = new Loader( Loader.OBJECTS );
  306. const json = loader.parse( JSON.parse( readerEvent.target.result ), ClassLib );
  307. this.loadJSON( json );
  308. };
  309. };
  310. input.click();
  311. } );
  312. saveButton.onClick( () => {
  313. const json = JSON.stringify( this.canvas.toJSON() );
  314. const a = document.createElement( 'a' );
  315. const file = new Blob( [ json ], { type: 'text/plain' } );
  316. a.href = URL.createObjectURL( file );
  317. a.download = 'node_editor.json';
  318. a.click();
  319. } );
  320. menu.add( examplesButton )
  321. .add( menuButton )
  322. .add( newButton )
  323. .add( openButton )
  324. .add( saveButton );
  325. this.domElement.append( menu.dom );
  326. this.menu = menu;
  327. }
  328. _initExamplesContext() {
  329. const context = new ContextMenu();
  330. //**************//
  331. // MAIN
  332. //**************//
  333. const onClickExample = async ( button ) => {
  334. this.examplesContext.hide();
  335. const filename = button.getExtra();
  336. const loader = new Loader( Loader.OBJECTS );
  337. const json = await loader.load( `./jsm/node-editor/examples/${filename}.json`, ClassLib );
  338. this.loadJSON( json );
  339. };
  340. const addExample = ( context, name, filename = null ) => {
  341. filename = filename || name.replaceAll( ' ', '-' ).toLowerCase();
  342. context.add( new ButtonInput( name )
  343. .setIcon( 'ti ti-file-symlink' )
  344. .onClick( onClickExample )
  345. .setExtra( filename )
  346. );
  347. };
  348. //**************//
  349. // EXAMPLES
  350. //**************//
  351. const basicContext = new ContextMenu();
  352. const advancedContext = new ContextMenu();
  353. addExample( basicContext, 'Animate UV' );
  354. addExample( basicContext, 'Fake top light' );
  355. addExample( basicContext, 'Oscillator color' );
  356. addExample( advancedContext, 'Rim' );
  357. //**************//
  358. // MAIN
  359. //**************//
  360. context.add( new ButtonInput( 'Basic' ), basicContext );
  361. context.add( new ButtonInput( 'Advanced' ), advancedContext );
  362. this.examplesContext = context;
  363. }
  364. _initSearch() {
  365. const traverseNodeEditors = ( item ) => {
  366. if ( item.nodeClass ) {
  367. const button = new ButtonInput( item.name );
  368. button.setIcon( `ti ti-${item.icon}` );
  369. button.addEventListener( 'complete', () => {
  370. const node = new item.nodeClass();
  371. this.add( node );
  372. this.centralizeNode( node );
  373. } );
  374. search.add( button );
  375. }
  376. if ( item.children ) {
  377. for ( const subItem of item.children ) {
  378. traverseNodeEditors( subItem );
  379. }
  380. }
  381. };
  382. const search = new Search();
  383. search.forceAutoComplete = true;
  384. search.onFilter( () => {
  385. search.clear();
  386. for ( const item of NodeList ) {
  387. traverseNodeEditors( item );
  388. }
  389. const object3d = this.scene;
  390. if ( object3d !== null ) {
  391. object3d.traverse( ( obj3d ) => {
  392. if ( obj3d.isMesh === true ) {
  393. const button = new ButtonInput( `Mesh - ${obj3d.name}` );
  394. button.setIcon( 'ti ti-3d-cube-sphere' );
  395. button.addEventListener( 'complete', () => {
  396. for ( const node of this.canvas.nodes ) {
  397. if ( node.value === obj3d ) {
  398. // not duplicated node
  399. this.canvas.select( node );
  400. return;
  401. }
  402. }
  403. const node = new MeshEditor( obj3d );
  404. this.add( node );
  405. this.centralizeNode( node );
  406. } );
  407. search.add( button );
  408. }
  409. } );
  410. }
  411. } );
  412. search.onSubmit( () => {
  413. if ( search.currentFiltered !== null ) {
  414. search.currentFiltered.button.dispatchEvent( new Event( 'complete' ) );
  415. }
  416. } );
  417. this.domElement.append( search.dom );
  418. }
  419. _initNodesContext() {
  420. const context = new ContextMenu( this.domElement );
  421. let isContext = false;
  422. const contextPosition = {};
  423. const add = ( node ) => {
  424. if ( isContext ) {
  425. node.setPosition(
  426. contextPosition.x,
  427. contextPosition.y
  428. );
  429. } else {
  430. this.centralizeNode( node );
  431. }
  432. context.hide();
  433. this.add( node );
  434. this.canvas.select( node );
  435. isContext = false;
  436. };
  437. context.onContext( () => {
  438. isContext = true;
  439. const { relativeClientX, relativeClientY } = this.canvas;
  440. contextPosition.x = relativeClientX;
  441. contextPosition.y = relativeClientY;
  442. } );
  443. //**************//
  444. // INPUTS
  445. //**************//
  446. const createButtonMenu = ( item ) => {
  447. const button = new ButtonInput( item.name );
  448. button.setIcon( `ti ti-${item.icon}` );
  449. let context = null;
  450. if ( item.nodeClass ) {
  451. button.onClick( () => add( new item.nodeClass() ) );
  452. }
  453. if ( item.tip ) {
  454. button.setToolTip( item.tip );
  455. }
  456. if ( item.children ) {
  457. context = new ContextMenu();
  458. for ( const subItem of item.children ) {
  459. const buttonMenu = createButtonMenu( subItem );
  460. context.add( buttonMenu.button, buttonMenu.context );
  461. }
  462. }
  463. return { button, context };
  464. };
  465. for ( const item of NodeList ) {
  466. const buttonMenu = createButtonMenu( item );
  467. context.add( buttonMenu.button, buttonMenu.context );
  468. }
  469. this.nodesContext = context;
  470. }
  471. }