Function to initialize the MaterialX node editor.
138 {
139 let my_icon_map = {
140 "_default_": "./Icons/materialx_logo.webp",
141 "_default_graph_": "./Icons/nodegraph_white.svg"
142 };
143
144 let geometryValues = ['teapot', 'shaderball', 'sphere', 'plane', 'cube', 'cylinder', 'twist', '_loadFromFile_']
145
146 if (user_icon_map) {
147
148 for (var key in user_icon_map) {
149 my_icon_map[key] = user_icon_map[key];
150 }
151 }
152
153
154 function uriExists(uri) {
155 return fetch(uri)
156 .then(response => {
157 if (response.ok) {
158 return Promise.resolve(true);
159 } else {
160 return Promise.resolve(false);
161 }
162 })
163 .catch(error => {
164 console.log('Error checking URI:', error);
165 return Promise.resolve(false);
166 });
167 }
168
169
170 function renderableItemUpdater(renderableItems) {
171 let renderableItemSelect = document.getElementById('renderableItem');
172 if (renderableItemSelect) {
173
174 const TRUNCATION_LENGTH = 12;
175
176 while (renderableItemSelect.firstChild) {
177 renderableItemSelect.removeChild(renderableItemSelect.firstChild);
178 }
179 for (let i = 0; i < renderableItems.length; i++) {
180 let item = renderableItems[i];
181 let option = document.createElement('option');
182 option.value = item;
183 let uiItem = item;
184
185 if (uiItem.length > 10)
186 uiItem = uiItem.substring(0, 10) + '...';
187 option.text = uiItem;
188 renderableItemSelect.appendChild(option);
189 }
190 }
191 }
192
193
194
195 function consoleLog(text, severity, clear = null) {
196 if (severity === 2) {
197 text = '> Error: ' + text
198 }
199 else if (severity === 1) {
200 text = '> Warning: ' + text
201 }
202 else {
203 if (text.length)
204 text = '> ' + text;
205 }
206
207 let console_area = document.getElementById('console_area');
208 if (console_area) {
209 if (clear) {
210 console_area.value = text + '\n';
211 }
212 else {
213 console_area.value = console_area.value + text + '\n';
214 }
215
216 console_area.scrollTop = console_area.scrollHeight;
217 }
218 else {
219 console.log(text);
220 }
221 }
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
244
245 function createMenuItem(text, filename) {
246 let menuItem = document.createElement('li');
247 menuItem.className = 'dropdown-item';
248 menuItem.innerText = text;
249 menuItem.onclick = function () {
250 console.log('Load library file:', filename);
252
253 let dropdownMenu = document.getElementById('libraryDropdown');
254 dropdownMenu.classList.remove('show');
255 };
256 return menuItem;
257 }
258
264
265 function createSubMenu(title, auto_close = false) {
266
267 let li = document.createElement('li');
268 li.className = "dropend dropdown";
269 li.id = key;
270
271 let subMenu = document.createElement('a');
272 subMenu.className = "dropdown-item dropdown-toggle";
273 subMenu.setAttribute('data-bs-toggle', "dropdown");
274 if (auto_close) {
275 subMenu.setAttribute('data-bs-auto-close', 'outside');
276 }
277 subMenu.setAttribute('aria-expanded', 'false');
278 subMenu.setAttribute('aria-haspopup', 'true');
279 subMenu.innerHTML = title;
280 li.appendChild(subMenu);
281
282 return li;
283 }
284
290
291 function createLibraryMenu(sampleFiles, libraryDropdown) {
292 for (let key in sampleFiles) {
293
294 let li = createSubMenu(key, true);
295 libraryDropdown.appendChild(li);
296
297 let value = sampleFiles[key];
298
299
300 if (typeof value === 'string') {
301 let value = sampleFiles[key];
302 li.appendChild(createMenuItem(key, value));
303 }
304 else if (typeof value === 'object') {
305
306 let subMenuList = document.createElement('ul');
307 subMenuList.className = 'dropdown-menu';
308 subMenuList.id = key;
309
310 for (key in value) {
311
312
313 if (typeof value[key] === 'string') {
314 subMenuList.appendChild(createMenuItem(key, value[key]));
315 }
316
317 else if (typeof value[key] === 'object') {
318
319
320 let sli = createSubMenu(key, false);
321 subMenuList.appendChild(sli);
322
323 let ssubMenuList = document.createElement('ul');
324 ssubMenuList.className = 'dropdown-menu';
325 for (let skey in value[key]) {
326 ssubMenuList.appendChild(createMenuItem(skey, (value[key])[skey]));
327 }
328 sli.appendChild(ssubMenuList);
329 }
330 }
331
332 li.appendChild(subMenuList);
333 }
334
335 }
336 }
337
338
339 if (sampleFiles && libraryDropdown) {
340 createLibraryMenu(sampleFiles, libraryDropdown);
341 }
342
343
344 let selectGeometryUI = false;
345 if (customRenderer) {
346
347 let geometryURL = geometryId;
348 if (geometryId.length > 0 && geometryValues.includes(geometryId)) {
349 geometryURL = 'Geometry/' + geometryId + '.glb';
350 selectGeometryUI = true;
351 }
352 var viewer = customRenderer.initialize(materialFilename, geometryURL, readOnly);
353 console.log('Setup renderer:', viewer);
354 }
355 else {
356 let preview_panel = document.getElementById("preview_panel");
357
358 if (preview_panel)
359 preview_panel.style.display = 'none';
360 }
361
362
367
368 function displayNodeTypes(nodeTypes) {
369
370 var nodeList = document.getElementById('nodeTypesList');
371 if (!nodeList) {
372 return;
373 }
374
375
376 while (nodeList.firstChild) {
377 nodeList.removeChild(nodeList.firstChild);
378 }
379
380
381 for (var typeName in nodeTypes) {
382
383 var rowItem = document.createElement("tr");
384
385 var cellItem = document.createElement("td");
386 cellItem.textContent = typeName;
387 rowItem.appendChild(cellItem);
388
389 cellItem = document.createElement("td");
390 var nodeDefString = '<None>';
391 var nodeDefName = nodeTypes[typeName].nodedef_name;
392 var nodeDefNode = nodeTypes[typeName].nodedef_node
393 var nodeDefHref = nodeTypes[typeName].nodedef_href;
394 if (nodeDefName) {
395 if (nodeDefNode) {
396 var link = document.createElement("a");
397 link.target = "_blank";
398 link.href = nodeDefHref;
399 link.textContent = nodeDefNode + " ( " + nodeDefName + " )";
400 cellItem.appendChild(link);
401 }
402 else {
403 cellItem.textContent = nodeDefName;
404 }
405 }
406 else {
407 cellItem.textContent = nodeDefString;
408 }
409 rowItem.appendChild(cellItem);
410
411 nodeList.appendChild(rowItem);
412 }
413 }
414
415
416 var cmeditor = setupXMLSyntax();
417 var cmeditor2 = setupJavascriptSyntax();
418 var cmeditor3 = setupGLTFSyntax();
419
424
425 function docDisplayUpdater(contents) {
426 if (cmeditor)
427 cmeditor.setValue(contents);
428 }
429
434
435 function gltfDisplayUpdater(contents) {
436 if (!contents || contents.length == 0) {
437 contents = '{}';
438 }
439 if (cmeditor3)
440 cmeditor3.setValue(contents);
441 else
442 console.log(contents);
443 }
444
449
450 function jsDefinitionsDisplayUpdater(contents) {
451 if (cmeditor2)
452 cmeditor2.setValue(contents);
453 }
454
455
456 var canvas = document.getElementById('mygraphcanvas');
457 var ui = {
458 consoleLogger: consoleLog,
459 nodeTypesListUpdater: displayNodeTypes,
460 renderableItemUpdater: renderableItemUpdater,
461 documentDisplayUpdater: docDisplayUpdater,
462 gltfDocumentDisplayUpdater: gltfDisplayUpdater,
463 definitionsDisplayUpdater: jsDefinitionsDisplayUpdater,
464 propertypanel_content: document.getElementById('propertypanel_content'),
465 propertypanel_icon: document.getElementById('propertypanel_icon'),
466 icon_map: my_icon_map,
467 };
469
471 monitor.setRenderer(customRenderer);
472 editor.initialize(canvas, ui, monitor, materialFilename, readOnly);
473
477
478 function addUIHandlers() {
479
480 var saveCanvasButton = document.getElementById('captureGraph');
481 if (saveCanvasButton) {
482 saveCanvasButton.addEventListener('click', function () {
483 var canvas = document.getElementById('mygraphcanvas');
484 var dataURL = canvas.toDataURL('image/png');
485 var link = document.createElement('a');
486 link.href = dataURL;
487 link.download = 'graph_capture.png';
488 link.click();
489 });
490 }
491
492
493 var auto_arrange_size = 80;
494
495
496 var loadMaterialXDocumentFromFile = document.getElementById('loadMaterialXDocumentFromFile');
497 if (loadMaterialXDocumentFromFile) {
498 loadMaterialXDocumentFromFile.addEventListener('click', function () {
499 editor.loadGraphFromFile('mtlx', auto_arrange_size);
501 });
502 }
503
504
505 var texAreaNumber = 0;
506 var loadMaterialXDocumentFromText = document.getElementById('loadMaterialXDocumentFromText');
507 if (loadMaterialXDocumentFromText) {
508 loadMaterialXDocumentFromText.addEventListener('click', function () {
509 var mtlxdoc = document.getElementById('mtlxdoc').value;
510
511 if (mtlxdoc.length > 0) {
512 var name = 'MaterialXGraph' + texAreaNumber++;
513 editor.loadGraphFromString('mtlx', mtlxdoc, name, auto_arrange_size);
515 }
516 });
517 }
518
519
520 var loadMaterialXDefinitions = document.getElementById('loadMaterialXDefinitions');
521 if (loadMaterialXDefinitions) {
522 loadMaterialXDefinitions.addEventListener('click', function () {
523 editor.loadDefinitionsFromFile('mtlx');
524 });
525 }
526
527
528 var clearGraphButton = document.getElementById('clearGraph');
529 if (clearGraphButton) {
530 clearGraphButton.addEventListener('click', function () {
531 editor.clearGraph();
533 });
534 }
535
536
537 var saveMaterialXGraph = document.getElementById('saveMaterialXGraph');
538 if (saveMaterialXGraph) {
539 saveMaterialXGraph.addEventListener('click', function () {
540 var sl = document.getElementById('writeCustomLibs').checked;
541 var sp = document.getElementById('saveNodePositions').checked;
542 var wo = true;
543 var graphWriteOptions = { writeCustomLibs: sl, saveNodePositions: sp, writeOutputs: wo };
544 editor.saveGraphToFile('mtlx', graphWriteOptions);
545 });
546 }
547
548
549 var saveMaterialXGraphText = document.getElementById('saveMaterialXGraphText');
550 if (saveMaterialXGraphText) {
551 saveMaterialXGraphText.addEventListener('click', function () {
552 saveToStringUI();
553 });
554 }
555
556
557 var openSubgraph = document.getElementById('openSubgraph');
558 if (openSubgraph) {
559 openSubgraph.addEventListener('click', function () {
560 editor.openSubgraph();
561 });
562 }
563
564
565 var closeSubgraph = document.getElementById('closeSubgraph');
566 if (closeSubgraph) {
567 closeSubgraph.addEventListener('click', function () {
568 editor.closeSubgraph();
569 });
570 }
571
572
573 var resetView = document.getElementById('resetView');
574 if (resetView) {
575 resetView.addEventListener('click', function () {
576 editor.resetView();
577 });
578 }
579
580
581 var arrangeGraphButton = document.getElementById('arrangeGraph');
582 if (arrangeGraphButton) {
583 arrangeGraphButton.addEventListener('click', function () {
584 editor.arrangeGraph();
585 });
586 }
587
588
589 var centerNodeButton = document.getElementById('centerNode');
590 if (centerNodeButton) {
591 centerNodeButton.addEventListener('click', function () {
592 editor.centerNode();
593 });
594 }
595
596
597 var collapseNodesButton = document.getElementById('collapseNodes');
598 if (collapseNodesButton) {
599 collapseNodesButton.addEventListener('click', function () {
600 editor.collapseExpandNodes(true);
601 });
602 }
603 var expandNodesButton = document.getElementById('expandNodes');
604 if (expandNodesButton) {
605 expandNodesButton.addEventListener('click', function () {
606 editor.collapseExpandNodes(false);
607 });
608 }
609
610
611 var copySelectedButton = document.getElementById('copySelected');
612 if (copySelectedButton) {
613 copySelectedButton.addEventListener('click', function () {
614 editor.copyToClipboard();
615 });
616 }
617
618
619 var pasteSelectedButton = document.getElementById('pasteSelected');
620 if (pasteSelectedButton) {
621 pasteSelectedButton.addEventListener('click', function () {
622 editor.pasteFromClipboard();
623 });
624 }
625
626
627 var createNodeGraphButton = document.getElementById('createNodeGraph');
628 if (createNodeGraphButton) {
629 createNodeGraphButton.addEventListener('click', function () {
630 editor.createNodeGraph();
631 });
632 }
633
634
635 var extractNodeGraphButton = document.getElementById('extractNodeGraph');
636 if (extractNodeGraphButton) {
637 extractNodeGraphButton.addEventListener('click', function () {
638 editor.extractNodeGraph();
639 });
640 }
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656 var xmlToGraph = document.getElementById('xmltograph');
657 if (xmlToGraph) {
658 xmlToGraph.addEventListener('click', function () {
659 var name = 'MaterialXGraph' + texAreaNumber++;
660 var mtlxdoc = document.getElementById('mtlxdoc').value;
661 editor.loadGraphFromString('mtlx', mtlxdoc, 'MaterialXGraph', auto_arrange_size);
663 });
664 }
665
666 function updateRenderableItemUI() {
667 let renderableItems = editor.findRenderableItems();
668 renderableItemUpdater(renderableItems);
669 }
670
671 function saveToStringUI() {
672 var cl = document.getElementById('writeCustomLibs').checked;
673 var sp = document.getElementById('saveNodePositions').checked;
674 var wo = true;
675 var graphWriteOptions = { writeCustomLibs: cl, saveNodePositions: sp, writeOutputs: wo };
676 console.log('Save with options: ', graphWriteOptions);
677 var result = editor.saveGraphToString('mtlx', graphWriteOptions);
678
679 cmeditor.setValue(result[0]);
680
681 if (customRenderer) {
682 customRenderer.setSourceColorSpace(editor.getSourceColorSpace());
683 customRenderer.setTargetDistanceUnit(editor.getTargetDistanceUnit());
684 customRenderer.updateMaterialFromText(result[0]);
685 updateRenderableItemUI();
686 }
687 }
688
689
690 var graphtoxml = document.getElementById('graphtoxml');
691 if (graphtoxml) {
692 graphtoxml.addEventListener('click', function () {
693 saveToStringUI();
694 });
695 }
696
697 let graphtoxml2 = document.getElementById('graphtoxml2');
698 if (graphtoxml2) {
699 graphtoxml2.addEventListener('click', function () {
700 saveToStringUI();
701 graphtoxml2.classList.remove('btn-outline-warning');
702 graphtoxml2.classList.add('btn-outline-secondary');
703 });
704 }
705
706
707 var graphtogltf = document.getElementById('graphtogltf');
708 if (graphtogltf) {
709 graphtogltf.addEventListener('click', function () {
710 var graphWriteOptions = { writeCustomLibs: false, saveNodePositions: false, writeOutputs: true };
711 var result = editor.saveGraphToString('gltf', graphWriteOptions);
712 gltfDisplayUpdater(result[0]);
713 if (result[1]) {
714 consoleLog(result[1], 1, false);
715 }
716 });
717 }
718
719
720 var gltftograph = document.getElementById('gltftograph');
721 if (gltftograph) {
722 gltftograph.addEventListener('click', function () {
723 var gltfdoc = document.getElementById('gltfgraph').value;
724 if (gltfdoc.length > 0) {
725 editor.loadGraphFromString('gltf', gltfdoc, 'GLTFGraph', auto_arrange_size);
727 }
728 });
729 }
730
731
732 let turntableEnabledUI = document.getElementById('turntableEnabled');
733 if (turntableEnabledUI) {
734 turntableEnabledUI.addEventListener('click', (e) => {
735
736 turntableEnabledUI.classList.toggle('btn-secondary');
737 if (customRenderer)
738 customRenderer.toggleTurntable();
739 });
740 }
741
742
743 let disableRenderingUI = document.getElementById('disableRendering');
744 if (disableRenderingUI) {
745 disableRenderingUI.addEventListener('click', (e) => {
746
747 disableRenderingUI.classList.toggle('btn-danger');
748 if (customRenderer)
749 customRenderer.toggleRendering();
750 });
751 }
752
753
754 let toggleBackgroundTextureUI = document.getElementById('toggleBackgroundTexture');
755 if (toggleBackgroundTextureUI) {
756 toggleBackgroundTextureUI.addEventListener('click', (e) => {
757 toggleBackgroundTextureUI.classList.toggle('btn-primary');
758 if (customRenderer)
759 customRenderer.toggleBackgroundTexture();
760 });
761 }
762
763
764 let resetCameraUI = document.getElementById('resetCamera');
765 if (resetCameraUI) {
766 resetCameraUI.addEventListener('click', (e) => {
767 if (customRenderer) {
768 customRenderer.resetCamera();
769 }
770 });
771 }
772
773
774 function loadFromMenu(e) {
775 var uiItem = e.target.value;
776 if (uiItem == '_loadFromFile_') {
777
778 var fileInput = document.createElement('input');
779 fileInput.type = 'file';
780 fileInput.accept = '.glb';
781
782 fileInput.onchange = function(event) {
783 var file = event.target.files[0];
784 if (file) {
785 var fileURL = URL.createObjectURL(file);
786 if (customRenderer)
787 customRenderer.setRenderGeometry(fileURL);
788 console.log('Change geometry to:', fileURL, 'from file:', file.name);
789 }
790 }
791 fileInput.click();
792 }
793 else {
794
795 var geometryURL = uiItem.toLowerCase().replace(/\s/g, '');
796 var geometryPath = 'Geometry/' + geometryURL + '.glb';
797 console.log('Change geometry to:', geometryPath);
798 if (customRenderer)
799 customRenderer.setRenderGeometry(geometryPath);
800 }
801 }
802
803
804 let geometryItemSelect = document.getElementById('loadGeometry');
805 if (geometryItemSelect) {
806
807
808 var geometryItems = ['Teapot', 'Shader Ball', 'Sphere', 'Plane', 'Cube', 'Cylinder', 'Twist', 'Custom...'];
809 for (var i = 0; i < geometryItems.length; i++) {
810 var option = document.createElement('option');
811 option.value = geometryValues[i];
812 option.text = geometryItems[i];
813 geometryItemSelect.appendChild(option);
814 }
815
816
817 geometryItemSelect.addEventListener('change', (e) => {
818 loadFromMenu(e);
819 if (e.target.value == '_loadFromFile_')
820 e.target.value = 'Custom Geometry'
821 });
822
823
824 if (selectGeometryUI) {
825
826 geometryItemSelect.value = geometryId;
827 }
828 }
829
830
831 let renderableItemSelect = document.getElementById('renderableItem');
832 if (renderableItemSelect) {
833 renderableItemSelect.addEventListener('change', (e) => {
834 let index = e.target.value;
835 if (customRenderer)
836 customRenderer.setRenderMaterial(index);
837 });
838 }
839
840
841 var canvas = document.getElementById('mygraphcanvas');
842 var canvasContainer = document.getElementById('canvasContainer');
843 var colContainer = document.getElementById('colContainer');
844
845
846 var observer = new ResizeObserver(function (entries) {
847
848
849
850
851
852 var parent = canvas.parentNode;
853 let newWidth = parent.offsetWidth;
854 let newHeight = parent.offsetHeight;
855
856
857
858
859
860
861 console.log('Resize node graph canvas to:', newWidth, newHeight);
862 editor.setDirty(newWidth, newHeight);
863
864
865 });
866
867
868 observer.observe(canvasContainer);
869
870 }
871
872 function setupGLTFSyntax() {
873
874 let cmeditor = null;
875 const gltfTextArea = document.getElementById('gltfgraph');
876 if (gltfTextArea) {
877 cmeditor = CodeMirror.fromTextArea(gltfTextArea, {
878 mode: 'application/json',
879 lineNumbers: true,
880 dragDrop: false,
881 theme: 'dracula'
882 });
883
884
885 const initialGLTF = '';
886 gltfTextArea.value = initialGLTF;
887 cmeditor.setValue(initialGLTF);
888
889
890 cmeditor.on('change', (e) => {
891 gltfTextArea.value = cmeditor.getValue();
892 });
893
894 var pasteButton = document.getElementById('gltfgraph_paste');
895 if (pasteButton)
896 addPasteHandler(pasteButton, cmeditor);
897
898 }
899 return cmeditor;
900 }
901
902 function setupJavascriptSyntax() {
903
904 const elem = document.getElementById('mtlxlib');
905 if (!elem) {
906 return;
907 }
908 let cmeditor = CodeMirror.fromTextArea(elem, {
909 mode: 'application/javascript',
910 lineNumbers: true,
911 dragDrop: false,
912 theme: 'dracula',
913 readOnly: true
914 });
915
916 elem.value = '';
917 cmeditor.setValue('');
918
919
920 cmeditor.on('change', () => {
921 elem.value = cmeditor.getValue();
922 });
923
924 return cmeditor;
925 }
926
927
928 function setupXMLSyntax() {
929
930 const materialXTextArea = document.getElementById('mtlxdoc');
931 let cmeditor = CodeMirror.fromTextArea(materialXTextArea, {
932 mode: 'application/xml',
933 lineNumbers: true,
934 dragDrop: true,
935 theme: 'night'
936 });
937
938
939 const initialXML = '';
940 materialXTextArea.value = initialXML;
941 cmeditor.setValue(initialXML);
942
943
944 cmeditor.on('change', (e) => {
945 materialXTextArea.value = cmeditor.getValue();
946 });
947
948 var pasteButton = document.getElementById('mtlxdoc_paste');
949 if (pasteButton)
950 addPasteHandler(pasteButton, cmeditor);
951
952 return cmeditor;
953 }
954
955 addUIHandlers();
956 addCopyHandlers();
957}
Custom monitor class for MaterialX graph.
This class is a wrapper around the LiteGraph library to provide a MaterialX node editor.
function toggleRequireUpdateUI()