UNPKG

sunrize

Version:
1,232 lines (1,056 loc) 134 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)); electron .ipcRenderer .on ("transform-to-zero", () => this .transformToZero ()); electron .ipcRenderer .on ("remove-empty-groups", () => this .removeEmptyGroups ()); 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, ]); } transformToZero () { const selection = this .sceneGraph .find (".node.primary, .node.manually"), ids = selection .map (function () { return this .id }) .get (), nodes = ids .length ? ids .map (id => this .getNode ($(`#${id}`))) : this .executionContext .rootNodes; Editor .transformToZero (this .executionContext, nodes); } removeEmptyGroups () { const selection = this .sceneGraph .find (".node.primary, .node.manually"), ids = selection .map (function () { return this .id }) .get (), nodes = ids .length ? ids .map (id => this .getNode ($(`#${id}`))) : this .executionContext .rootNodes; Editor .removeEmptyGroups (this .executionContext, nodes); } 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 (".externproto, .proto, .proto-scene, .node, .field") && !element .is (".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"), 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); if (node) { var menu = [ { label: _("Rename Node..."), args: ["renameNode", 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"), args: ["cutNodes"], }, { label: _("Copy"), args: ["copyNodes"], }, { label: _("Paste"), args: ["pasteNodes", element .attr ("id"), executionContext .getId (), node .getId ()], }, { label: _("Delete"), 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 (node .getType () .includes (X3D .X3DConstants .X3DChildNode)) { 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: "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" }); } 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 ()], }); } 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 .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 .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 ()], }); 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 .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"), args: ["cutNodes"], }, { label: _("Copy"), args: ["copyNodes"], }, { label: _("Delete"), args: ["deleteNodes"], }, ]; } } else if (element .is (".exported-node")) { const exportedNode = this .objects .get (parseInt (element .attr ("exported-node-id"))); var menu = [ { label: _("Rename Exported Node..."), visible: exportedNode .getExecutionContext () === this .executionContext, args: ["renameExportedNode", 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 importedNode = this .objects .get (parseInt (element .attr ("imported-node-id"))); var menu = [ { label: _("Rename Imported Node..."), visible: importedNode .getExecutionContext () .getLocalScene () === this .executionContext, args: ["renameImportedNode", element .attr ("id")], }, { label: _("Remove Imported Node"), visible: importedNode .getExecutionContext () .getLocalScene () === this .executionContext, args: ["removeImportedNode", element .attr ("id")], }, ]; } 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: _("Rename Prototype..."), args: ["renamePrototype", 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 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"), args: ["pasteNodes", element .attr ("id"), executionContext .getId ()], }, ]; } else { return; } electron .ipcRenderer .send ("context-menu", "outline-editor", menu); } 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 (); } renameNode (id, executionContextId, nodeId) { require ("../Controls/RenameNodePopover"); const element = $(`#${id}`), node = this .objects .get (nodeId); element .find ("> .item") .renameNodePopover (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); } renameExportedNode (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 ()); } 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 ()); } renameImportedNode (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 ()); } removeImportedNode (id) { const element = $(`#${id}`), importedNode = this .objects .get (parseInt (element .attr ("imported-node-id"))); Editor .removeImportedNode (importedNode .getExecutionContext (), importedNode .getImportedName ()); } async cutNodes () { const primary = $(".node.primary, .proto.primary, .externproto.primary"), selected = this .sceneGraph .find (".node.manually, .proto.manually, .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 (); await this .deleteNodes (); UndoManager .shared .endUndo (); } async copyNodes (deselect) { const primary = $(".node.primary, .proto.primary, .externproto.primary"), selected = this .sceneGraph .find (".node.manually, .proto.manually, .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 = 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 (); if (deselect) this .deselectAll (); } 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) { // try { // if there is a selected field or node, update nodeId and fieldId. const primary = $(".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 = nodeId !== undefined ? this .getModelMatrix ($(`.node[node-id=${nodeId}]`)) : new X3D .Matrix4 (); UndoManager .shared .beginUndo (_("Paste Nodes")); const nodes = await Editor .importX3D (executionContext, x3dSyntax); for (const node of nodes) { 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 (); await this .browser .nextFrame (); for (const node of nodes) this .expandTo (node); } // catch (error) // { // // Catch "Document is not focused." from navigator.clipboard.readText. // console .error (`Paste failed: ${error .message}`); // } } deleteNodes () { const primary = $(".node.primary"), selected = this .sceneGraph .find (".node.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; const nodes = [ ]; for (const id of ids .reverse ()) { const element = $(`#${id}`), node = this .getNode (element), 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; } nodes .push (node); } Editor .removeNodesFromExecutionContextIfNecessary (this .executionContext, nodes); 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 (".node.manually,.node.primary"), e => this .getNode ($(e))), selectedElements = Array .from (this .sceneGraph .find (".node.manually"), e => $(e)), destinationModelMatrix = this .getModelMatrix (parentNodeElement); // Add other selected nodes. const otherElements = selectedElements .filter (e => e [0] !== element [0]) .sort ((a, b) => b .attr ("index") - a .attr ("index")); for (const otherElement of otherElements) { const otherChildNode = this .getNode (otherElement), otherChildIndex = parseInt (otherElement .attr ("index")), otherParentFieldElement = otherElement .closest (".field, .scene", this .sceneGraph), otherParentNodeElement = otherParentFieldElement .closest (".node, .proto, .scene", this .sceneGraph), otherParentNode = this .getNode (otherParentNodeElement), otherParentField = otherParentFieldElement .hasClass ("scene") ? otherParentNode .rootNodes : this .getField (otherParentFieldElement); // Adjust matrix. if (otherParentField !== parentField) { if (otherChildNode .getType () .some (type => this .transformLikeNodes .has (type))) { const sourceModelMatrix = this .getModelMatrix (otherElement), matrix = destinationModelMatrix .copy () .inverse () .multLeft (sourceModelMatrix); Editor .setMatrixWithCenter (otherChildNode, matrix); } } // Move node. Editor .insertValueIntoArray (executionContext, node, field, 1, otherChildNode); switch (otherParentField .getType ()) { case X3D .X3DConstants .SFNode: Editor .setFieldValue (executionContext, otherParentNode, otherParentField, null); break; case X3D .X3DConstants .MFNode: Editor .removeValueFromArray (executionContext, otherParentNode, otherParentField, otherChildIndex); break; } } // Reorder nodes. Editor .setFieldValue (executionContext, node, field, selectedNodes); } UndoManager .shared .endUndo (); await this .browser .nextFrame (); this .expandTo (node, { expandObject: true }); const groupElement = this .sceneGraph .find (`.node[node-id=${node .getId ()}]`); this .selectNodeElement (groupElement, { target: true }); } removeParent (id, executionContextId, nodeId) { const element = $(`#${id}`), executionContext = this .objects .get (executionContextId), childNode = this .objects .get (nodeId), 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), parentIndex = parseInt (parentNodeElement .attr ("index")), parent2FieldElement = parentNodeElement .closest (".field, .scene", this .sceneGraph), parent2NodeElement = parent2FieldElement .closest (".node, .proto, .scene", this .sceneGraph), parent2Node = this .getNode (parent2NodeElement), parent2Field = parent2FieldElement .hasClass ("scene") ? parent2Node .rootNodes : this .getField (parent2FieldElement); UndoManager .shared .beginUndo (_("Remove Parent of %s"), childNode .getTypeName ()); if (parent2Field instanceof X3D .X3DArrayField) { if (parentF