NodeEditor.js 14 KB

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