UNPKG

sunrize

Version:

Sunrize — A Multi-Platform X3D Editor

1,247 lines (1,083 loc) 143 kB
"use strict"; const $ = require ("jquery"), electron = require ("electron"), path = require ("path"), url = require ("url"), fs = require ("fs"), mime = require ("../Bits/MimeTypes"), X3D = require ("../X3D"), OutlineRouteGraph = require ("./OutlineRouteGraph"), Editor = require ("../Undo/Editor"), UndoManager = require ("../Undo/UndoManager"), _ = require ("../Application/GetText"); module .exports = class OutlineEditor extends OutlineRouteGraph { constructor (element) { super (element); element .on ("contextmenu", (event) => this .showContextMenu (event)); electron .ipcRenderer .on ("outline-editor", (event, key, ... args) => this [key] (... args)); this .setup (); } updateComponents () { super .updateComponents (); this .matrixNodes = new Set ([ X3D .X3DConstants .Billboard, X3D .X3DConstants .CADPart, X3D .X3DConstants .GeoLocation, X3D .X3DConstants .GeoTransform, X3D .X3DConstants .HAnimHumanoid, X3D .X3DConstants .HAnimJoint, X3D .X3DConstants .HAnimSite, X3D .X3DConstants .LayoutGroup, X3D .X3DConstants .ScreenGroup, X3D .X3DConstants .Transform, X3D .X3DConstants .X3DNBodyCollidableNode, ]); this .transformLikeNodes = new Set ([ X3D .X3DConstants .HAnimHumanoid, X3D .X3DConstants .X3DTransformNode, ]); } showContextMenu (event) { const element = $(document .elementFromPoint (event .pageX, event .pageY)) .closest ("li, #outline-editor", this .outlineEditor); if (!(this .isEditable (element) || element .is (".exported-node"))) return; if (!element .is (".manually")) this .sceneGraph .find (".manually") .removeClass ("manually"); if (element .is (":is(.externproto, .proto, .proto-scene, .node, .field, .imported-node, .exported-node):not(.manually)")) this .selectPrimaryElement (element); const executionContextElement = element .closest (".scene"), executionContext = this .getNode (executionContextElement) ?? this .executionContext, node = this .getNode (element); if (element .is (".field")) { const outerNode = executionContext .getOuterNode (), field = this .getField (element), userDefined = node .getUserDefinedFields () .has (field .getName ()); const addReferences = [ ], removeReferences = [ ]; if (outerNode instanceof X3D .X3DProtoDeclaration && node .getType () .includes (X3D .X3DConstants .X3DNode)) { const proto = outerNode, references = [ ]; for (const protoField of proto .getUserDefinedFields ()) { if (protoField .getType () !== field .getType ()) continue; if (!protoField .isReference (field .getAccessType ())) continue; references .push (protoField); } // Make menus. for (const reference of references) { const menuItem = { label: reference .getName (), args: [proto .getId (), reference .getId (), node .getId (), field .getId ()], }; if (field .getReferences () .has (reference)) { menuItem .args .unshift ("removeReference"); removeReferences .push (menuItem); } else { menuItem .args .unshift ("addReference"); addReferences .push (menuItem); } } } var menu = [ { label: _("Add Node..."), visible: field .getType () === X3D .X3DConstants .SFNode || field .getType () === X3D .X3DConstants .MFNode, args: ["openLibrary", element .attr ("id"), executionContext .getId (), node .getId (), field .getId ()], }, { type: "separator" }, { label: _("Paste"), accelerator: "CmdOrCtrl+V", visible: field .getType () === X3D .X3DConstants .SFNode || field .getType () === X3D .X3DConstants .MFNode, args: ["pasteNodes", element .attr ("id"), executionContext .getId (), node .getId (), field .getId ()], }, { type: "separator" }, { label: _("Add Field..."), visible: node .canUserDefinedFields (), args: ["addUserDefinedField", element .attr ("id"), executionContext .getId (), node .getId (), field .getId ()], }, { label: _("Edit Field..."), visible: node .canUserDefinedFields (), enabled: userDefined, args: ["editUserDefinedField", element .attr ("id"), executionContext .getId (), node .getId (), field .getId ()], }, { label: _("Delete Field"), visible: node .canUserDefinedFields (), enabled: userDefined, args: ["deleteUserDefinedField", element .attr ("id"), executionContext .getId (), node .getId (), field .getId ()], }, { type: "separator" }, { label: _("Add Reference to"), submenu: addReferences, visible: !! addReferences .length, }, { label: _("Remove Reference to"), submenu: removeReferences, visible: !! removeReferences .length, }, { type: "separator" }, { label: _("Reset to Default Value"), visible: field .getAccessType () !== X3D .X3DConstants .outputOnly, args: ["resetToDefaultValue", element .attr ("id"), executionContext .getId (), node .getId (), field .getId ()], }, { label: _("Trigger Event"), visible: field .getAccessType () !== X3D .X3DConstants .outputOnly, args: ["triggerEvent", element .attr ("id"), node .getId (), field .getId ()], }, ]; } else if (element .is (".node")) { const parentFieldElement = element .closest (".field, .scene", this .sceneGraph), parentNodeElement = parentFieldElement .closest (".node, .proto, .scene", this .sceneGraph), innerNode = $.try (() => node .getInnerNode ()) ?? node; if (node) { var menu = [ { label: _("Edit Node..."), args: ["editNode", element .attr ("id"), executionContext .getId (), node .getId ()], }, { label: _("Export Node..."), enabled: executionContext === this .executionContext, args: ["addExportedNode", element .attr ("id"), executionContext .getId (), node .getId ()], }, { label: _("Add Node..."), args: ["openLibrary", element .attr ("id"), executionContext .getId (), node .getId ()], }, { type: "separator" }, { label: _("Cut"), accelerator: "CmdOrCtrl+X", args: ["cutNodes"], }, { label: _("Copy"), accelerator: "CmdOrCtrl+C", args: ["copyNodes"], }, { label: _("Paste"), accelerator: "CmdOrCtrl+V", args: ["pasteNodes", element .attr ("id"), executionContext .getId (), node .getId ()], }, { label: _("Delete"), accelerator: "CmdOrCtrl+Backspace", args: ["deleteNodes"], }, { label: _("Unlink Clone"), enabled: node .getCloneCount () > 1, args: ["unlinkClone", element .attr ("id"), executionContext .getId (), node .getId ()], }, { type: "separator" }, { label: _("Add Field..."), visible: node .canUserDefinedFields (), args: ["addUserDefinedField", element .attr ("id"), executionContext .getId (), node .getId ()], }, { type: "separator" }, ]; if (innerNode .getType () .includes (X3D .X3DConstants .X3DChildNode)) this .addChildNodeMenu (menu, element, parentNodeElement, executionContext, node); for (const type of node .getType () .toReversed ()) { switch (type) { case X3D .X3DConstants .ElevationGrid: case X3D .X3DConstants .GeoElevationGrid: case X3D .X3DConstants .X3DComposedGeometryNode: { if (node ._normal .getValue ()) { menu .push ({ label: _("Remove Normals"), args: ["removeNormalsFromGeometry", element .attr ("id"), executionContext .getId (), node .getId ()], }); } else { menu .push ({ label: _("Add Normals"), args: ["addNormalsToGeometry", element .attr ("id"), executionContext .getId (), node .getId ()], }); } continue; } case X3D .X3DConstants .X3DGeometryNode: { if (node .toIndexedTriangleSet) { menu .push ( { label: _("Convert Node to IndexedTriangleSet"), args: ["toIndexedTriangleSet", element .attr ("id"), executionContext .getId (), node .getId ()], }); } if (node .toPrimitive) { menu .push ( { label: _("Convert Node to Next Lower Geometry Type"), args: ["toPrimitive", element .attr ("id"), executionContext .getId (), node .getId ()], }); } if (!node .getType () .includes (X3D .X3DConstants .InlineGeometry)) { menu .push ({ label: _("Convert Node to InlineGeometry..."), args: ["convertNodeToInlineFile", element .attr ("id"), executionContext .getId (), node .getId (), "InlineGeometry"], }); } continue; } case X3D .X3DConstants .ImageTexture: { if (node .checkLoadState () === X3D .X3DConstants .COMPLETE_STATE) { menu .push ({ label: _("Convert Node to PixelTexture"), args: ["convertImageTextureToPixelTexture", element .attr ("id"), executionContext .getId (), node .getId ()], }); } continue; } case X3D .X3DConstants .Inline: { menu .push ({ label: _("Open Inline Scene in New Tab"), enabled: node .checkLoadState () === X3D .X3DConstants .COMPLETE_STATE, args: ["openFileInNewTab", node .getInternalScene () ?.worldURL], }, { label: _("Fold Inline Back into Scene"), enabled: node .checkLoadState () === X3D .X3DConstants .COMPLETE_STATE, args: ["foldInlineBackIntoScene", element .attr ("id"), executionContext .getId (), node .getId ()], }); continue; } case X3D .X3DConstants .InlineGeometry: { menu .push ({ label: _("Open InlineGeometry Scene in New Tab"), enabled: node .checkLoadState () === X3D .X3DConstants .COMPLETE_STATE, args: ["openFileInNewTab", node .getInternalScene () ?.worldURL], }, { label: _("Fold InlineGeometry Back into Scene"), enabled: node .checkLoadState () === X3D .X3DConstants .COMPLETE_STATE && !!$.try (() => node .getInnerNode ()), args: ["foldInlineGeometryBackIntoScene", element .attr ("id"), executionContext .getId (), node .getId ()], }); continue; } case X3D .X3DConstants .PixelTexture: { menu .push ({ label: _("Update Image from File..."), args: ["updatePixelTextureFromFile", element .attr ("id"), executionContext .getId (), node .getId ()], }); if (node .checkLoadState () === X3D .X3DConstants .COMPLETE_STATE) { menu .push ({ label: _("Convert Node to ImageTexture"), args: ["convertPixelTextureToImageTexture", element .attr ("id"), executionContext .getId (), node .getId ()], }); } continue; } case X3D .X3DConstants .Normal: { menu .push ({ label: _("Negate Normal Vectors"), args: ["negateNormalVectors", element .attr ("id"), executionContext .getId (), node .getId ()], }); continue; } case X3D .X3DConstants .Material: { menu .push ({ label: _("Convert Node to PhysicalMaterial"), args: ["convertToPhysicalMaterial", element .attr ("id"), executionContext .getId (), node .getId ()], }); continue; } case X3D .X3DConstants .X3DPrototypeInstance: { if (!$.try (() => node .getInnerNode ())) continue; menu .push ({ label: _("Unwrap Inner Node"), args: ["unwrapInnerNode", element .attr ("id"), executionContext .getId (), node .getId ()], }); if (node .getInnerNode () .getType () .includes (X3D .X3DConstants .X3DChildNode)) continue; // Proceed with next case: } case X3D .X3DConstants .X3DChildNode: { menu .push ({ label: _("Convert Node to Inline File..."), args: ["convertNodeToInlineFile", element .attr ("id"), executionContext .getId (), node .getId (), "Inline"], }); continue; } case X3D .X3DConstants .X3DBoundedObject: { menu .push ({ label: _("Determine Bounding Box from Scratch"), args: ["determineBoundingBoxFromScratch", element .attr ("id"), executionContext .getId (), node .getId ()], }); if (!node ._bboxSize .equals (new X3D .SFVec3f (-1, -1, -1))) { menu .push ({ label: _("Remove Custom Bounding Box"), args: ["removeCustomBoundingBox", element .attr ("id"), executionContext .getId (), node .getId ()], }); } continue; } case X3D .X3DConstants .X3DTransformNode: { menu .push ({ label: _("Transform to Zero"), args: ["transformToZero", element .attr ("id"), executionContext .getId (), node .getId ()], }); continue; } case X3D .X3DConstants .X3DUrlObject: { if (node ._url .some (fileURL => !fileURL .match (/^\s*(?:data|ecmascript|javascript|vrmlscript):/s))) { menu .push ({ label: _("Embed External Resource as Data URL"), args: ["embedExternalResourceAsDataURL", element .attr ("id"), executionContext .getId (), node .getId ()], }); } if (node ._url .some (fileURL => fileURL .match (/^\s*(?:data|ecmascript|javascript|vrmlscript):/s))) { menu .push ({ label: _("Save Data URL to File..."), args: ["saveDataUrlToFile", element .attr ("id"), executionContext .getId (), node .getId ()], }); } continue; } case X3D .X3DConstants .X3DViewpointNode: { menu .push ({ label: _("Move Viewpoint to User Position"), args: ["moveViewpointToUserPosition", element .attr ("id"), executionContext .getId (), node .getId ()], }); continue; } default: continue; } } } else { var menu = [ { label: _("Cut"), accelerator: "CmdOrCtrl+X", args: ["cutNodes"], }, { label: _("Copy"), accelerator: "CmdOrCtrl+C", args: ["copyNodes"], }, { label: _("Delete"), accelerator: "CmdOrCtrl+Backspace", args: ["deleteNodes"], }, ]; } } else if (element .is (".exported-node")) { const exportedNode = this .objects .get (parseInt (element .attr ("exported-node-id"))); var menu = [ { label: _("Edit Exported Node..."), visible: exportedNode .getExecutionContext () === this .executionContext, args: ["editExportedNode", element .attr ("id")], }, { label: _("Remove Exported Node"), visible: exportedNode .getExecutionContext () === this .executionContext, args: ["removeExportedNode", element .attr ("id")], }, { label: _("Import Node..."), visible: executionContext !== this .executionContext && !element .closest (".instance-scene") .length, args: ["addImportedNode", element .attr ("id")], }, ]; } else if (element .is (".imported-node")) { const parentFieldElement = element .closest (".field, .scene", this .sceneGraph), parentNodeElement = parentFieldElement .closest (".node, .proto, .scene", this .sceneGraph), node = this .objects .get (parseInt (element .attr ("node-id"))), innerNode = $.try (() => node .getInnerNode ()) ?? node, importedNode = this .objects .get (parseInt (element .attr ("imported-node-id"))), local = importedNode .getExecutionContext () .getLocalScene () === this .executionContext; var menu = [ { label: _("Edit Imported Node..."), visible: local, args: ["editImportedNode", element .attr ("id")], }, { label: _("Remove Imported Node"), visible: local && !node .getCloneCount (), args: ["removeImportedNode", element .attr ("id")], }, { label: _("Add Clone"), visible: local, args: ["addImportedNodeClone", element .attr ("id")], }, { type: "separator" }, { label: _("Cut"), visible: local && element .hasClass ("proxy"), accelerator: "CmdOrCtrl+X", args: ["cutNodes"], }, { label: _("Copy"), visible: local && element .hasClass ("proxy"), accelerator: "CmdOrCtrl+C", args: ["copyNodes"], }, { label: _("Delete"), visible: local && element .hasClass ("proxy"), accelerator: "CmdOrCtrl+Backspace", args: ["deleteNodes"], }, { type: "separator" }, ]; if (local && element .hasClass ("proxy") && innerNode .getType () .includes (X3D .X3DConstants .X3DChildNode)) this .addChildNodeMenu (menu, element, parentNodeElement, executionContext, node); } else if (element .is (".externproto, .proto")) { const protoNode = node, used = Editor .isProtoNodeUsed (executionContext, protoNode), available = Editor .getNextAvailableProtoNode (executionContext, protoNode); var menu = [ { label: _("Add Prototype..."), args: ["addPrototype", element .attr ("id"), executionContext .getId ()], }, { label: _("Edit Prototype..."), args: ["editPrototype", element .attr ("id"), executionContext .getId (), protoNode .getId ()], }, { label: _("Delete Prototype"), enabled: !used || !!available, args: ["deletePrototype", element .attr ("id"), executionContext .getId (), protoNode .getId (), used, available ?.getId ()], }, { type: "separator" }, { label: _("Copy"), args: ["copyNodes"], }, { label: _("Copy as Extern Prototype"), visible: !protoNode .isExternProto, enabled: executionContext instanceof X3D .X3DScene, args: ["copyExternPrototype"], }, { type: "separator" }, { label: _("Add Field..."), visible: !protoNode .isExternProto, args: ["addUserDefinedField", element .attr ("id"), executionContext .getId (), protoNode .getId ()], }, { type: "separator" }, { label: _("Open Extern Prototype Scene in New Tab"), visible: protoNode .isExternProto, enabled: protoNode .isExternProto && protoNode .checkLoadState () === X3D .X3DConstants .COMPLETE_STATE, args: ["openFileInNewTab", protoNode .getInternalScene ?.() ?.worldURL], }, { label: _("Turn into Extern Prototype..."), visible: !protoNode .isExternProto, args: ["turnIntoExternPrototype", element .attr ("id"), executionContext .getId (), protoNode .getId ()], }, { label: _("Turn into Prototype"), visible: protoNode .isExternProto, enabled: protoNode .isExternProto && protoNode .checkLoadState () === X3D .X3DConstants .COMPLETE_STATE, args: ["turnIntoPrototype", element .attr ("id"), executionContext .getId (), protoNode .getId ()], }, { type: "separator" }, { label: _("Add Instance"), enabled: !(protoNode .isExternProto && executionContext .protos .get (protoNode .getName ())), args: ["addInstance", element .attr ("id"), executionContext .getId (), protoNode .getId ()], }, ] } else if (element .is ("#outline-editor, .proto-scene, .description.externprotos, .description.protos, .description.root-nodes, .description.empty-scene")) { var menu = [ { label: _("Add Node..."), args: ["openLibrary", element .attr ("id"), executionContext .getId ()], }, { label: _("Add Prototype..."), args: ["addPrototype", element .attr ("id"), executionContext .getId ()], }, { label: _("Paste"), accelerator: "CmdOrCtrl+V", args: ["pasteNodes", element .attr ("id"), executionContext .getId (), undefined, undefined, true], }, ]; } else { return; } electron .ipcRenderer .send ("context-menu", "outline-editor", menu); } addChildNodeMenu (menu, element, parentNodeElement, executionContext, node) { menu .push ({ label: _("Add Parent Group"), submenu: [ { label: "Transform", args: ["addParentGroup", element .attr ("id"), executionContext .getId (), node .getId (), "Grouping", "Transform", "children"], }, { label: "Group", args: ["addParentGroup", element .attr ("id"), executionContext .getId (), node .getId (), "Grouping", "Group", "children"], }, { label: "StaticGroup", args: ["addParentGroup", element .attr ("id"), executionContext .getId (), node .getId (), "Grouping", "StaticGroup", "children"], }, { label: "Switch", args: ["addParentGroup", element .attr ("id"), executionContext .getId (), node .getId (), "Grouping", "Switch", "children"], }, { type: "separator" }, { label: "Billboard", args: ["addParentGroup", element .attr ("id"), executionContext .getId (), node .getId (), "Navigation", "Billboard", "children"], }, { label: "Collision", args: ["addParentGroup", element .attr ("id"), executionContext .getId (), node .getId (), "Navigation", "Collision", "children"], }, { label: "LOD", args: ["addParentGroup", element .attr ("id"), executionContext .getId (), node .getId (), "Navigation", "LOD", "children"], }, { label: "ViewpointGroup", args: ["addParentGroup", element .attr ("id"), executionContext .getId (), node .getId (), "Navigation", "ViewpointGroup", "children"], }, { type: "separator" }, { label: "Anchor", args: ["addParentGroup", element .attr ("id"), executionContext .getId (), node .getId (), "Navigation", "Anchor", "children"], }, { type: "separator" }, { label: "LayoutLayer", args: ["addParentGroup", element .attr ("id"), executionContext .getId (), node .getId (), "Layout", "LayoutLayer", "children"], }, { label: "LayoutGroup", args: ["addParentGroup", element .attr ("id"), executionContext .getId (), node .getId (), "Layout", "LayoutGroup", "children"], }, { label: "ScreenGroup", args: ["addParentGroup", element .attr ("id"), executionContext .getId (), node .getId (), "Layout", "ScreenGroup", "children"], }, { type: "separator" }, { label: "GeoTransform", args: ["addParentGroup", element .attr ("id"), executionContext .getId (), node .getId (), "Geospatial", "GeoTransform", "children"], }, { label: "GeoLocation", args: ["addParentGroup", element .attr ("id"), executionContext .getId (), node .getId (), "Geospatial", "GeoLocation", "children"], }, { type: "separator" }, { label: "CADLayer", args: ["addParentGroup", element .attr ("id"), executionContext .getId (), node .getId (), "CADGeometry", "CADLayer", "children"], }, { label: "CADAssembly", args: ["addParentGroup", element .attr ("id"), executionContext .getId (), node .getId (), "CADGeometry", "CADAssembly", "children"], }, { label: "CADPart", args: ["addParentGroup", element .attr ("id"), executionContext .getId (), node .getId (), "CADGeometry", "CADPart", "children"], }, { label: "CADFace", args: ["addParentGroup", element .attr ("id"), executionContext .getId (), node .getId (), "CADGeometry", "CADFace", "shape"], }, { type: "separator" }, { label: "LayerSet", args: ["addParentGroup", element .attr ("id"), executionContext .getId (), node .getId (), "Layering", "LayerSet", "layers"], }, { label: "Layer", args: ["addParentGroup", element .attr ("id"), executionContext .getId (), node .getId (), "Layering", "Layer", "children"], }, { label: "Viewport", args: ["addParentGroup", element .attr ("id"), executionContext .getId (), node .getId (), "Layering", "Viewport", "children"], }, { type: "separator" }, { label: "PickableGroup", args: ["addParentGroup", element .attr ("id"), executionContext .getId (), node .getId (), "Picking", "PickableGroup", "children"], }, { type: "separator" }, { label: "CollidableOffset", args: ["addParentGroup", element .attr ("id"), executionContext .getId (), node .getId (), "RigidBodyPhysics", "CollidableOffset", "collidable"], }, { label: "CollidableShape", args: ["addParentGroup", element .attr ("id"), executionContext .getId (), node .getId (), "RigidBodyPhysics", "CollidableShape", "shape"], }, ], }, { label: _("Remove Parent"), enabled: parentNodeElement .hasClass ("node"), args: ["removeParent", element .attr ("id"), executionContext .getId (), node .getId ()], }, { type: "separator" }); } addUserDefinedField (id, executionContextId, nodeId, fieldId) { require ("../Controls/EditUserDefinedFieldPopover"); const element = $(`#${id}`), executionContext = this .objects .get (executionContextId), node = this .objects .get (nodeId), field = this .objects .get (fieldId), index = node .getUserDefinedFields () .indexOf (field); element .find ("> .item") .editUserDefinedFieldPopover (executionContext, node, index < 0 ? 0 : index + 1); } editUserDefinedField (id, executionContextId, nodeId, fieldId) { require ("../Controls/EditUserDefinedFieldPopover"); const element = $(`#${id}`), executionContext = this .objects .get (executionContextId), node = this .objects .get (nodeId), field = this .objects .get (fieldId); element .find ("> .item") .editUserDefinedFieldPopover (executionContext, node, field); } deleteUserDefinedField (id, executionContextId, nodeId, fieldId) { const executionContext = this .objects .get (executionContextId), node = this .objects .get (nodeId), field = this .objects .get (fieldId); Editor .removeUserDefinedField (executionContext, node, field); } addReference (protoId, protoFieldId, nodeId, fieldId) { const proto = this .objects .get (protoId), protoField = this .objects .get (protoFieldId), node = this .objects .get (nodeId), field = this .objects .get (fieldId) Editor .addReference (proto, protoField, node, field) } removeReference (protoId, protoFieldId, nodeId, fieldId) { const proto = this .objects .get (protoId), protoField = this .objects .get (protoFieldId), node = this .objects .get (nodeId), field = this .objects .get (fieldId) Editor .removeReference (proto, protoField, node, field) } resetToDefaultValue (id, executionContextId, nodeId, fieldId) { const executionContext = this .objects .get (executionContextId), node = this .objects .get (nodeId), field = this .objects .get (fieldId); this .beginUndoSetFieldValue (node, field); Editor .resetToDefaultValue (executionContext, node, field); this .endUndoSetFieldValue (node, field); } triggerEvent (id, nodeId, fieldId) { const field = this .objects .get (fieldId); field .addEvent (); } editNode (id, executionContextId, nodeId) { require ("../Controls/EditNodePopover"); const element = $(`#${id}`), node = this .objects .get (nodeId); element .find ("> .item") .editNodePopover (node); } openLibrary (id, executionContextId, nodeId, fieldId) { const executionContext = this .objects .get (executionContextId), node = this .objects .get (nodeId), field = this .objects .get (fieldId); require ("./Library") .open (executionContext, node, field); } addExportedNode (id, executionContextId, nodeId) { require ("../Controls/ExportNodePopover"); const element = $(`#${id}`), node = this .objects .get (nodeId); element .find ("> .item") .exportNodePopover (node); } editExportedNode (id) { require ("../Controls/ExportNodePopover"); const element = $(`#${id}`), exportedNode = this .objects .get (parseInt (element .attr ("exported-node-id"))); element .find ("> .item") .exportNodePopover (exportedNode .getLocalNode (), exportedNode .getExportedName (), exportedNode .getDescription ()); } removeExportedNode (id) { const element = $(`#${id}`), exportedNode = this .objects .get (parseInt (element .attr ("exported-node-id"))); Editor .removeExportedNode (exportedNode .getExecutionContext (), exportedNode .getExportedName ()); } addImportedNode (id) { require ("../Controls/ImportNodePopover"); const element = $(`#${id}`), exportedNode = this .objects .get (parseInt (element .attr ("exported-node-id"))), inlineNode = this .getNode (element .closest (".node", this .sceneGraph)); element .find ("> .item") .importNodePopover (inlineNode, exportedNode .getExportedName (), "", exportedNode .getDescription ()); } editImportedNode (id) { require ("../Controls/ImportNodePopover"); const element = $(`#${id}`), importedNode = this .objects .get (parseInt (element .attr ("imported-node-id"))); element .find ("> .item") .importNodePopover (importedNode .getInlineNode (), importedNode .getExportedName (), importedNode .getImportedName (), importedNode .getDescription ()); } removeImportedNode (id) { const element = $(`#${id}`), importedNode = this .objects .get (parseInt (element .attr ("imported-node-id"))); Editor .removeImportedNode (importedNode .getExecutionContext (), importedNode .getImportedName ()); } addImportedNodeClone (id) { const element = $(`#${id}`), importedNode = this .objects .get (parseInt (element .attr ("imported-node-id"))), executionContext = importedNode .getExecutionContext (); UndoManager .shared .beginUndo (_("Add Clone of Imported Node »%s«"), importedNode .getImportedName ()); Editor .appendValueToArray (executionContext, executionContext, executionContext .rootNodes, importedNode .getExportedNode ()); UndoManager .shared .endUndo (); } async cutNodes () { const primary = $(":is(.node, .proto, .externproto).primary"), selected = this .sceneGraph .find (":is(.node, .proto, .externproto).manually"), selection = selected .filter (primary) .length ? selected : primary, ids = selection .map (function () { return this .id }) .get (), elements = ids .map (id => $(`#${id}`)), nodes = elements .map (element => this .getNode ($(element))); UndoManager .shared .beginUndo (nodes .length === 1 ? _("Cut %d Node") : _("Cut %d Nodes"), nodes .length); await this .copyNodes (false); await this .deleteNodes (); UndoManager .shared .endUndo (); } async copyNodes () { const primary = $(":is(.node, .proto, .externproto, .imported-node.proxy).primary"), selected = this .sceneGraph .find (":is(.node, .proto, .externproto, .imported-node.proxy).manually"), selection = selected .filter (primary) .length ? selected : primary, ids = selection .map (function () { return this .id }) .get (), elements = ids .map (id => $(`#${id}`)), nodes = elements .map (element => this .getNode ($(element))), undoManager = new UndoManager (); undoManager .beginUndo (); for (const element of elements) { const node = this .getNode ($(element)); if (!node ?.getType () .some (type => this .transformLikeNodes .has (type))) continue; Editor .setMatrixWithCenter (node, this .getModelMatrix (element), undefined, undoManager); } undoManager .endUndo (); const x3dSyntax = await Editor .exportX3D (this .executionContext, nodes, { importedNodes: true }); //console .log (x3dSyntax) navigator .clipboard .writeText (x3dSyntax); undoManager .undo (); } async copyExternPrototype () { const elements = $(".proto.primary, .proto.manually"), protos = [... elements] .map (element => this .getNode ($(element))); const browser = this .executionContext .getBrowser (), scene = await browser .createScene (browser .getProfile ("Full")), worldURL = new URL (this .executionContext .worldURL), basename = path .basename (worldURL .pathname); scene .setMetaData ("base", this .executionContext .worldURL); for (const proto of protos) { const url = `${basename}#${proto .getName ()}`, externproto = new X3D .X3DExternProtoDeclaration (scene, new X3D .MFString (url)); scene .addExternProtoDeclaration (proto .getName (), externproto); } navigator .clipboard .writeText (scene .toXMLString ()); scene .dispose (); } async pasteNodes (id, executionContextId, nodeId, fieldId, deselect) { // try { // if there is a selected field or node, update nodeId and fieldId. const primary = deselect ? $("") : $(".node.primary"), executionContextElement = primary .closest (".scene", this .sceneGraph), executionContext = this .objects .get (executionContextId) ?? this .getNode (executionContextElement) ?? this .executionContext, targetNode = this .objects .get (nodeId) ?? this .getNode (primary), targetField = this .objects .get (fieldId) ?? this .getField (primary), numRootNodes = executionContext .rootNodes .length, x3dSyntax = await navigator .clipboard .readText (), destinationModelMatrix = this .getModelMatrix ($(`.node[node-id=${targetNode ?.getId ()}]`)); UndoManager .shared .beginUndo (_("Paste Nodes")); const nodes = await Editor .importX3D (executionContext, x3dSyntax); for (const node of nodes) { if (!node) continue; const containerField = $.try (() => node .getInnerNode () .getContainerField ()) ?? node .getContainerField (), field = targetField ?? $.try (() => targetNode ?.getField (containerField)); if (!field) continue; // Adjust matrix. if (node .getType () .some (type => this .transformLikeNodes .has (type))) { const sourceModelMatrix = node .getMatrix (), matrix = destinationModelMatrix .copy () .inverse () .multLeft (sourceModelMatrix); Editor .setMatrixWithCenter (node, matrix); } // Move node. switch (field .getType ()) { case X3D .X3DConstants .SFNode: { Editor .setFieldValue (executionContext, targetNode, field, node); Editor .removeValueFromArray (executionContext, executionContext, executionContext .rootNodes, numRootNodes); break; } case X3D .X3DConstants .MFNode: { Editor .insertValueIntoArray (executionContext, targetNode, field, field .length, node); Editor .removeValueFromArray (executionContext, executionContext, executionContext .rootNodes, numRootNodes); break; } } } UndoManager .shared .endUndo (); // Expand to pasted nodes and select them. await this .browser .nextFrame (); this .deselectAll (); for (const node of nodes .filter (node => node)) { this .expandTo (node); this .selectNodeElement ($(`.node[node-id="${node .getId ()}"]`), { add: true }); } } // catch (error) // { // // Catch "Document is not focused." from navigator.clipboard.readText. // console .error (`Paste failed: ${error .message}`); // } } deleteNodes () { const primary = $(".node.primary, .imported-node.proxy.primary"), selected = this .sceneGraph .find (".node.manually, .imported-node.proxy.manually"), selection = !primary .length || selected .filter (primary) .length ? selected : primary, ids = selection .map (function () { return this .id }) .get (); if (ids .length > 1) UndoManager .shared .beginUndo (_("Delete %s Nodes"), ids .length); else if (ids .length === 1) UndoManager .shared .beginUndo (_("Delete Node %s"), this .getNode ($(`#${ids [0]}`)) ?.getTypeName () ?? "NULL"); else return; for (const id of ids .reverse ()) { const element = $(`#${id}`), parentFieldElement = element .closest (".field, .scene", this .sceneGraph), parentNodeElement = parentFieldElement .closest (".node, .scene, .proto", this .sceneGraph), parentNode = this .getNode (parentNodeElement), parentField = parentFieldElement .hasClass ("scene") ? parentNode .rootNodes : this .getField (parentFieldElement), index = parseInt (element .attr ("index")), executionContextElement = element .closest (".scene", this .sceneGraph), executionContext = this .getNode (executionContextElement); switch (parentField .getType ()) { case X3D .X3DConstants .SFNode: Editor .setFieldValue (executionContext, parentNode, parentField, null); break; case X3D .X3DConstants .MFNode: Editor .removeValueFromArray (executionContext, parentNode, parentField, index); break; } } UndoManager .shared .endUndo (); } unlinkClone (id, executionContextId, nodeId) { const element = $(`#${id}`), executionContext = this .objects .get (executionContextId), parentFieldElement = element .closest (".field, .scene", this .sceneGraph), parentNodeElement = parentFieldElement .closest (".node, .proto, .scene", this .sceneGraph), parentNode = this .getNode (parentNodeElement), parentField = parentFieldElement .hasClass ("scene") ? parentNode .rootNodes : this .getField (parentFieldElement), node = this .objects .get (nodeId), copy = node .copy (executionContext), index = parseInt (element .attr ("index")); copy .setup (); UndoManager .shared .beginUndo (_("Unlink Clone")); if (node .getName ()) Editor .updateNamedNode (executionContext, executionContext .getUniqueName (node .getName ()), copy); switch (parentField .getType ()) { case X3D .X3DConstants .SFNode: { Editor .setFieldValue (executionContext, parentNode, parentField, copy); break; } case X3D .X3DConstants .MFNode: { Editor .removeValueFromArray (executionContext, parentNode, parentField, index); Editor .insertValueIntoArray (executionContext, parentNode, parentField, index, copy); break; } } UndoManager .shared .endUndo (); if (element .hasClass ("selected")) require ("../Application/Selection") .add (copy); } async addParentGroup (id, executionContextId, nodeId, component, typeName, fieldName) { const element = $(`#${id}`), executionContext = this .objects .get (executionContextId), childNode = this .objects .get (nodeId), childIndex = parseInt (element .attr ("index")), parentFieldElement = element .closest (".field, .scene", this .sceneGraph), parentNodeElement = parentFieldElement .closest (".node, .proto, .scene", this .sceneGraph), parentNode = this .getNode (parentNodeElement), parentField = parentFieldElement .hasClass ("scene") ? parentNode .rootNodes : this .getField (parentFieldElement); UndoManager .shared .beginUndo (_("Add Parent %s to Node %s"), typeName, childNode .getTypeName ()); await Editor .addComponent (executionContext, component); // Create new parent node. const node = executionContext .createNode (typeName) .getValue (), field = node .getField (fieldName); switch (typeName) { case "Switch": { node ._whichChoice = 0; break; } } // Add primary node to new parent node. if (field .getType () === X3D .X3DConstants .MFNode) Editor .insertValueIntoArray (executionContext, node, field, 0, childNode); else Editor .setFieldValue (executionContext, node, field, childNode); // Insert new parent node. switch (parentField .getType ()) { case X3D .X3DConstants .SFNode: Editor .setFieldValue (executionContext, parentNode, parentField, node); break; case X3D .X3DConstants .MFNode: Editor .insertValueIntoArray (executionContext, parentNode, parentField, childIndex, node); Editor .removeValueFromArray (executionContext, parentNode, parentField, childIndex + 1); break; } if (field .getType () === X3D .X3DConstants .MFNode) { const selectedNodes = Array .from (this .sceneGraph .find (":is(.node, .imported-node.proxy):is(.manually,.primary)"), e => this .getNode ($(e))), selectedElements = Array .from (this .sceneGraph .find (":is(.node, .imported-node.proxy).manually"), e => $(e)), destinationModelMatrix = this .getModelMatrix (parentNodeElement); // Add other selected nodes. const otherElements = selectedElements .filter (e => e [0] !== element [0]) .sort ((a, b) =