NodeEditor.js 14 KB

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