TSLGraphEditor.js 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916
  1. import { Raycaster, Vector2, BoxHelper, error, warn } from 'three/webgpu';
  2. import { Extension } from 'three/addons/inspector/Extension.js';
  3. import { TSLGraphLoader } from './TSLGraphLoader.js';
  4. const HOST_SOURCE = 'tsl-graph-host';
  5. const EDITOR_SOURCE = 'tsl-graph-editor';
  6. const _resposeByCommand = {
  7. 'tsl:command:get-code': 'tsl:response:get-code',
  8. 'tsl:command:set-root-material': 'tsl:response:set-root-material',
  9. 'tsl:command:get-graph': 'tsl:response:get-graph',
  10. 'tsl:command:load': 'tsl:response:load',
  11. 'tsl:command:clear-graph': 'tsl:response:clear-graph'
  12. };
  13. const _refMaterials = new WeakMap();
  14. class TSLGraphEditor extends Extension {
  15. constructor( options = {} ) {
  16. super( 'TSL Graph', options );
  17. const editorUrl = new URL( 'https://www.tsl-graph.xyz/editor/standalone' );
  18. editorUrl.searchParams.set( 'graphs', 'material' );
  19. editorUrl.searchParams.set( 'targetOrigin', '*' );
  20. // UI Setup
  21. this.content.style.display = 'flex';
  22. this.content.style.flexDirection = 'column';
  23. this.content.style.position = 'relative';
  24. const headerDiv = document.createElement( 'div' );
  25. headerDiv.style.padding = '4px';
  26. headerDiv.style.backgroundColor = 'var(--profiler-header-bg, #2a2a33aa)';
  27. headerDiv.style.borderBottom = '1px solid var(--profiler-border, #4a4a5a)';
  28. headerDiv.style.display = 'flex';
  29. headerDiv.style.justifyContent = 'center';
  30. headerDiv.style.gap = '4px';
  31. headerDiv.style.position = 'relative';
  32. const importBtn = document.createElement( 'button' );
  33. importBtn.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="17 8 12 3 7 8"></polyline><line x1="12" y1="3" x2="12" y2="15"></line></svg>';
  34. importBtn.className = 'panel-action-btn';
  35. importBtn.title = 'Import';
  36. importBtn.style.padding = '5px 8px';
  37. importBtn.onclick = () => this._importData();
  38. const exportBtn = document.createElement( 'button' );
  39. exportBtn.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="7 10 12 15 17 10"></polyline><line x1="12" y1="15" x2="12" y2="3"></line></svg>';
  40. exportBtn.className = 'panel-action-btn';
  41. exportBtn.title = 'Export';
  42. exportBtn.style.padding = '5px 8px';
  43. exportBtn.onclick = () => this._exportData();
  44. const manageBtn = document.createElement( 'button' );
  45. manageBtn.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="14" width="7" height="7" rx="1"></rect><rect x="3" y="3" width="7" height="7" rx="1"></rect><path d="M14 4h7"></path><path d="M14 9h7"></path><path d="M14 15h7"></path><path d="M14 20h7"></path></svg>';
  46. manageBtn.className = 'panel-action-btn';
  47. manageBtn.title = 'Saved Materials';
  48. manageBtn.style.padding = '5px 8px';
  49. manageBtn.onclick = () => this._showManagerModal();
  50. const autoIdBtn = document.createElement( 'button' );
  51. autoIdBtn.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3c.132 5.466 2.534 7.868 8 8-5.466.132-7.868 2.534-8 8-.132-5.466-2.534-7.868-8-8 5.466-.132 7.868-2.534 8-8z"></path></svg>';
  52. autoIdBtn.className = 'panel-action-btn';
  53. autoIdBtn.title = 'Auto-Generate Graph ID';
  54. autoIdBtn.style.padding = '5px 8px';
  55. autoIdBtn.style.position = 'absolute';
  56. autoIdBtn.style.right = '4px';
  57. autoIdBtn.style.top = '4px';
  58. this.autoGraphId = false;
  59. autoIdBtn.onclick = () => {
  60. this.autoGraphId = ! this.autoGraphId;
  61. if ( this.autoGraphId ) {
  62. autoIdBtn.style.backgroundColor = 'rgba(255, 255, 255, 0.1)';
  63. autoIdBtn.style.color = '#fff';
  64. } else {
  65. autoIdBtn.style.backgroundColor = '';
  66. autoIdBtn.style.color = '';
  67. }
  68. };
  69. headerDiv.appendChild( importBtn );
  70. headerDiv.appendChild( exportBtn );
  71. headerDiv.appendChild( manageBtn );
  72. headerDiv.appendChild( autoIdBtn );
  73. this.content.appendChild( headerDiv );
  74. this.iframe = document.createElement( 'iframe' );
  75. this.iframe.style.width = '100%';
  76. this.iframe.style.height = '100%';
  77. this.iframe.style.border = 'none';
  78. this.iframe.src = editorUrl.toString();
  79. this.editorOrigin = new URL( this.iframe.src ).origin;
  80. this.content.appendChild( this.iframe );
  81. this.material = null;
  82. this.uniforms = null;
  83. this.isReady = false;
  84. this._codeData = null;
  85. this._codeSaveTimeout = null;
  86. this._pending = new Map();
  87. this._resolveReady = null;
  88. this._editorReady = new Promise( ( resolve ) => {
  89. this._resolveReady = resolve;
  90. } );
  91. window.addEventListener( 'message', this.onMessage.bind( this ) );
  92. }
  93. get hasGraphs() {
  94. return TSLGraphLoader.hasGraphs;
  95. }
  96. _initPicker( inspector ) {
  97. const renderer = inspector.getRenderer();
  98. let boundingBox = null;
  99. const raycaster = new Raycaster();
  100. const pointer = new Vector2();
  101. const removeBoundingBox = () => {
  102. if ( boundingBox ) {
  103. boundingBox.removeFromParent();
  104. boundingBox.dispose();
  105. boundingBox = null;
  106. }
  107. };
  108. this.addEventListener( 'change', ( { material } ) => {
  109. if ( material === null ) {
  110. removeBoundingBox();
  111. }
  112. } );
  113. this.addEventListener( 'remove', ( { graphId } ) => {
  114. const frame = inspector.getFrame();
  115. const scene = frame && frame.renders.length > 0 ? frame.renders[ 0 ].scene : null;
  116. if ( scene ) {
  117. scene.traverse( ( object ) => {
  118. if ( object.material && object.material.userData && object.material.userData.graphId === graphId ) {
  119. this.restoreMaterial( object.material );
  120. }
  121. } );
  122. }
  123. } );
  124. const pointerDownPosition = new Vector2();
  125. renderer.domElement.addEventListener( 'pointerdown', ( e ) => {
  126. pointerDownPosition.set( e.clientX, e.clientY );
  127. } );
  128. renderer.domElement.addEventListener( 'pointerup', ( e ) => {
  129. const frame = inspector.getFrame();
  130. for ( const render of frame.renders ) {
  131. const scene = render.scene;
  132. if ( scene.isScene !== true ) continue;
  133. const camera = render.camera;
  134. if ( pointerDownPosition.distanceTo( pointer.set( e.clientX, e.clientY ) ) > 2 ) return;
  135. const rect = renderer.domElement.getBoundingClientRect();
  136. pointer.x = ( ( e.clientX - rect.left ) / rect.width ) * 2 - 1;
  137. pointer.y = - ( ( e.clientY - rect.top ) / rect.height ) * 2 + 1;
  138. raycaster.setFromCamera( pointer, camera );
  139. const intersects = raycaster.intersectObjects( scene.children, true );
  140. let graphMaterial = null;
  141. if ( intersects.length > 0 ) {
  142. for ( const intersect of intersects ) {
  143. const object = intersect.object;
  144. const material = object.material;
  145. if ( material && material.isNodeMaterial ) {
  146. removeBoundingBox();
  147. boundingBox = new BoxHelper( object, 0xffff00 );
  148. scene.add( boundingBox );
  149. graphMaterial = material;
  150. }
  151. if ( object.isMesh || object.isSprite ) {
  152. break;
  153. }
  154. }
  155. }
  156. this.setMaterial( graphMaterial );
  157. }
  158. } );
  159. }
  160. apply( scene ) {
  161. const loader = new TSLGraphLoader();
  162. const applier = loader.parse( TSLGraphLoader.getCodes() );
  163. applier.apply( scene );
  164. return this;
  165. }
  166. restoreMaterial( material ) {
  167. material.copy( new material.constructor() );
  168. material.needsUpdate = true;
  169. }
  170. init( inspector ) {
  171. this._initPicker( inspector );
  172. }
  173. async setMaterial( material ) {
  174. if ( this.material === material ) return;
  175. await this._setMaterial( material );
  176. this.dispatchEvent( { type: 'change', material } );
  177. }
  178. async loadGraph( graphData ) {
  179. await this.command( 'load', { graphData } );
  180. }
  181. async command( type, payload ) {
  182. type = 'tsl:command:' + type;
  183. await this._editorReady;
  184. const requestId = this._makeRequestId();
  185. const expectedType = _resposeByCommand[ type ];
  186. return new Promise( ( resolve, reject ) => {
  187. const timer = window.setTimeout( () => {
  188. if ( ! this._pending.has( requestId ) ) return;
  189. this._pending.delete( requestId );
  190. reject( new Error( `Timeout for ${type}` ) );
  191. }, 5000 );
  192. this._pending.set( requestId, { expectedType, resolve, reject, timer } );
  193. const message = { source: HOST_SOURCE, type, requestId };
  194. if ( payload !== undefined ) message.payload = payload;
  195. this._post( message );
  196. } );
  197. }
  198. async getCode() {
  199. return this.command( 'get-code' );
  200. }
  201. async getTSLFunction() {
  202. const graphLoader = new TSLGraphLoader();
  203. const applier = graphLoader.parse( await this.getCode() );
  204. return applier.tslGraphFns[ 'tslGraph' ];
  205. }
  206. async getGraph() {
  207. return ( await this.command( 'get-graph' ) ).graphData;
  208. }
  209. async onResponse( /*type, payload*/ ) {
  210. }
  211. async onEvent( type, payload ) {
  212. if ( type === 'ready' ) {
  213. if ( ! this.isReady ) {
  214. this.isReady = true;
  215. this._resolveReady();
  216. }
  217. } else if ( type === 'graph-changed' ) {
  218. if ( this.material === null ) return;
  219. await this._updateMaterial();
  220. const graphData = await this.getGraph();
  221. const graphId = this.material.userData.graphId;
  222. TSLGraphLoader.setGraph( graphId, graphData );
  223. } else if ( type === 'uniforms-changed' ) {
  224. this._updateUniforms( payload.uniforms );
  225. }
  226. }
  227. async onMessage( event ) {
  228. if ( event.origin !== this.editorOrigin ) return;
  229. if ( ! this._isEditorMessage( event.data ) ) return;
  230. const msg = event.data;
  231. if ( msg.requestId && msg.type.startsWith( 'tsl:response:' ) ) {
  232. const waiter = this._pending.get( msg.requestId );
  233. if ( ! waiter ) return;
  234. if ( msg.type !== waiter.expectedType ) return;
  235. this._pending.delete( msg.requestId );
  236. window.clearTimeout( waiter.timer );
  237. if ( msg.error ) waiter.reject( new Error( msg.error ) );
  238. else waiter.resolve( msg.payload );
  239. this.onResponse( msg.type.substring( 'tsl:response:'.length ), msg.payload );
  240. } else if ( msg.type.startsWith( 'tsl:event:' ) ) {
  241. this.onEvent( msg.type.substring( 'tsl:event:'.length ), msg.payload );
  242. }
  243. }
  244. async _setMaterial( material ) {
  245. if ( ! material ) {
  246. this.material = null;
  247. this.materialDefault = null;
  248. this.uniforms = null;
  249. await this.command( 'clear-graph' );
  250. return;
  251. }
  252. if ( material.isNodeMaterial !== true ) {
  253. error( 'TSLGraphEditor: "Material" needs be a "NodeMaterial".' );
  254. return;
  255. }
  256. if ( material.userData.graphId === undefined ) {
  257. if ( this.autoGraphId ) {
  258. material.userData.graphId = material.name || 'id:' + material.id;
  259. } else {
  260. warn( 'TSLGraphEditor: "NodeMaterial" has no graphId. Set a "graphId" for the material in "material.userData.graphId".' );
  261. return;
  262. }
  263. }
  264. let materialDefault = _refMaterials.get( material );
  265. if ( materialDefault === undefined ) {
  266. //materialDefault = material.clone();
  267. materialDefault = new material.constructor();
  268. materialDefault.userData = material.userData;
  269. _refMaterials.set( material, materialDefault );
  270. }
  271. this.material = material;
  272. this.materialDefault = materialDefault;
  273. this.uniforms = null;
  274. const graphData = TSLGraphLoader.getGraph( this.material.userData.graphId );
  275. if ( graphData ) {
  276. await this.loadGraph( graphData );
  277. } else {
  278. await this.command( 'clear-graph' );
  279. await this.command( 'set-root-material', { materialType: this._getGraphType( this.material ) } );
  280. }
  281. }
  282. _getGraphType( material ) {
  283. if ( material.isMeshPhysicalNodeMaterial ) return 'material/physical';
  284. if ( material.isMeshStandardNodeMaterial ) return 'material/standard';
  285. if ( material.isMeshPhongNodeMaterial ) return 'material/phong';
  286. if ( material.isMeshBasicNodeMaterial ) return 'material/basic';
  287. if ( material.isSpriteNodeMaterial ) return 'material/sprite';
  288. return 'material/node';
  289. }
  290. _showManagerModal() {
  291. const overlay = document.createElement( 'div' );
  292. overlay.style.position = 'absolute';
  293. overlay.style.top = '0';
  294. overlay.style.left = '0';
  295. overlay.style.width = '100%';
  296. overlay.style.height = '100%';
  297. overlay.style.backgroundColor = 'rgba(0, 0, 0, 0.7)';
  298. overlay.style.zIndex = '100';
  299. overlay.style.display = 'flex';
  300. overlay.style.justifyContent = 'center';
  301. overlay.style.alignItems = 'center';
  302. overlay.onclick = ( e ) => {
  303. if ( e.target === overlay ) {
  304. this.content.removeChild( overlay );
  305. }
  306. };
  307. const modal = document.createElement( 'div' );
  308. modal.style.width = '80%';
  309. modal.style.maxWidth = '500px';
  310. modal.style.height = '400px';
  311. modal.style.backgroundColor = 'var(--profiler-bg, #1e1e24f5)';
  312. modal.style.border = '1px solid var(--profiler-border, #4a4a5a)';
  313. modal.style.borderRadius = '8px';
  314. modal.style.display = 'flex';
  315. modal.style.flexDirection = 'column';
  316. const header = document.createElement( 'div' );
  317. header.style.padding = '15px';
  318. header.style.borderBottom = '1px solid var(--profiler-border, #4a4a5a)';
  319. header.style.display = 'flex';
  320. header.style.justifyContent = 'space-between';
  321. header.style.alignItems = 'center';
  322. header.style.gap = '15px';
  323. const filterInput = document.createElement( 'input' );
  324. filterInput.type = 'text';
  325. filterInput.className = 'console-filter-input';
  326. filterInput.placeholder = 'Filter...';
  327. filterInput.style.flex = '1';
  328. const closeBtn = document.createElement( 'button' );
  329. closeBtn.innerHTML = '&#x2715;';
  330. closeBtn.style.background = 'transparent';
  331. closeBtn.style.border = 'none';
  332. closeBtn.style.color = 'var(--text-secondary, #9a9aab)';
  333. closeBtn.style.cursor = 'pointer';
  334. closeBtn.style.fontSize = '16px';
  335. closeBtn.onmouseover = () => closeBtn.style.color = 'var(--text-primary, #e0e0e0)';
  336. closeBtn.onmouseout = () => closeBtn.style.color = 'var(--text-secondary, #9a9aab)';
  337. closeBtn.onclick = () => this.content.removeChild( overlay );
  338. header.appendChild( filterInput );
  339. header.appendChild( closeBtn );
  340. const codes = this.getCodes();
  341. const materialIds = Object.keys( codes.materials || {} );
  342. if ( materialIds.length === 0 ) {
  343. const listContainer = document.createElement( 'div' );
  344. listContainer.style.padding = '10px';
  345. listContainer.style.flex = '1';
  346. const emptyMsg = document.createElement( 'div' );
  347. emptyMsg.textContent = 'No saved materials found.';
  348. emptyMsg.style.color = 'var(--text-secondary, #9a9aab)';
  349. emptyMsg.style.padding = '10px';
  350. emptyMsg.style.textAlign = 'center';
  351. emptyMsg.style.fontFamily = 'var(--font-family, sans-serif)';
  352. emptyMsg.style.fontSize = '12px';
  353. listContainer.appendChild( emptyMsg );
  354. modal.appendChild( header );
  355. modal.appendChild( listContainer );
  356. } else {
  357. const listHeaderContainer = document.createElement( 'div' );
  358. listHeaderContainer.style.display = 'grid';
  359. listHeaderContainer.style.gridTemplateColumns = '1fr 80px';
  360. listHeaderContainer.style.gap = '10px';
  361. listHeaderContainer.style.padding = '10px 15px 8px 15px';
  362. listHeaderContainer.style.borderBottom = '1px solid var(--profiler-border, #4a4a5a)';
  363. listHeaderContainer.style.backgroundColor = 'var(--profiler-bg, #1e1e24f5)';
  364. listHeaderContainer.style.fontFamily = 'var(--font-family, sans-serif)';
  365. listHeaderContainer.style.fontSize = '11px';
  366. listHeaderContainer.style.fontWeight = 'bold';
  367. listHeaderContainer.style.textTransform = 'uppercase';
  368. listHeaderContainer.style.letterSpacing = '0.5px';
  369. listHeaderContainer.style.color = 'var(--text-secondary, #9a9aab)';
  370. listHeaderContainer.style.boxShadow = '0 2px 4px rgba(0,0,0,0.1)';
  371. listHeaderContainer.style.zIndex = '1';
  372. const col1 = document.createElement( 'div' );
  373. col1.textContent = 'Material Name / ID';
  374. const col2 = document.createElement( 'div' );
  375. col2.textContent = 'Action';
  376. col2.style.textAlign = 'right';
  377. listHeaderContainer.appendChild( col1 );
  378. listHeaderContainer.appendChild( col2 );
  379. const scrollWrapper = document.createElement( 'div' );
  380. scrollWrapper.style.flex = '1';
  381. scrollWrapper.style.overflowY = 'auto';
  382. scrollWrapper.style.padding = '0';
  383. const rows = [];
  384. for ( const id of materialIds ) {
  385. const itemRow = document.createElement( 'div' );
  386. itemRow.style.display = 'grid';
  387. itemRow.style.gridTemplateColumns = '1fr 80px';
  388. itemRow.style.gap = '10px';
  389. itemRow.style.alignItems = 'center';
  390. itemRow.style.padding = '8px 15px';
  391. itemRow.style.borderBottom = '1px solid rgba(74, 74, 90, 0.4)';
  392. itemRow.onmouseover = () => itemRow.style.backgroundColor = 'rgba(255, 255, 255, 0.04)';
  393. itemRow.onmouseout = () => itemRow.style.backgroundColor = 'transparent';
  394. const nameSpan = document.createElement( 'span' );
  395. const materialData = codes.materials[ id ];
  396. const materialName = materialData.name || id;
  397. nameSpan.textContent = materialName;
  398. nameSpan.style.fontFamily = 'var(--font-mono, monospace)';
  399. nameSpan.style.fontSize = '12px';
  400. nameSpan.style.color = 'var(--text-primary, #e0e0e0)';
  401. nameSpan.style.userSelect = 'all';
  402. nameSpan.style.overflow = 'hidden';
  403. nameSpan.style.textOverflow = 'ellipsis';
  404. nameSpan.style.whiteSpace = 'nowrap';
  405. const actionContainer = document.createElement( 'div' );
  406. actionContainer.style.textAlign = 'right';
  407. const removeBtn = document.createElement( 'button' );
  408. removeBtn.textContent = 'Remove';
  409. removeBtn.style.background = 'rgba(244, 67, 54, 0.1)';
  410. removeBtn.style.border = '1px solid var(--color-red, #f44336)';
  411. removeBtn.style.color = 'var(--color-red, #f44336)';
  412. removeBtn.style.borderRadius = '4px';
  413. removeBtn.style.padding = '4px 8px';
  414. removeBtn.style.cursor = 'pointer';
  415. removeBtn.style.fontSize = '11px';
  416. removeBtn.onmouseover = () => removeBtn.style.background = 'rgba(244, 67, 54, 0.2)';
  417. removeBtn.onmouseout = () => removeBtn.style.background = 'rgba(244, 67, 54, 0.1)';
  418. actionContainer.appendChild( removeBtn );
  419. itemRow.appendChild( nameSpan );
  420. itemRow.appendChild( actionContainer );
  421. scrollWrapper.appendChild( itemRow );
  422. rows.push( { element: itemRow, text: materialName.toLowerCase() } );
  423. removeBtn.onclick = async () => {
  424. delete codes.materials[ id ];
  425. TSLGraphLoader.setCodes( codes );
  426. TSLGraphLoader.deleteGraph( id );
  427. scrollWrapper.removeChild( itemRow );
  428. const index = rows.findIndex( r => r.element === itemRow );
  429. if ( index > - 1 ) rows.splice( index, 1 );
  430. if ( rows.length === 0 ) {
  431. modal.removeChild( listHeaderContainer );
  432. modal.removeChild( scrollWrapper );
  433. const listContainer = document.createElement( 'div' );
  434. listContainer.style.padding = '10px';
  435. listContainer.style.flex = '1';
  436. const emptyMsg = document.createElement( 'div' );
  437. emptyMsg.textContent = 'No saved materials found.';
  438. emptyMsg.style.color = 'var(--text-secondary, #9a9aab)';
  439. emptyMsg.style.padding = '10px';
  440. emptyMsg.style.textAlign = 'center';
  441. emptyMsg.style.fontFamily = 'var(--font-family, sans-serif)';
  442. emptyMsg.style.fontSize = '12px';
  443. listContainer.appendChild( emptyMsg );
  444. modal.appendChild( listContainer );
  445. }
  446. _refMaterials.delete( this.material );
  447. if ( this.material && this.material.userData.graphId === id ) {
  448. this.restoreMaterial( this.material );
  449. await this.setMaterial( null );
  450. }
  451. this.dispatchEvent( { type: 'remove', graphId: id } );
  452. };
  453. }
  454. filterInput.addEventListener( 'input', ( e ) => {
  455. const term = e.target.value.toLowerCase();
  456. for ( const row of rows ) {
  457. row.element.style.display = row.text.includes( term ) ? 'grid' : 'none';
  458. }
  459. } );
  460. modal.appendChild( header );
  461. modal.appendChild( listHeaderContainer );
  462. modal.appendChild( scrollWrapper );
  463. }
  464. overlay.appendChild( modal );
  465. this.content.appendChild( overlay );
  466. }
  467. _exportData() {
  468. const codes = this.getCodes();
  469. const materialIds = Object.keys( codes.materials || {} );
  470. const exportPayload = {
  471. codes: codes,
  472. graphs: {}
  473. };
  474. for ( const id of materialIds ) {
  475. const graphData = TSLGraphLoader.getGraph( id );
  476. if ( graphData ) {
  477. exportPayload.graphs[ id ] = graphData;
  478. }
  479. }
  480. const dataStr = 'data:text/json;charset=utf-8,' + encodeURIComponent( JSON.stringify( exportPayload, null, '\t' ) );
  481. const downloadAnchorNode = document.createElement( 'a' );
  482. downloadAnchorNode.setAttribute( 'href', dataStr );
  483. downloadAnchorNode.setAttribute( 'download', 'tsl-graphs.json' );
  484. document.body.appendChild( downloadAnchorNode );
  485. downloadAnchorNode.click();
  486. downloadAnchorNode.remove();
  487. }
  488. _importData() {
  489. const fileInput = document.createElement( 'input' );
  490. fileInput.type = 'file';
  491. fileInput.accept = '.json';
  492. fileInput.onchange = e => {
  493. const file = e.target.files[ 0 ];
  494. if ( ! file ) return;
  495. const reader = new FileReader();
  496. reader.onload = async ( event ) => {
  497. try {
  498. const importedData = TSLGraphLoader.setGraphs( JSON.parse( event.target.result ) );
  499. this._codeData = importedData.codes;
  500. // Reload visual state if we have a material open
  501. if ( this.material ) {
  502. // refresh material
  503. await this._setMaterial( this.material );
  504. }
  505. } catch ( err ) {
  506. error( 'TSLGraphEditor: Failed to parse or load imported JSON.', err );
  507. }
  508. };
  509. reader.readAsText( file );
  510. };
  511. fileInput.click();
  512. }
  513. getCodes() {
  514. if ( this._codeData === null ) {
  515. this._codeData = TSLGraphLoader.getCodes();
  516. }
  517. return this._codeData;
  518. }
  519. _saveCode() {
  520. const graphId = this.material.userData.graphId;
  521. clearTimeout( this._codeSaveTimeout );
  522. this._codeSaveTimeout = setTimeout( async () => {
  523. if ( this.material === null || graphId !== this.material.userData.graphId ) return;
  524. const codes = this.getCodes();
  525. const codeData = await this.getCode();
  526. codes.materials[ graphId ] = codeData.material;
  527. TSLGraphLoader.setCodes( codes );
  528. }, 1000 );
  529. }
  530. _restoreMaterial() {
  531. this.material.copy( this.materialDefault );
  532. }
  533. async _updateMaterial() {
  534. this._restoreMaterial();
  535. const applyNodes = await this.getTSLFunction();
  536. const { uniforms } = applyNodes( this.material );
  537. this.uniforms = uniforms;
  538. this.material.needsUpdate = true;
  539. this._saveCode();
  540. }
  541. _updateUniforms( uniforms ) {
  542. if ( this.uniforms === null ) return;
  543. for ( const uniform of uniforms ) {
  544. const uniformNode = this.uniforms[ uniform.name ];
  545. const uniformType = uniform.uniformType;
  546. const value = uniform.value;
  547. if ( uniformType.startsWith( 'vec' ) ) {
  548. uniformNode.value.fromArray( value );
  549. } else if ( uniformType.startsWith( 'color' ) ) {
  550. uniformNode.value.setHex( parseInt( value.slice( 1 ), 16 ) );
  551. } else {
  552. uniformNode.value = value;
  553. }
  554. }
  555. this._saveCode();
  556. }
  557. _isEditorMessage( value ) {
  558. if ( ! value || typeof value !== 'object' ) return false;
  559. return value.source === EDITOR_SOURCE && typeof value.type === 'string';
  560. }
  561. _makeRequestId() {
  562. return `${Date.now()}-${Math.random().toString( 36 ).slice( 2, 10 )}`;
  563. }
  564. _post( message ) {
  565. if ( this.iframe.contentWindow ) {
  566. this.iframe.contentWindow.postMessage( message, this.editorOrigin );
  567. }
  568. }
  569. }
  570. export default TSLGraphEditor;
粤ICP备19079148号