UNPKG

sunrize

Version:

Sunrize — A Multi-Platform X3D Editor

1,541 lines (1,196 loc) 124 kB
"use strict"; const $ = require ("jquery"), electron = require ("electron"), util = require ("util"), jstree = require ("jstree"), X3D = require ("../X3D"), Interface = require ("../Application/Interface"), ActionKeys = require ("../Application/ActionKeys"), Traverse = require ("x3d-traverse") (X3D), X3DUOM = require ("../Bits/X3DUOM"), _ = require ("../Application/GetText"); const _expanded = Symbol (), _fullExpanded = Symbol (); module .exports = class OutlineView extends Interface { naturalCompare = new Intl .Collator (undefined, { numeric: true, sensitivity: "base" }) .compare; constructor (element) { super (`Sunrize.OutlineEditor.${element .attr ("id")}.`); this .outlineEditor = element; this .objects = new Map (); // <id, node> this .onDemandToolNodes = new Set (); this .config .global .setDefaultValues ({ expandExternProtoDeclarations: true, expandPrototypeInstances: true, expandInlineNodes: true, }); this .treeView = $("<div><div/>") .attr ("tabindex", "0") .addClass ("tree-view") .appendTo (this .outlineEditor); this .resizeObserver = new ResizeObserver (this .onresize .bind (this)); this .resizeObserver .observe (this .treeView [0]); this .sceneGraph = $("<div><div/>") .addClass (["tree", "scene-graph", "scene"]) .on ("dragenter dragover", this .onDragEnter .bind (this)) .on ("dragleave dragend drop", this .onDragLeave .bind (this)) .on ("drop", this .onDrop .bind (this)) .on ("dragend", this .onDragEnd .bind (this)) .appendTo (this .treeView); this .browser .getBrowserOptions () ._ColorSpace .addInterest ("updateSceneGraph", this); this .browser ._activeLayer .addInterest ("updateActiveLayer", this); electron .ipcRenderer .on ("select-all", () => this .selectAll ()); electron .ipcRenderer .on ("deselect-all", () => this .deselectAll ()); electron .ipcRenderer .on ("hide-unselected-objects", () => this .hideUnselectedObjects ()); electron .ipcRenderer .on ("show-selected-objects", () => this .showSelectedObjects ()); electron .ipcRenderer .on ("show-all-objects", () => this .showAllObjects ()); electron .ipcRenderer .on ("expand-extern-proto-declarations", (event, value) => this .expandExternProtoDeclarations = value); electron .ipcRenderer .on ("expand-prototype-instances", (event, value) => this .expandPrototypeInstances = value); electron .ipcRenderer .on ("expand-inline-nodes", (event, value) => this .expandInlineNodes = value); electron .ipcRenderer .on ("close", () => this .saveExpanded (this .config .file)); $(window) .on ("close", () => this .saveExpanded (this .config .file)); } get expandExternProtoDeclarations () { return this .config .global .expandExternProtoDeclarations; } set expandExternProtoDeclarations (value) { this .config .global .expandExternProtoDeclarations = value; this .updateSceneGraph (); } get expandPrototypeInstances () { return this .config .global .expandPrototypeInstances; } set expandPrototypeInstances (value) { this .config .global .expandPrototypeInstances = value; this .updateSceneGraph (); } get expandInlineNodes () { return this .config .global .expandInlineNodes; } set expandInlineNodes (value) { this .config .global .expandInlineNodes = value; this .updateSceneGraph (); } get autoExpandMaxChildren () { return 30; } accessTypes = { [X3D .X3DConstants .initializeOnly]: "initializeOnly", [X3D .X3DConstants .inputOnly]: "inputOnly", [X3D .X3DConstants .outputOnly]: "outputOnly", [X3D .X3DConstants .inputOutput]: "inputOutput", }; configure () { if (this .executionContext) { this .saveExpanded (this .config .last); this .removeSubtree (this .sceneGraph); this .executionContext .profile_changed .removeInterest ("updateComponents", this); this .executionContext .components .removeInterest ("updateComponents", this); } this .executionContext = this .browser .currentScene; this .executionContext .profile_changed .addInterest ("updateComponents", this); this .executionContext .components .addInterest ("updateComponents", this); this .updateComponents (); // Clear tree. this .objects .clear (); this .objects .set (this .executionContext .getId (), this .executionContext); this .sceneGraph .empty (); this .sceneGraph .attr ("node-id", this .executionContext .getId ()); // Expand scene. this .expandScene (this .sceneGraph, this .executionContext); this .restoreExpanded (); } updateComponents () { this .onDemandToolNodes = new Set ([ X3D .X3DConstants .DirectionalLight, X3D .X3DConstants .EnvironmentLight, X3D .X3DConstants .ListenerPointSource, X3D .X3DConstants .PointLight, X3D .X3DConstants .Sound, X3D .X3DConstants .SpatialSound, X3D .X3DConstants .SpotLight, X3D .X3DConstants .ViewpointGroup, X3D .X3DConstants .X3DEnvironmentalSensorNode, X3D .X3DConstants .X3DTextureProjectorNode, X3D .X3DConstants .X3DViewpointNode, ]); } updateSceneGraph () { this .updateScene (this .sceneGraph, this .executionContext); } updateScene (parent, scene) { if (!parent .prop ("isConnected")) return; this .saveScrollPositions (); this .saveExpanded (this .config .file); this .removeSubtree (parent); this .expandScene (parent, scene); this .restoreExpanded (); this .restoreScrollPositions (); } expandScene (parent, scene) { parent .data ("expanded", true) .data ("full-expanded", false); if (scene instanceof X3D .X3DScene) scene .units .addInterest ("updateScene", this, parent, scene); // Generate subtrees. const externprotos = this .expandSceneExternProtoDeclarations (parent, scene), protos = this .expandSceneProtoDeclarations (parent, scene), rootNodes = this .expandSceneRootNodes (parent, scene), importedNodes = this .expandSceneImportedNodes (parent, scene), exportedNodes = this .expandSceneExportedNodes (parent, scene); if (!externprotos .is (":empty") || !protos .is (":empty") || !rootNodes .is (":empty") || !importedNodes .is (":empty") || !exportedNodes .is (":empty")) { return; } // Add empty scene. const child = $("<div></div>") .addClass (["empty-scene", "subtree"]); const ul = $("<ul></ul>") .appendTo (child); $("<li></li>") .addClass (["empty-scene", "description", "no-select"]) .text ("Empty Scene") .appendTo (ul); this .connectSceneSubtree (parent, child); } connectSceneSubtree (parent, child) { // Make jsTree. child .jstree () .off ("keypress.jstree dblclick.jstree") .on ("before_open.jstree", this .nodeBeforeOpen .bind (this)) .on ("close_node.jstree", this .nodeCloseNode .bind (this)) .appendTo (parent) .hide (); child .removeAttr ("tabindex") .find (".jstree-anchor") .removeAttr ("href") .removeAttr ("tabindex") .on ("click", this .selectNone .bind (this)); child .find (".externproto, .proto, .node, .imported-node, .exported-node") .on ("dblclick", this .activateNode .bind (this)); child .find (".jstree-ocl") .addClass ("material-icons") .text ("arrow_right") .on ("click", this .selectExpander .bind (this)) .on ("dblclick", this .activateExpander .bind (this)); child .find (".jstree-node") .wrapInner ("<div class=\"item no-select\"/>") .find (".item") .append ("<div class=\"route-curves-wrapper\"><canvas class=\"route-curves\"></canvas></div>"); // Connect actions. this .connectNodeActions (parent, child); // Expand children. const specialElements = child .find (".externproto, .proto, .imported-node, .exported-node"), elements = child .find (".node"); child .show (); this .expandSceneSubtreeComplete (specialElements, elements); } connectNodeActions (parent, child) { if (this .isEditable (parent)) { child .find (".externproto > .item") .attr ("draggable", "true") .on ("dragstart", this .onDragStartExternProto .bind (this)); child .find (".proto > .item") .attr ("draggable", "true") .on ("dragstart", this .onDragStartProto .bind (this)); if (this .getField (parent) ?.getAccessType () !== X3D .X3DConstants .outputOnly) { child .find (".node > .item") .attr ("draggable", "true") .on ("dragstart", this .onDragStartNode .bind (this)); } child .find (".imported-node > .item") .attr ("draggable", "true") .on ("dragstart", this .onDragStartImportedNode .bind (this)); } child .find (":is(.externproto, .proto, .node, .imported-node, .exported-node) :where(.name, .icon)") .on ("click", this .selectNode .bind (this)) .on ("mouseenter", this .updateNodeTitle .bind (this)); child .find ("[action]") .on ("click", this .nodeAction .bind (this)); } nodeAction (event) { const button = $(event .target); switch (button .attr ("action")) { case "toggle-visibility": this .toggleVisibility (event); break; case "toggle-tool": this .toggleTool (event); break; case "proxy-display": this .proxyDisplay (event); break; case "activate-layer": this .activateLayer (event); break; case "bind-node": this .bindNode (event); break; case "play-node": this .playNode (event); break; case "stop-node": this .stopNode (event); break; case "loop-node": this .loopNode (event); break; case "reload-node": this .reloadNode (event); break; case "show-preview": this .showPreview (event); break; case "show-branch": this .showBranch (event); break; } } connectFieldActions (child) { child .find ("area.input-selector") .on ("mouseenter", this .hoverInSingleConnector .bind (this, "input")) .on ("mouseleave", this .hoverOutSingleConnector .bind (this, "input")) .on ("click", this .selectSingleConnector .bind (this, "input")); child .find ("area.output-selector") .on ("mouseenter", this .hoverInSingleConnector .bind (this, "output")) .on ("mouseleave", this .hoverOutSingleConnector .bind (this, "output")) .on ("click", this .selectSingleConnector .bind (this, "output")); child .find ("area.input-routes-selector") .on ("click", this .selectSingleRoute .bind (this, "input")); child .find ("area.output-routes-selector") .on ("click", this .selectSingleRoute .bind (this, "output")); } expandSceneSubtreeComplete (specialElements, elements) { this .requestUpdateRouteGraph (); this .updateQtips (); // Reopen externprotos, protos, imported, exported nodes. for (const e of specialElements) { const element = $(e), node = this .getNode (element); if (node ?.getUserData (_expanded) && element .jstree ("is_closed", element)) { element .jstree ("open_node", element); } } // Reopen nodes. for (const e of elements) { const element = $(e), node = this .getNode (element); if (node ?.getUserData (_expanded) && element .jstree ("is_closed", element)) { element .data ("auto-expand", true); element .jstree ("open_node", element); } } } updateSceneSubtree (parent, scene, type, func) { if (scene .externprotos .length || scene .protos .length || scene .rootNodes .length) { this .saveScrollPositions (); const oldSubtree = parent .find (`> .${type}.subtree`); this .disconnectSubtree (oldSubtree); const newSubtree = this [func] (parent, scene); oldSubtree .replaceWith (newSubtree .detach ()); parent .find ("> .empty-scene.subtree") .detach (); this .restoreScrollPositions (); } else { this .updateScene (parent, scene); } } expandSceneExternProtoDeclarations (parent, scene) { scene .externprotos .addInterest ("updateSceneSubtree", this, parent, scene, "externprotos", "expandSceneExternProtoDeclarations"); const child = $("<div></div>") .addClass (["externprotos", "subtree"]); if (!scene .externprotos .length) return child .appendTo (parent); const ul = $("<ul></ul>") .appendTo (child); $("<li></li>") .addClass (["externprotos", "description", "no-select"]) .text ("Extern Prototypes") .appendTo (ul); let index = 0; for (const externproto of scene .externprotos) ul .append (this .createNodeElement ("externproto", parent, externproto, index ++)); this .connectSceneSubtree (parent, child); return child; } expandSceneProtoDeclarations (parent, scene) { scene .protos .addInterest ("updateSceneSubtree", this, parent, scene, "protos", "expandSceneProtoDeclarations"); const child = $("<div></div>") .addClass (["protos", "subtree"]); if (!scene .protos .length) return child .appendTo (parent); const ul = $("<ul></ul>") .appendTo (child); $("<li></li>") .addClass (["protos", "description", "no-select"]) .text ("Prototypes") .appendTo (ul); let index = 0; for (const proto of scene .protos) ul .append (this .createNodeElement ("proto", parent, proto, index ++)); this .connectSceneSubtree (parent, child); return child; } #updateSceneRootNodesSymbol = Symbol (); expandSceneRootNodes (parent, scene) { scene .rootNodes .addFieldCallback (this .#updateSceneRootNodesSymbol, this .updateSceneRootNodes .bind (this, parent, scene, "root-nodes", "expandSceneRootNodes")); parent .attr ("index", scene .rootNodes .length); const child = $("<div></div>") .addClass (["root-nodes", "subtree"]); if (!scene .rootNodes .length) return child .appendTo (parent); const ul = $("<ul></ul>") .appendTo (child); $("<li></li>") .addClass (["root-nodes", "description", "no-select"]) .text ("Root Nodes") .appendTo (ul); let index = 0; for (const rootNode of scene .rootNodes) ul .append (this .createNodeElement ("node", parent, rootNode ?.getValue (), index ++)); // Added to prevent bug, that last route is not drawn right. $("<li></li>") .addClass (["last", "no-select"]) .appendTo (ul); this .connectSceneSubtree (parent, child); return child; } updateSceneRootNodes (parent, scene, type, func) { if (this .#changing) return; this .updateSceneSubtree (parent, scene, type, func); } expandSceneImportedNodes (parent, scene) { scene .importedNodes .addInterest ("updateSceneSubtree", this, parent, scene, "imported-nodes", "expandSceneImportedNodes"); const child = $("<div></div>") .addClass (["imported-nodes", "subtree"]); if (!scene .importedNodes .length) return child .appendTo (parent); const importedNodes = Array .from (scene .importedNodes) .sort ((a, b) => { return this .naturalCompare (a .getImportedName (), b .getImportedName ()); }); const ul = $("<ul></ul>") .appendTo (child); $("<li></li>") .addClass (["imported-nodes", "description", "no-select"]) .text ("Imported Nodes") .appendTo (ul); for (const [index, importedNode] of importedNodes .entries ()) ul .append (this .createImportedNodeElement (["imported-node"], parent, scene, importedNode .getExportedNode (), index)); // Added to prevent bug, that last route is not drawn right. $("<li></li>") .addClass (["last", "no-select"]) .appendTo (ul); this .connectSceneSubtree (parent, child); return child; } expandSceneExportedNodes (parent, scene) { if (!(scene instanceof X3D .X3DScene)) return $("<div></div>") const child = $("<div></div>") .addClass (["exported-nodes", "subtree"]) scene .exportedNodes .addInterest ("updateSceneSubtree", this, parent, scene, "exported-nodes", "expandSceneExportedNodes") if (!scene .exportedNodes .length) return child .appendTo (parent) const exportedNodes = Array .from (scene .exportedNodes) .sort ((a, b) => { return this .naturalCompare (a .getExportedName (), b .getExportedName ()) }) .sort ((a, b) => { return this .naturalCompare (a .getLocalNode () .getTypeName (), b .getLocalNode () .getTypeName ()) }) const ul = $("<ul></ul>") .appendTo (child) $("<li></li>") .addClass (["exported-nodes", "description", "no-select"]) .text ("Exported Nodes") .appendTo (ul) for (const exportedNode of exportedNodes) { ul .append (this .createExportedNodeElement ("exported-node", parent, exportedNode) .prop ("outerHTML")) } // Added to prevent bug, that last route is not drawn right. $("<li></li>") .addClass (["last", "no-select"]) .appendTo (ul) this .connectSceneSubtree (parent, child) return child } createSceneElement (scene, typeName, classes) { this .objects .set (scene .getId (), scene) // Scene const child = $("<li></li>") .addClass ("scene") .addClass (classes) .attr ("node-id", scene .getId ()) // Icon const icon = $("<img></img>") .addClass ("icon") .attr ("src", "../images/OutlineEditor/Node/X3DExecutionContext.svg") .appendTo (child) // Name const name = $("<div></div>") .addClass ("name") .appendTo (child) $("<span></span>") .addClass ("field-name") .text (typeName) .appendTo (name) // Append empty tree to enable expander. $("<ul><li></li></ul>") .appendTo (child) return child } updateNode (parent, node, full) { if (!parent .prop ("isConnected")) return; this .saveScrollPositions (); this .removeSubtree (parent); this .expandNode (parent, node, full); this .restoreScrollPositions (); } #updateNodeSymbol = Symbol (); expandNode (parent, node, full) { parent .data ("expanded", true) .data ("full-expanded", full); // Generate tree. const child = $("<div></div>") .addClass ("subtree"); const ul = $("<ul></ul>") .appendTo (child); // Fields let fields = full ? node .getFields () : node .getChangedFields (true); if (!fields .length) fields = node .getFields (); if (node .canUserDefinedFields ()) { // Move user-defined fields on top. const userDefinedFields = node .getUserDefinedFields (); fields .sort ((a, b) => { const ua = userDefinedFields .get (a .getName ()) === a, ub = userDefinedFields .get (b .getName ()) === b; return ub - ua; }); // Move metadata field on top. fields .sort ((a, b) => { const ma = a === node ._metadata, mb = b === node ._metadata; return mb - ma; }); // Proto fields, user-defined fields. // Instances are updated, because they completely change. node .getPredefinedFields () .addInterest ("updateNode", this, parent, node, full); node .getUserDefinedFields () .addInterest ("updateNode", this, parent, node, full); } for (const field of fields) ul .append (this .createFieldElement (parent, node, field)); // Extern proto if (parent .hasClass ("externproto")) { // URL ul .append (this .createFieldElement (parent, node, node ._url, "special")); // Proto node .getLoadState () .addFieldCallback (this .#updateNodeSymbol, this .updateNode .bind (this, parent, node, full)); if (this .expandExternProtoDeclarations && node .checkLoadState () === X3D .X3DConstants .COMPLETE_STATE) ul .append (this .createNodeElement ("proto", parent, node .getProtoDeclaration ())); else ul .append (this .createLoadStateElement (node .checkLoadState (), "Extern Prototype")); } // Proto Body or Instance Body if (node instanceof X3D .X3DProtoDeclaration) { ul .append (this .createSceneElement (node .getBody (), "Body", "proto-scene")); } else if (this .expandPrototypeInstances && node .getType () .includes (X3D .X3DConstants .X3DPrototypeInstance)) { if (node .getBody ()) ul .append (this .createSceneElement (node .getBody (), "Body", "instance-scene")); else if (node .getProtoNode () .isExternProto) ul .append (this .createLoadStateElement (node .getProtoNode () .checkLoadState (), node .getTypeName ())); } // X3DUrlObject scene or load state if (parent .is (".node, .imported-node, .exported-node") && node .getType () .includes (X3D .X3DConstants .X3DUrlObject)) { // X3DUrlObject if (node .getType () .includes (X3D .X3DConstants .Inline)) { node .getLoadState () .addFieldCallback (this .#updateNodeSymbol, this .updateNode .bind (this, parent, node, full)); } else { node .getLoadState () .addFieldCallback (this .#updateNodeSymbol, this .updateFieldLoadState .bind (this, node)); } if (node .checkLoadState () === X3D .X3DConstants .COMPLETE_STATE && this .expandInlineNodes && node .getType () .includes (X3D .X3DConstants .Inline)) { ul .append (this .createSceneElement (node .getInternalScene (), "Scene", "internal-scene")) } else { ul .append (this .createLoadStateElement (node .checkLoadState (), node .getTypeName ()) .addClass (`load-state-${node .getId ()}`)); } } // Make jsTree. child .jstree () .off ("keypress.jstree dblclick.jstree") .on ("before_open.jstree", this .fieldBeforeOpen .bind (this)) .on ("close_node.jstree", this .fieldCloseNode .bind (this)) .appendTo (parent) .hide (); child .removeAttr ("tabindex") .find (".jstree-anchor") .removeAttr ("href") .removeAttr ("tabindex") .on ("click", this .selectNone .bind (this)); child .find ("li") .on ("dblclick", this .activateField .bind (this)); child .find (".jstree-ocl") .addClass ("material-icons") .text ("arrow_right") .on ("click", this .selectExpander .bind (this)) .on ("dblclick", this .activateExpander .bind (this)); child .find (".jstree-node") .wrapInner ("<div class=\"item no-select\"/>") .find (".item") .append ("<div class=\"route-curves-wrapper\"><canvas class=\"route-curves\"></canvas></div>"); child .find (".field .name, .field .icon, .special .name, .special .icon") .on ("click", this .selectField .bind (this)) child .find (".field .name, .special .name") .on ("mouseenter", this .updateFieldTitle .bind (this)); child .find ("area.input-selector") .on ("mouseenter", this .hoverInConnector .bind (this, "input")) .on ("mouseleave", this .hoverOutConnector .bind (this, "input")) .on ("click", this .selectConnector .bind (this, "input")); child .find ("area.output-selector") .on ("mouseenter", this .hoverInConnector .bind (this, "output")) .on ("mouseleave", this .hoverOutConnector .bind (this, "output")) .on ("click", this .selectConnector .bind (this, "output")); child .find ("area.input-routes-selector") .on ("click", this .selectRoutes .bind (this, "input")); child .find ("area.output-routes-selector") .on ("click", this .selectRoutes .bind (this, "output")); if (this .isEditable (parent)) { child .find (".field > .item") .attr ("draggable", "true") .on ("dragstart", this .onDragStartField .bind (this)); } // Add special field buttons. this .addFieldButtons (parent, child, node); // Expand children. const protos = child .find (".proto"), scenes = child .find (".scene"), elements = child .find (".field, .special"); child .show (); this .expandNodeComplete (protos, scenes, elements); } expandNodeComplete (protos, scenes, elements) { this .requestUpdateRouteGraph (); this .updateQtips (); // Auto expand SFNodes for (const e of elements .filter ("li[type-name=SFNode]")) { const element = $(e), field = this .getField (element); if (field .getValue ()) { element .data ("auto-expand", true); element .jstree ("open_node", element); } } // Auto expand MFNodes for (const e of elements .filter ("li[type-name=MFNode]")) { const element = $(e), field = this .getField (element); if (field .length && field .length <= this .autoExpandMaxChildren) { element .data ("auto-expand", true); element .jstree ("open_node", element); } } // Reopen protos. for (const e of protos) { const element = $(e), node = this .getNode (element); if (node .getUserData (_expanded) && element .jstree ("is_closed", element)) { element .jstree ("open_node", element); } } // Reopen scenes. for (const e of scenes) { const element = $(e), scene = this .getNode (element); if (scene .getUserData (_expanded) && element .jstree ("is_closed", element)) { element .data ("auto-expand", true); element .jstree ("open_node", element); } } // Reopen fields. for (const e of elements) { const element = $(e), field = this .getField (element); if (field .getUserData (_expanded) && element .jstree ("is_closed", element)) { element .data ("auto-expand", true); element .jstree ("open_node", element); } } } updateFieldLoadState (node) { const [className, description] = this .getLoadState (node .checkLoadState (), node .getTypeName ()); this .sceneGraph .find (`.load-state-${node .getId ()}`) .removeClass (["not-started-state", "in-progress-state", "complete-state", "failed-state"]) .addClass (className) .find (".load-state-text") .text (description); } createLoadStateElement (loadState, typeName) { const [className, description] = this .getLoadState (loadState, typeName); return $("<li></li>") .addClass (["description", "load-state", className, "no-select"]) .append ($("<span></span>") .addClass ("load-state-text") .text (description)); } getLoadState (loadState, typeName) { switch (loadState) { case X3D .X3DConstants .NOT_STARTED_STATE: return ["not-started-state", util .format (_("Loading %s not started."), typeName)]; case X3D .X3DConstants .IN_PROGRESS_STATE: return ["in-progress-state", util .format (_("Loading %s in progress."), typeName)]; case X3D .X3DConstants .COMPLETE_STATE: return ["complete-state", util .format (_("Loading %s completed."), typeName)]; case X3D .X3DConstants .FAILED_STATE: return ["failed-state", util .format (_("Loading %s failed."), typeName)]; } } #nodeSymbol = Symbol (); #updateNodeVisibilitySymbol = Symbol (); #updateNodeBoundSymbol = Symbol (); #updateNodeLoadStateSymbol = Symbol (); #updateNodePlaySymbol = Symbol (); createNodeElement (type, parent, node, index) { if (node instanceof X3D .X3DImportedNodeProxy) return this .createImportedNodeElement (["imported-node", "proxy"], parent, node .getExecutionContext (), node, index); if (node) { if (!node .isInitialized ()) { // Setup nodes in protos, disable some init functions. for (const type of node .getType ()) { switch (type) { case X3D .X3DConstants .Script: node .initialize__ = Function .prototype; break; case X3D .X3DConstants .X3DTimeDependentNode: node .set_start = Function .prototype; break; } } node .setup (); } this .objects .set (node .getId (), node .valueOf ()); // These fields are observed and must never be disconnected, because clones would also lose connection. node .typeName_changed .addFieldCallback (this .#nodeSymbol, this .updateNodeTypeName .bind (this, node)); node .name_changed .addFieldCallback (this .#nodeSymbol, this .updateNodeName .bind (this, node)); node .parents_changed .addFieldCallback (this .#nodeSymbol, this .updateCloneCount .bind (this, node)); } // Classes const classes = [type]; if (node) { const selection = require ("../Application/Selection"); if (selection .has (node)) classes .push ("selected"); if (this .isInParents (parent, node)) classes .push ("circular-reference", "no-expand"); } else { classes .push ("no-expand"); } // Node const child = $("<li></li>") .addClass (classes) .attr ("node-id", node ? node .getId () : "NULL") .attr ("index", index); // Icon const icon = $("<img></img>") .addClass ("icon") .attr ("src", `../images/OutlineEditor/Node/${this .nodeIcons .get (type)}.svg`) .appendTo (child); if (node) { // Name const name = $("<div></div>") .addClass ("name") .appendTo (child); $("<span></span>") .addClass ("node-type-name") .text (this .typeNames .get (node .getTypeName ()) ?? node .getTypeName ()) .appendTo (name); name .append (document .createTextNode (" ")); $("<span></span>") .addClass ("node-name") .text (node .getDisplayName ()) .appendTo (name); name .append (document .createTextNode (" ")); const cloneCount = node .getCloneCount ?.() ?? 0 $("<span></span>") .addClass ("clone-count") .text (cloneCount > 1 ? `[${cloneCount}]` : "") .appendTo (name); // Add buttons to name. this .addNodeButtons (this .getNode (parent), node, name); // Append empty tree to enable expander. if (!this .isInParents (parent, node)) $("<ul><li></li></ul>") .appendTo (child); } else { $("<div></div>") .addClass ("name") .append ($("<span></span>") .addClass ("node-type-name") .text ("NULL")) .appendTo (child); } return child; } addNodeButtons (parent, node, name) { // Add buttons to name. const buttons = [ ]; if (!(node .getExecutionContext () .getOuterNode () instanceof X3D .X3DProtoDeclaration)) { if (node ._hidden) { buttons .push ($("<span></span>") .attr ("order", "0") .attr ("title", "Toggle visibility.") .attr ("action", "toggle-visibility") .addClass (["button", "material-symbols-outlined"]) .addClass (node .isHidden () ? "off" : "on") .text (node .isHidden () ? "visibility_off" : "visibility")); node ._hidden .addFieldCallback (this .#updateNodeVisibilitySymbol, this .updateNodeVisibility .bind (this, node)); } if (node .getType () .some (type => this .onDemandToolNodes .has (type))) { buttons .push ($("<span></span>") .attr ("order", "1") .attr ("title", _("Toggle display tool.")) .attr ("action", "toggle-tool") .addClass (["button", "material-symbols-outlined"]) .addClass (node .valueOf () === node ? "off" : "on") .text ("build_circle")); } } for (const type of node .getType ()) { switch (type) { case X3D .X3DConstants .Collision: { buttons .push ($("<span></span>") .attr ("order", "2") .attr ("title", _("Display proxy node.")) .attr ("action", "proxy-display") .addClass (["button", "material-symbols-outlined"]) .addClass (node .getProxyDisplay () ? "on" : "off") .text ("highlight_mouse_cursor")); break; } case X3D .X3DConstants .X3DLayerNode: { if (node .getExecutionContext () !== this .executionContext) continue; buttons .push ($("<span></span>") .attr ("order", "3") .attr ("title", _("Activate layer.")) .attr ("action", "activate-layer") .addClass (["button", "material-symbols-outlined"]) .addClass (this .browser .getActiveLayer () === node ? "on" : "off") .text ("check_circle")); continue; } case X3D .X3DConstants .X3DBindableNode: { if (node .getExecutionContext () .getOuterNode () instanceof X3D .X3DProtoDeclaration) continue; node ._isBound .addFieldCallback (this .#updateNodeBoundSymbol, this .updateNodeBound .bind (this, node)); buttons .push ($("<span></span>") .attr ("order", "4") .attr ("title", _("Bind node.")) .attr ("action", "bind-node") .addClass (["button", "material-symbols-outlined"]) .addClass (node ._isBound .getValue () ? "on" : "off") .text (node ._isBound .getValue () ? "radio_button_checked" : "radio_button_unchecked")); continue; } case X3D .X3DConstants .X3DTimeDependentNode: { if (node .getExecutionContext () !== this .executionContext) continue; node ._enabled .addFieldCallback (this .#updateNodePlaySymbol, this .updateNodePlay .bind (this, node)); node ._isActive .addFieldCallback (this .#updateNodePlaySymbol, this .updateNodePlay .bind (this, node)); node ._isPaused .addFieldCallback (this .#updateNodePlaySymbol, this .updateNodePlay .bind (this, node)); node ._loop .addFieldCallback (this .#updateNodePlaySymbol, this .updateNodePlay .bind (this, node)); buttons .push ($("<span></span>") .attr ("order", "5") .attr ("title", node ._isActive .getValue () && !node ._isPaused .getValue () ? _("Pause timer.") : _("Start timer.")) .attr ("action", "play-node") .addClass (["button", "material-icons"]) .addClass (node ._isPaused .getValue () ? "on" : "off") .text (node ._isActive .getValue () ? "pause" : "play_arrow")); buttons .push ($("<span></span>") .attr ("order", "6") .attr ("title", _("Stop timer.")) .attr ("action", "stop-node") .addClass (["button", "material-icons"]) .addClass (node ._isActive .getValue () ? "on" : "off") .text ("stop")); buttons .push ($("<span></span>") .attr ("order", "7") .attr ("title", _("Toggle loop.")) .attr ("action", "loop-node") .addClass (["button", "material-icons"]) .addClass (node ._loop .getValue () ? "on" : "off") .text ("repeat")); if (!node ._enabled .getValue ()) buttons .slice (-3) .forEach (button => button .hide ()); continue; } case X3D .X3DConstants .X3DUrlObject: { if (node .getExecutionContext () .getOuterNode () instanceof X3D .X3DProtoDeclaration) { if (!node .getType () .includes (X3D .X3DConstants .Inline)) continue; } const [className] = this .getLoadState (node .checkLoadState (), node .getTypeName ()); node .getLoadState () .addFieldCallback (this .#updateNodeLoadStateSymbol, this .updateNodeLoadState .bind (this, node)); buttons .push ($("<span></span>") .attr ("order", "8") .attr ("title", "Load now.") .attr ("action", "reload-node") .addClass (["button", "material-symbols-outlined", className]) .text ("autorenew")); continue; } case X3D .X3DConstants .AudioClip: case X3D .X3DConstants .BufferAudioSource: case X3D .X3DConstants .X3DMaterialNode: case X3D .X3DConstants .X3DSingleTextureNode: { buttons .push ($("<span></span>") .attr ("order", "9") .attr ("title", _("Show preview.")) .attr ("action", "show-preview") .addClass (["button", "material-symbols-outlined", "off"]) .css ("top", "2px") .text ("preview")); continue; } } } for (const type of parent .getType ()) { switch (type) { case X3D .X3DConstants .LOD: case X3D .X3DConstants .Switch: { buttons .push ($("<span></span>") .attr ("order", "10") .attr ("title", _("Show branch.")) .attr ("action", "show-branch") .addClass (["button", "material-symbols-outlined"]) .addClass (parent .getEditChild () === node ? "on" : "off") .text ("highlight_mouse_cursor")); break; } } } buttons .sort ((a, b) => a .attr ("order") - b .attr ("order")) for (const button of buttons) { name .append (document .createTextNode (" ")); name .append (button); } } updateNodeTypeName (node) { this .sceneGraph .find (`.node[node-id=${node .getId ()}], .exported-node[node-id=${node .getId ()}]`) .find ("> .item .node-type-name") .text (node .getTypeName ()) } updateNodeName (node) { this .sceneGraph .find (`.node[node-id=${node .getId ()}], .exported-node[node-id=${node .getId ()}]`) .find ("> .item .node-name") .text (node .getDisplayName ()) } updateExportedNodeName (exportedNode) { const node = exportedNode .getLocalNode (), name = this .sceneGraph .find (`.exported-node[node-id=${node .getId ()}]`) .find ("> .item .name"); name .find (".node-name") .text (node .getName ()); name .find (".as-name") .text (exportedNode .getExportedName ()); if (exportedNode .getExportedName () === node .getName ()) name .find (".node-name") .nextAll () .hide (); else name .find (".node-name") .nextAll () .show (); } updateCloneCount (node) { const cloneCount = node .getCloneCount ?.() ?? 0; this .sceneGraph .find (`:is(.node, .imported-node)[node-id="${node .getId ()}"]`) .find ("> .item .clone-count") .text (cloneCount > 1 ? `[${cloneCount}]` : ""); } updateActiveLayer () { const node = this .browser .getActiveLayer (); this .sceneGraph .find ("[action=activate-layer]") .addClass ("off"); if (!node) return; this .sceneGraph .find (`.node[node-id=${node .getId ()}], .imported-node[node-id=${node .getId ()}], .exported-node[node-id=${node .getId ()}]`) .find ("> .item [action=activate-layer]") .removeClass ("off") .addClass ("on"); } updateNodeVisibility (node) { this .sceneGraph .find (`.node[node-id=${node .getId ()}], .imported-node[node-id=${node .getId ()}], .exported-node[node-id=${node .getId ()}]`) .find ("> .item [action=toggle-visibility]") .removeClass (["on", "off"]) .addClass (node .isHidden () ? "off" : "on") .text (node .isHidden () ? "visibility_off" : "visibility"); } updateNodeBound (node) { this .sceneGraph .find (`.node[node-id=${node .getId ()}], .imported-node[node-id=${node .getId ()}], .exported-node[node-id=${node .getId ()}]`) .find ("> .item [action=bind-node]") .removeClass (["on", "off"]) .addClass (node ._isBound .getValue () ? "on" : "off") .text (node ._isBound .getValue () ? "radio_button_checked" : "radio_button_unchecked"); } updateNodeLoadState (node) { const [className] = this .getLoadState (node .checkLoadState (), node .getTypeName ()); this .sceneGraph .find (`.node[node-id=${node .getId ()}], .imported-node[node-id=${node .getId ()}], .exported-node[node-id=${node .getId ()}], .externproto[node-id=${node .getId ()}]`) .find ("> .item .reload-node") .removeClass (["not-started-state", "in-progress-state", "complete-state", "failed-state"]) .addClass (className); } updateNodePlay (node) { const buttons = [ ]; buttons .push (this .sceneGraph .find (`.node[node-id=${node .getId ()}], .imported-node[node-id=${node .getId ()}], .exported-node[node-id=${node .getId ()}]`) .find ("> .item [action=play-node]") .removeClass (["on", "off"]) .addClass (node ._isPaused .getValue () ? "on" : "off") .attr ("title", node ._isActive .getValue () && !node ._isPaused .getValue () ? _("Pause timer.") : _("Start timer.")) .text (node ._isActive .getValue () ? "pause" : "play_arrow")); buttons .push (this .sceneGraph .find (`.node[node-id=${node .getId ()}], .imported-node[node-id=${node .getId ()}], .exported-node[node-id=${node .getId ()}]`) .find ("> .item [action=stop-node]") .removeClass (["on", "off"]) .addClass (node ._isActive .getValue () ? "on" : "off")); buttons .push (this .sceneGraph .find (`.node[node-id=${node .getId ()}], .imported-node[node-id=${node .getId ()}], .exported-node[node-id=${node .getId ()}]`) .find ("> .item [action=loop-node]") .removeClass (["on", "off"]) .addClass (node ._loop .getValue () ? "on" : "off")); if (node ._enabled .getValue ()) buttons .slice (-3) .forEach (button => button .show ()); else buttons .slice (-3) .forEach (button => button .hide ()); if (!node ._isActive .getValue ()) node ._evenLive = false; } isInParents (parent, node) { return parent .closest (`.node[node-id=${node .getId ()}]`, this .sceneGraph) .length; } #importedNodeSymbol = Symbol (); createImportedNodeElement (type, parent, scene, node, index) { const importedNode = node .getImportedNode (); if (!importedNode) return this .createNodeElement ("node", parent, null, index); // These fields are observed and must never be disconnected, because clones would also lose connection. node .name_changed .addFieldCallback (this .#importedNodeSymbol, this .updateImportedNodeName .bind (this, importedNode)); node .parents_changed .addFieldCallback (this .#nodeSymbol, this .updateCloneCount .bind (this, node)); importedNode .getInlineNode () .getLoadState () .addFieldCallback (this .#importedNodeSymbol, this .updateScene .bind (this, parent .closest (".scene"), scene)); this .objects .set (node .getId (), node); this .objects .set (importedNode .getId (), importedNode); // Node const classes = type; if (importedNode .getExportedNode () .getSharedNode ()) { const selection = require ("../Application/Selection"); if (selection .has (node)) classes .push ("selected"); } else { classes .push ("no-expand"); } const child = $("<li></li>") .addClass (classes) .attr ("node-id", node .getId ()) .attr ("imported-node-id", importedNode .getId ()) .attr ("index", index) .attr ("title", importedNode .getDescription ()); // Icon const icon = $("<img></img>") .addClass ("icon") .attr ("src", `../images/OutlineEditor/Node/${this .nodeIcons .get (type [0])}.svg`) .appendTo (child); // Name const name = $("<div></div>") .addClass ("name") .appendTo (child); $("<span></span>") .addClass ("node-type-name") .text (node .getTypeName ()) .appendTo (name); name .append (document .createTextNode (" ")); $("<span></span>") .addClass ("node-name") .text (importedNode .getExportedName ()) .appendTo (name); const nodeAsName = $("<span></span>") .addClass ("node-as-name") .append (document .createTextNode (" ")) .append ($("<span></span>") .addClass ("as") .text ("AS")) .append (document .createTextNode (" ")) .append ($("<span></span>") .addClass ("as-name") .text (importedNode .getImportedName ())) .appendTo (name); if (importedNode .getExportedName () === importedNode .getImportedName ()) nodeAsName .hide (); name .append (document .createTextNode (" ")); const cloneCount = node .getCloneCount ?.() ?? 0 $("<span></span>") .addClass ("clone-count") .text (cloneCount > 1 ? `[${cloneCount}]` : "") .appendTo (name); // Add buttons to name. this .addNodeButtons (this .getNode (parent), node, name); // Append empty tree to enable expander. $("<ul><li></li></ul>") .appendTo (child); return child; } updateImportedNodeName (importedNode) { const nodeAsName = this .sceneGraph .find (`.imported-node[imported-node-id="${importedNode .getId ()}"]`) .find ("> .item .node-as-name"); nodeAsName .find (".as-name") .text (importedNode .getImportedName ()); if (importedNode .getExportedName () === importedNode .getImportedName ()) nodeAsName .hide (); else nodeAsName .show (); } #exportedNodeSymbol = Symbol (); createExportedNodeElement (type, parent, exportedNode) { const node = exportedNode .getLocalNode (); this .objects .set (exportedNode .getId (), exportedNode); this .objects .set (node .getId (), node .valueOf ()); // These fields are observed and must never be disconnected, because clones would also lose connection. node .typeName_changed .addFieldCallback (this .#exportedNodeSymbol