sunrize
Version:
A Multi-Platform X3D Editor
1,232 lines (1,056 loc) • 134 kB
JavaScript
"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