NodeEditor.js 12 KB

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