NodeEditor.js 14 KB

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