sunrize
Version:
A Multi-Platform X3D Editor
1,619 lines (1,258 loc) • 118 kB
JavaScript
"use strict"
const
$ = require ("jquery"),
path = require ("path"),
url = require ("url"),
fs = require ("fs"),
zlib = require ("zlib"),
X3D = require ("../X3D"),
Traverse = require ("x3d-traverse") (X3D),
UndoManager = require ("./UndoManager"),
_ = require ("../Application/GetText")
module .exports = class Editor
{
/**
* X3D Id
*/
static Id = /(?:^[^\x30-\x39\x00-\x20\x22\x23\x27\x2b\x2c\x2d\x2e\x5b\x5c\x5d\x7b\x7d\x7f]{1}[^\x00-\x20\x22\x23\x27\x2c\x2e\x5b\x5c\x5d\x7b\x7d\x7f]*$|^$)/
/**
*
* @param {X3DExecutionContext} executionContext source execution context
* @param {string} filePath file path
* @returns {string} URI encoded relative path
*/
static relativePath (executionContext, filePath)
{
try
{
return encodeURI (path .relative (path .dirname (url .fileURLToPath (executionContext .getWorldURL ())), filePath));
}
catch
{
return url .pathToFileURL (filePath);
}
}
static #specialChars = new Map ("\t\n\r \"[]{}" .split ("") .map (c => [encodeURIComponent (c), c]));
static #specialCharsRegExp = new RegExp ([... this .#specialChars .keys ()] .join ("|"), "ig");
static decodeURI (uri)
{
if (uri .match (/^\s*(?:ecmascript|javascript|vrmlscript):/s))
return uri;
return $.try (() => decodeURI (uri)) ?? uri;
}
static encodeURI (uri)
{
if (uri .match (/^\s*(?:ecmascript|javascript|vrmlscript):/s))
return uri;
if (uri .match (/^\s*data:/s))
return encodeURI (uri) .replace (this .#specialCharsRegExp, c => this .#specialChars .get (c .toUpperCase ()));
return encodeURI (uri);
}
/**
*
* @param {X3DExecutionContext} executionContext source execution context
* @param {Array<X3DNode|X3DExternProtoDeclaration|X3DProtoDeclaration>} objects objects to export
* @param {Object} options
* @returns {string} x3dSyntax
*/
static async exportX3D (executionContext, objects = [ ], { type = "x3d", importedNodes = false, exportedNodes = false } = { })
{
const
externprotos = new Set (),
protos = new Set (),
nodes = new X3D .MFNode (... objects .filter (o => o ?.getType () .includes (X3D .X3DConstants .X3DNode) ?? true));
const
browser = executionContext .getBrowser (),
scene = await browser .createScene (browser .getProfile ("Core"));
// Determine protos.
const protoNodes = new Set ()
for (const object of Traverse .traverse (objects, Traverse .PROTO_DECLARATIONS | Traverse .PROTO_DECLARATION_BODY | Traverse .ROOT_NODES | Traverse .PROTOTYPE_INSTANCES))
{
if (object instanceof X3D .X3DProtoDeclarationNode)
{
protoNodes .add (object);
}
else if (object instanceof X3D .SFNode)
{
const node = object .getValue ();
if (node .getType () .includes (X3D .X3DConstants .X3DPrototypeInstance))
protoNodes .add (node .getProtoNode ());
}
}
for (const protoNode of protoNodes)
{
if (protoNode .getExecutionContext () === executionContext)
continue;
if (objects .includes (protoNode))
continue;
protoNodes .delete (protoNode);
}
for (const protoNode of protoNodes)
{
if (protoNode .isExternProto)
externprotos .add (protoNode);
else
protos .add (protoNode);
}
// Determine components, imported nodes and routes.
const
componentNames = new Set (),
children = new Set (),
childRoutes = new Set (),
inlineNodes = new Set ();
for (const object of nodes .traverse (Traverse .ROOT_NODES))
{
const node = object .getValue ();
componentNames .add (node .getComponentInfo () .name);
children .add (node .valueOf ());
for (const field of node .getFields ())
{
for (const route of field .getInputRoutes ())
childRoutes .add (route);
for (const route of field .getOutputRoutes ())
childRoutes .add (route);
}
if (node .getType () .includes (X3D .X3DConstants .Inline))
inlineNodes .add (node .valueOf ());
}
// Add exported nodes.
if (exportedNodes && (executionContext instanceof X3D .X3DScene))
{
for (const exportedNode of executionContext .exportedNodes)
{
if (!children .has (exportedNode .getLocalNode () .valueOf ()))
continue;
scene .exportedNodes .add (exportedNode .getExportedName (), exportedNode);
}
}
// Add imported nodes.
if (importedNodes)
{
for (const importedNode of executionContext .importedNodes)
{
if (!inlineNodes .has (importedNode .getInlineNode () .valueOf ()))
continue;
children .add (importedNode);
scene .importedNodes .add (importedNode .getImportedName (), importedNode);
}
}
// Filter out routes.
const routes = [... childRoutes] .filter (route => children .has (route .getSourceNode () .valueOf ()) && children .has (route .getDestinationNode () .valueOf ()));
// Store world url.
if (!executionContext .worldURL .startsWith ("data:"))
scene .setMetaData ("base", executionContext .worldURL);
// Add protos.
for (const externproto of externprotos)
scene .externprotos .add (externproto .getName (), externproto);
for (const proto of protos)
scene .protos .add (proto .getName (), proto);
// Set profile and components.
scene .setProfile (browser .getProfile ("Core"));
for (const name of componentNames)
scene .addComponent (browser .getComponent (name));
// Add nodes.
scene .rootNodes = nodes;
// Add routes.
for (const route of routes)
scene .routes .add (route .getRouteId (), route);
// Return XML string.
this .inferProfileAndComponents (scene, new UndoManager ());
const x3dSyntax = this .getContents (scene, type);
// Dispose scene.
scene .routes .clear ();
scene .dispose ();
nodes .dispose ();
return x3dSyntax;
}
/**
*
* @param {X3DExecutionContext} executionContext
* @param {string} x3dSyntax
* @param {UndoManager} undoManager
* @returns {Promise<Array<X3DNode>>}
*/
static async importX3D (executionContext, x3dSyntax, undoManager = UndoManager .shared)
{
// Parse string.
const
browser = executionContext .getBrowser (),
scene = executionContext .getLocalScene (),
profile = scene .getProfile (),
x_ite = scene .hasComponent ("X_ITE"),
externprotos = new Map (Array .from (executionContext .externprotos, p => [p .getName (), p])),
protos = new Map (Array .from (executionContext .protos, p => [p .getName (), p])),
rootNodes = executionContext .rootNodes .copy (),
tempScene = await browser .createScene (browser .getProfile ("Core"));
scene .setProfile (browser .getProfile ("Full"));
scene .updateComponent (browser .getComponent ("X_ITE"));
try
{
const parser = new X3D .GoldenGate (tempScene);
parser .pushExecutionContext (executionContext);
await new Promise ((resolve, reject) => parser .parseIntoScene (x3dSyntax, resolve, reject));
}
catch (error)
{
console .error (error);
return [ ];
}
finally
{
// Restore profile and components.
scene .setProfile (profile);
if (!x_ite)
scene .removeComponent ("X_ITE");
}
// Undo.
undoManager .beginUndo (_("Import X3D"));
undoManager .registerUndo (() =>
{
// Restore ExternProtos.
this .setExternProtoDeclarations (executionContext, externprotos, undoManager);
// Restore Protos.
this .setProtoDeclarations (executionContext, protos, undoManager);
// Restore Root Nodes.
this .setFieldValue (executionContext, executionContext, executionContext .rootNodes, rootNodes, undoManager);
});
// Add components.
await Promise .all ([... tempScene .getProfile () .components, ... tempScene .getComponents ()]
.map (component => this .addComponent (scene, component, undoManager)));
// Remove protos that already exists in context.
const
nodes = [... executionContext .rootNodes] .slice (rootNodes .length) .map (n => n ?.getValue ()),
newExternProtos = [... executionContext .externprotos] .slice (externprotos .size),
newProtos = [... executionContext .protos] .slice (protos .size),
updatedExternProtos = new Map (),
updatedProtos = new Map (),
removedProtoNodes = new Set ();
for (const object of Traverse .traverse ([... newProtos, ... nodes], Traverse .PROTO_DECLARATIONS | Traverse .PROTO_DECLARATION_BODY | Traverse .ROOT_NODES))
{
if (!(object instanceof X3D .SFNode))
continue;
const node = object .getValue ();
if (!node .getType () .includes (X3D .X3DConstants .X3DPrototypeInstance))
continue;
if (node .getProtoNode () .getExecutionContext () !== executionContext)
continue;
const proto = protos .get (node .getTypeName ());
if (proto)
{
updatedProtos .set (node .getTypeName (), proto);
this .setProtoNode (executionContext, node, proto, undoManager);
continue;
}
const externproto = externprotos .get (node .getTypeName ());
if (externproto)
{
updatedExternProtos .set (node .getTypeName (), externproto);
this .setProtoNode (executionContext, node, externproto, undoManager);
continue;
}
const available = this .getNextAvailableProtoNode (executionContext, node .getTypeName ());
if (available)
{
removedProtoNodes .add (node .getProtoNode ());
this .setProtoNode (executionContext, node, available, undoManager);
continue;
}
}
for (const [name, externproto] of updatedExternProtos)
this .updateExternProtoDeclaration (executionContext, name, externproto, undoManager);
for (const [name, proto] of updatedProtos)
this .updateProtoDeclaration (executionContext, name, proto, undoManager);
for (const protoNode of removedProtoNodes)
{
if (protoNode .isExternProto)
this .removeExternProtoDeclaration (executionContext, protoNode .getName (), undoManager);
else
this .removeProtoDeclaration (executionContext, protoNode .getName (), undoManager);
}
const oldWorldURL = tempScene .getMetaData ("base");
if (oldWorldURL)
{
for (const objects of [newExternProtos, newProtos, nodes])
this .rewriteURLs (executionContext, objects, oldWorldURL [0], executionContext .worldURL, new UndoManager ());
}
// Add exported nodes.
if (executionContext instanceof X3D .X3DScene)
{
for (const exportedNode of tempScene .exportedNodes)
{
this .updateExportedNode (executionContext, executionContext .getUniqueExportName (exportedNode .getExportedName ()), "", exportedNode .getLocalNode (), undoManager);
}
}
tempScene .dispose ();
this .requestUpdateInstances (executionContext, undoManager);
undoManager .endUndo ();
return nodes;
}
/**
*
* @param {X3DExecutionContext} executionContext
* @param {Array<X3DNode>} nodes
* @param {string} filePath
* @param {UndoManager} undoManager
* @returns {Promise<void>}
*/
static async convertNodesToInlineFile (executionContext, nodes, filePath)
{
const
browser = executionContext .getBrowser (),
scene = await browser .createScene (browser .getProfile ("Core")),
x3dSyntax = await this .exportX3D (executionContext, nodes, { importedNodes: true, exportedNodes: true }),
loadUrlObjects = browser .getBrowserOption ("LoadUrlObjects");
browser .setBrowserOption ("LoadUrlObjects", false);
scene .setWorldURL (url .pathToFileURL (filePath));
await this .importX3D (scene, x3dSyntax, new UndoManager ());
this .rewriteURLs (scene, scene, executionContext .worldURL, scene .worldURL, new UndoManager ());
this .inferProfileAndComponents (scene, new UndoManager ());
fs .writeFileSync (filePath, this .getContents (scene, path .extname (filePath)));
for (const object of scene .traverse (Traverse .ROOT_NODES | Traverse .PROTOTYPE_INSTANCES))
object .dispose ();
browser .setBrowserOption ("LoadUrlObjects", loadUrlObjects);
}
/**
*
* @param {X3DScene} scene a scene
* @param {string=} type default is ".x3d"
* @returns {string}
*/
static getContents (scene, type)
{
switch (type ?.toLowerCase ())
{
case ".x3d":
default:
return scene .toXMLString ()
case ".x3dz":
return zlib .gzipSync (scene .toXMLString ({ style: "CLEAN" }))
case ".x3dv":
return scene .toVRMLString ()
case ".x3dvz":
return zlib .gzipSync (scene .toVRMLString ({ style: "CLEAN" }))
case ".x3dj":
return scene .toJSONString ()
case ".x3djz":
return zlib .gzipSync (scene .toJSONString ({ style: "CLEAN" }))
case ".html":
return this .getHTML (scene)
}
}
/**
*
* @param {X3DScene} scene a scene
* @returns {string}
*/
static getHTML (scene)
{
return /* html */ `<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script src="https://cdn.jsdelivr.net/npm/x_ite@latest/dist/x_ite.min.js"></script>
<style>
body {
background-color: rgb(21, 22, 24);
color: rgb(108, 110, 113);
}
a {
color: rgb(106, 140, 191);
}
x3d-canvas {
width: 768px;
height: 432px;
}
</style>
</head>
<body>
<h1>${path .basename (url .fileURLToPath (scene .worldURL))}</h1>
<x3d-canvas>
${scene .toXMLString ({ html: true, indent: " " .repeat (6) }) .trimEnd () }
</x3d-canvas>
<p>Made with <a href="https://create3000.github.io/sunrize/" target="_blank">Sunrize X3D Editor.</a></p>
</body>
</html>`
}
static absoluteURL = new RegExp ("^(?:[a-z]+:|//)", "i")
static fontFamilies = new Set (["SERIF", "SANS", "TYPEWRITER"])
/**
*
* @param {X3DExecutionContext} executionContext
* @param {X3DExecutionContext|Array<X3DNode|X3DProtoDeclaration>} objects
* @param {string} oldWorldURL
* @param {string} newWorldURL
* @param {UndoManager} undoManager
*/
static rewriteURLs (executionContext, objects, oldWorldURL, newWorldURL, undoManager = UndoManager .shared)
{
undoManager .beginUndo (_("Rewrite URLs"))
for (const object of Traverse .traverse (objects, Traverse .EXTERNPROTO_DECLARATIONS | Traverse .PROTO_DECLARATIONS | Traverse .PROTO_DECLARATION_BODY | Traverse .ROOT_NODES))
{
const
node = object instanceof X3D .SFNode ? object .getValue () : object,
urlObject = node .getType () .includes (X3D .X3DConstants .X3DUrlObject),
fontStyleNode = node .getType () .includes (X3D .X3DConstants .X3DFontStyleNode);
if (!(urlObject || fontStyleNode))
continue;
const newURL = new X3D .MFString ();
for (const fileURL of node ._url)
{
if (fontStyleNode && this .fontFamilies .has (fileURL))
{
newURL .push (fileURL);
continue;
}
else if (this .absoluteURL .test (fileURL))
{
try
{
const filePath = url .fileURLToPath (new URL (fileURL, oldWorldURL));
let relativePath = path .relative (path .dirname (url .fileURLToPath (newWorldURL)), filePath);
relativePath += new URL (fileURL) .search;
relativePath += new URL (fileURL) .hash;
// Add new relative file URL.
newURL .push (encodeURI (relativePath));
continue;
}
catch (error)
{
// console .log (error)
}
newURL .push (fileURL);
}
else
{
try
{
const
filePath = path .resolve (path .dirname (url .fileURLToPath (oldWorldURL)), fileURL),
relativePath = path .relative (path .dirname (url .fileURLToPath (newWorldURL)), filePath);
// Add new relative file URL.
newURL .push (encodeURI (relativePath));
continue;
}
catch
{ }
try
{
// Add absolute URL.
newURL .push (new URL (fileURL, oldWorldURL));
continue;
}
catch
{ }
// Fallback, use original URL.
newURL .push (fileURL);
}
}
const uniqueURL = new X3D .MFString (... new Set (newURL));
this .setFieldValue (executionContext, node, node ._url, uniqueURL, undoManager);
}
undoManager .endUndo ();
}
/**
*
* @param {X3DExecutionContext} parentContext
* @param {X3DExecutionContext} childContext
* @returns {boolean}
*/
static isParentContext (parentContext, childContext)
{
if (parentContext === childContext)
return false
let executionContext = childContext
while (!(executionContext instanceof X3D .X3DScene))
{
if (executionContext === parentContext)
return true
executionContext = executionContext .getExecutionContext ()
}
return executionContext === parentContext
}
/**
*
* @param {X3DScene} scene
* @param {UndoManager} undoManager
*/
static inferProfileAndComponents (scene, undoManager = UndoManager .shared)
{
const
browser = scene .getBrowser (),
usedComponents = this .getUsedComponents (scene),
profileAndComponents = this .getProfileAndComponentsFromUsedComponents (browser, usedComponents);
undoManager .beginUndo (_("Infer Profile and Components from Source"));
this .setProfile (scene, profileAndComponents .profile, undoManager);
this .setComponents (scene, profileAndComponents .components, undoManager);
undoManager .endUndo ();
}
/**
*
* @param {X3DScene} scene
* @returns {Array<ComponentInfo>}
*/
static getUsedComponents (scene)
{
const components = new Set ();
for (const object of scene .traverse (Traverse .PROTO_DECLARATIONS | Traverse .PROTO_DECLARATION_BODY | Traverse .ROOT_NODES | Traverse .PROTOTYPE_INSTANCES))
{
if (!(object instanceof X3D .SFNode))
continue;
const node = object .getValue ();
components .add (node .getComponentInfo () .name);
}
return components;
}
static getProfileAndComponentsFromUsedComponents (browser, usedComponents)
{
const profiles = ["Interactive", "Interchange", "Immersive"] .map (name =>
{
return { profile: browser .getProfile (name), components: new Set (usedComponents) };
});
profiles .forEach (object =>
{
for (const component of object .profile .components)
object .components .delete (component .name);
});
const min = profiles .reduce ((min, object) =>
{
const count = object .profile .components .length + object .components .size;
return min .count < count ? min : {
count: count,
object: object,
};
},
{ count: Number .POSITIVE_INFINITY });
return {
profile: min .object .profile,
components: Array .from (min .object .components) .sort () .map (name => browser .getSupportedComponents () .get (name)),
};
}
/**
*
* @param {X3DScene} scene
* @param {ProfileInfo} profile
* @param {UndoManager} undoManager
*/
static setProfile (scene, profile, undoManager = UndoManager .shared)
{
const oldProfile = scene .getProfile ();
if ((profile && oldProfile && profile .name === oldProfile .name) || (profile === oldProfile))
return;
const browser = scene .getBrowser ();
undoManager .beginUndo (_("Set Profile to »%s«"), profile ? profile .title : "Full");
scene .setProfile (profile);
undoManager .registerUndo (() =>
{
this .setProfile (scene, oldProfile, undoManager);
});
undoManager .endUndo ();
}
/**
*
* @param {X3DScene} scene
* @param {Array<ComponentInfo>} components
* @param {UndoManager} undoManager
*/
static setComponents (scene, components, undoManager = UndoManager .shared)
{
const
browser = scene .getBrowser (),
oldComponents = Array .from (scene .getComponents ());
undoManager .beginUndo (_("Set Components of Scene"));
for (const { name } of oldComponents)
scene .removeComponent (name);
for (const component of components)
scene .addComponent (component);
undoManager .registerUndo (() =>
{
this .setComponents (scene, oldComponents, undoManager);
});
undoManager .endUndo ();
}
/**
*
* @param {X3DExecutionContext} scene
* @param {string|ComponentInfo} name
* @param {UndoManager} undoManager
*/
static addComponent (scene, name, undoManager = UndoManager .shared)
{
scene = scene .getLocalScene ();
name = name instanceof X3D .ComponentInfo ? name .name : name;
if (scene .hasComponent (name))
return;
const browser = scene .getBrowser ();
undoManager .beginUndo (_("Add Component %s"), name);
scene .addComponent (browser .getComponent (name));
undoManager .registerUndo (() =>
{
this .removeComponent (scene, name, undoManager);
});
undoManager .endUndo ();
}
/**
*
* @param {X3DExecutionContext} scene
* @param {string|ComponentInfo} name
* @param {UndoManager} undoManager
*/
static removeComponent (scene, name, undoManager = UndoManager .shared)
{
scene = scene .getLocalScene ();
name = name instanceof X3D .ComponentInfo ? name .name : name;
if (!scene .hasComponent (name))
return;
undoManager .beginUndo (_("Remove Component %s"), name);
scene .removeComponent (name);
undoManager .registerUndo (() =>
{
this .addComponent (scene, name, undoManager);
});
undoManager .endUndo ();
}
/**
*
* @param {X3DScene} scene
* @param {string} category
* @param {string} name
* @param {number} conversionFactor
* @param {UndoManager} undoManager
*/
static updateUnit (scene, category, name, conversionFactor, undoManager = UndoManager .shared)
{
const
unit = scene .getUnit (category),
oldName = unit .name,
oldConversionFactor = unit .conversionFactor
undoManager .beginUndo (_("Update Unit Category »%s«"), category)
scene .updateUnit (category, name, conversionFactor)
undoManager .registerUndo (() =>
{
this .updateUnit (scene, category, oldName, oldConversionFactor, undoManager)
});
undoManager .endUndo ();
}
/**
*
* @param {X3DScene} scene
* @param {Array<[string,string]>} entries
* @param {UndoManager} undoManager
*/
static setMetaData (scene, entries, undoManager = UndoManager .shared)
{
const oldEntries = [ ];
for (const [key, values] of scene .getMetaDatas ())
{
for (const value of values)
oldEntries .push ([key, value]);
}
undoManager .beginUndo (_("Change Meta Data"));
for (const key of Array .from (scene .getMetaDatas () .keys ()))
scene .removeMetaData (key);
for (const [key, value] of entries)
scene .addMetaData (key, value);
undoManager .registerUndo (() =>
{
this .setMetaData (scene, oldEntries, undoManager);
});
undoManager .endUndo ();
}
/**
*
* @param {X3DExecutionContext} executionContext
* @param {string} name
* @param {X3DNode} node
* @param {UndoManager} undoManager
*/
static updateNamedNode (executionContext, name, node, undoManager = UndoManager .shared)
{
node = node .valueOf ();
const
oldNode = $.try (() => executionContext .getNamedNode (name)),
oldName = node .getName ();
undoManager .beginUndo (_("Rename Node to »%s«"), name);
executionContext .updateNamedNode (name, node);
undoManager .registerUndo (() =>
{
if (oldNode)
this .updateNamedNode (executionContext, name, oldNode, undoManager);
if (oldName)
this .updateNamedNode (executionContext, oldName, node, undoManager);
else
this .removeNamedNode (executionContext, node, undoManager);
});
this .requestUpdateInstances (executionContext, undoManager);
undoManager .endUndo ();
}
/**
*
* @param {X3DExecutionContext} executionContext
* @param {X3DNode} node
* @param {UndoManager} undoManager
*/
static removeNamedNode (executionContext, node, undoManager = UndoManager .shared)
{
node = node .valueOf ();
const oldName = node .getName ();
undoManager .beginUndo (_("Remove Node Name »%s«"), oldName);
executionContext .removeNamedNode (oldName);
undoManager .registerUndo (() =>
{
if (oldName)
this .updateNamedNode (executionContext, oldName, node, undoManager);
});
this .requestUpdateInstances (executionContext, undoManager);
undoManager .endUndo ();
}
/**
*
* @param {X3DExecutionContext} executionContext
* @param {X3DNode} inlineNode
* @param {string} exportedName
* @param {string} importedName
* @param {UndoManager} undoManager
*/
static updateImportedNode (executionContext, inlineNode, exportedName, importedName, oldImportedName, undoManager = UndoManager .shared)
{
inlineNode = inlineNode .valueOf ();
undoManager .beginUndo (_("Update Imported Node »%s«"), importedName);
executionContext .updateImportedNode (inlineNode .valueOf (), exportedName, importedName);
if (oldImportedName && oldImportedName !== importedName)
{
const
oldImportedNode = executionContext .getImportedNodes () .get (oldImportedName),
newImportedNode = executionContext .getImportedNodes () .get (importedName);
const routes = executionContext .getRoutes () .filter (route =>
{
if (route .sourceNode === oldImportedNode)
return true;
if (route .destinationNode === oldImportedNode)
return true;
return false;
});
executionContext .removeImportedNode (oldImportedName);
for (let { sourceNode, sourceField, destinationNode, destinationField } of routes)
{
if (sourceNode === oldImportedNode)
sourceNode = newImportedNode;
if (destinationNode === oldImportedNode)
destinationNode = newImportedNode;
executionContext .addRoute (sourceNode, sourceField, destinationNode, destinationField);
}
}
undoManager .registerUndo (() =>
{
if (oldImportedName)
this .updateImportedNode (executionContext, inlineNode, exportedName, oldImportedName, importedName, undoManager);
else
this .removeImportedNode (executionContext, importedName, undoManager);
});
this .requestUpdateInstances (executionContext, undoManager);
undoManager .endUndo ();
}
/**
*
* @param {X3DExecutionContext} executionContext
* @param {string} importedName
* @param {UndoManager} undoManager
*/
static removeImportedNode (executionContext, importedName, undoManager = UndoManager .shared)
{
const
importedNode = executionContext .getImportedNodes () .get (importedName),
inlineNode = importedNode .getInlineNode (),
exportedName = importedNode .getExportedName ();
const routes = executionContext .getRoutes () .filter (route =>
{
if (route .sourceNode === importedNode)
return true;
if (route .destinationNode === importedNode)
return true;
return false;
});
undoManager .beginUndo (_("Remove Imported Node »%s«"), importedName);
executionContext .removeImportedNode (importedName);
undoManager .registerUndo (() =>
{
this .updateImportedNode (executionContext, inlineNode, exportedName, importedName, "", undoManager);
const newImportedNode = executionContext .getImportedNodes () .get (importedName);
for (let { sourceNode, sourceField, destinationNode, destinationField } of routes)
{
if (sourceNode === importedNode)
sourceNode = newImportedNode;
if (destinationNode === importedNode)
destinationNode = newImportedNode;
executionContext .addRoute (sourceNode, sourceField, destinationNode, destinationField);
}
});
this .requestUpdateInstances (executionContext, undoManager);
undoManager .endUndo ();
}
/**
*
* @param {X3DScene} scene
* @param {string} exportedName
* @param {X3DNode} node
* @param {UndoManager} undoManager
*/
static updateExportedNode (scene, exportedName, oldExportedName, node, undoManager = UndoManager .shared)
{
node = node .valueOf ();
undoManager .beginUndo (_("Update Exported Node »%s«"), exportedName);
if (oldExportedName)
scene .removeExportedNode (oldExportedName);
scene .updateExportedNode (exportedName, node);
undoManager .registerUndo (() =>
{
if (oldExportedName)
this .updateExportedNode (scene, oldExportedName, exportedName, node, undoManager);
else
this .removeExportedNode (scene, exportedName, undoManager);
});
this .requestUpdateInstances (scene, undoManager);
undoManager .endUndo ();
}
/**
*
* @param {X3DScene} scene
* @param {string} exportedName
* @param {UndoManager} undoManager
*/
static removeExportedNode (scene, exportedName, undoManager = UndoManager .shared)
{
const
exportedNode = scene .getExportedNodes () .get (exportedName),
node = exportedNode .getLocalNode ();
undoManager .beginUndo (_("Remove Exported Node »%s«"), exportedName);
scene .removeExportedNode (exportedName);
undoManager .registerUndo (() =>
{
this .updateExportedNode (scene, exportedName, "", node, undoManager);
});
this .requestUpdateInstances (scene, undoManager);
undoManager .endUndo ();
}
/**
*
* @param {X3DExecutionContext} executionContext
* @param {string} name
* @param {UndoManager} undoManager
* @returns {X3DProtoDeclaration}
*/
static addProtoDeclaration (executionContext, name, undoManager = UndoManager .shared)
{
const
oldProtos = new Map (Array .from (executionContext .protos, p => [p .getName (), p])),
proto = new X3D .X3DProtoDeclaration (executionContext)
undoManager .beginUndo (_("Add Proto Declaration »%s«"), name)
proto .setup ()
executionContext .updateProtoDeclaration (name, proto)
undoManager .registerUndo (() =>
{
this .setProtoDeclarations (executionContext, oldProtos, undoManager)
});
this .requestUpdateInstances (executionContext, undoManager)
undoManager .endUndo ();
return proto
}
/**
*
* @param {X3DExecutionContext} executionContext
* @param {string} name
* @param {X3DProtoDeclaration} proto
* @param {UndoManager} undoManager
*/
static updateProtoDeclaration (executionContext, name, proto, undoManager = UndoManager .shared)
{
const oldName = proto .getName ()
undoManager .beginUndo (_("Update Proto Declaration »%s«"), name)
executionContext .updateProtoDeclaration (name, proto)
undoManager .registerUndo (() =>
{
if (oldName)
this .updateProtoDeclaration (executionContext, oldName, proto, undoManager)
else
this .removeProtoDeclaration (executionContext, name, undoManager)
});
this .requestUpdateInstances (executionContext, undoManager)
undoManager .endUndo ();
}
/**
*
* @param {X3DExecutionContext} executionContext
* @param {string} name
* @param {UndoManager} undoManager
*/
static removeProtoDeclaration (executionContext, name, undoManager = UndoManager .shared)
{
const oldProtos = new Map (Array .from (executionContext .protos, p => [p .getName (), p]))
undoManager .beginUndo (_("Remove Proto Declaration »%s«"), name)
executionContext .removeProtoDeclaration (name)
undoManager .registerUndo (() =>
{
this .setProtoDeclarations (executionContext, oldProtos, undoManager)
});
this .requestUpdateInstances (executionContext, undoManager)
undoManager .endUndo ();
}
/**
*
* @param {X3DExecutionContext} executionContext
* @param {Array<X3DProtoDeclaration} protos
* @param {UndoManager} undoManager
*/
static setProtoDeclarations (executionContext, protos, undoManager = UndoManager .shared)
{
const oldProtos = new Map (Array .from (executionContext .protos, p => [p .getName (), p]))
undoManager .beginUndo (_("Update Proto Declarations"))
for (const name of oldProtos .keys ())
executionContext .removeProtoDeclaration (name)
if (Array .isArray (protos))
{
for (const proto of protos)
executionContext .updateProtoDeclaration (proto .getName (), proto)
}
else
{
for (const [name, proto] of protos)
executionContext .updateProtoDeclaration (name, proto)
}
undoManager .registerUndo (() =>
{
this .setProtoDeclarations (executionContext, oldProtos, undoManager)
});
this .requestUpdateInstances (executionContext, undoManager)
undoManager .endUndo ();
}
/**
*
* @param {X3DExecutionContext} executionContext
* @param {X3DProtoDeclaration} proto
* @param {string} filePath
* @param {UndoManager} undoManager
* @returns {Promise<void>}
*/
static async turnIntoExternProto (executionContext, proto, filePath, undoManager = UndoManager .shared)
{
const
browser = executionContext .getBrowser (),
scene = await browser .createScene (browser .getProfile ("Core")),
x3dSyntax = await this .exportX3D (executionContext, [proto]);
undoManager .beginUndo (_("Turn Prototype »%s« into Extern Prototype"), proto .getName ());
this .removeProtoDeclaration (executionContext, proto .getName (), undoManager);
scene .setWorldURL (url .pathToFileURL (filePath));
await this .importX3D (scene, x3dSyntax, new UndoManager ());
this .rewriteURLs (scene, scene, executionContext .worldURL, scene .worldURL, new UndoManager ());
this .inferProfileAndComponents (scene, new UndoManager ());
fs .writeFileSync (filePath, this .getContents (scene, path .extname (filePath)));
scene .dispose ();
const
name = executionContext .getUniqueExternProtoName (proto .getName ()),
externproto = this .addExternProtoDeclaration (executionContext, name, undoManager),
relativePath = this .relativePath (executionContext, filePath),
absolutePath = url .pathToFileURL (filePath) .href,
hash = "#" + encodeURIComponent (proto .getName ());
externproto ._url = new X3D .MFString (relativePath + hash, absolutePath + hash);
this .replaceProtoNodes (executionContext, proto, externproto, undoManager);
undoManager .endUndo ();
}
/**
*
* @param {X3DProtoDeclaration} proto
* @param {X3DProtoDeclaration} parent
* @returns
*/
static protoIsUsedInProto (proto, parent)
{
for (const object of parent .traverse (Traverse .PROTO_DECLARATIONS | Traverse .PROTO_DECLARATION_BODY | Traverse .ROOT_NODE))
{
if (!(object instanceof X3D .SFNode))
continue;
const node = object .getValue ();
if (!node .getType () .includes (X3D .X3DConstants .X3DPrototypeInstance))
continue;
if (node .getProtoNode () === proto)
return true;
}
return false;
}
/**
*
* @param {X3DExecutionContext} executionContext
* @param {string} name
* @param {UndoManager} undoManager
* @returns {X3DExternProtoDeclaration}
*/
static addExternProtoDeclaration (executionContext, name, undoManager = UndoManager .shared)
{
const
oldExternprotos = new Map (Array .from (executionContext .externprotos, p => [p .getName (), p])),
externproto = new X3D .X3DExternProtoDeclaration (executionContext, new X3D .MFString ());
undoManager .beginUndo (_("Add Extern Prototype Declaration »%s«"), name);
externproto .setup ();
executionContext .updateExternProtoDeclaration (name, externproto);
undoManager .registerUndo (() =>
{
this .setExternProtoDeclarations (executionContext, oldExternprotos, undoManager);
});
this .requestUpdateInstances (executionContext, undoManager);
undoManager .endUndo ();
return externproto;
}
/**
*
* @param {X3DExecutionContext} executionContext
* @param {string} name
* @param {X3DExternProtoDeclaration} proto
* @param {UndoManager} undoManager
*/
static updateExternProtoDeclaration (executionContext, name, externproto, undoManager = UndoManager .shared)
{
const oldName = externproto .getName ();
undoManager .beginUndo (_("Update Extern Prototype Declaration »%s«"), name);
executionContext .updateExternProtoDeclaration (name, externproto);
undoManager .registerUndo (() =>
{
if (oldName)
this .updateExternProtoDeclaration (executionContext, oldName, externproto, undoManager);
else
this .removeExternProtoDeclaration (executionContext, name, undoManager);
});
this .requestUpdateInstances (executionContext, undoManager);
undoManager .endUndo ();
}
/**
*
* @param {X3DExecutionContext} executionContext
* @param {string} name
* @param {UndoManager} undoManager
*/
static removeExternProtoDeclaration (executionContext, name, undoManager = UndoManager .shared)
{
const oldExternProtos = new Map (Array .from (executionContext .externprotos, p => [p .getName (), p]));
undoManager .beginUndo (_("Remove Extern Prototype Declaration »%s«"), name);
executionContext .removeExternProtoDeclaration (name);
undoManager .registerUndo (() =>
{
this .setExternProtoDeclarations (executionContext, oldExternProtos, undoManager);
});
this .requestUpdateInstances (executionContext, undoManager);
undoManager .endUndo ();
}
/**
*
* @param {X3DExecutionContext} executionContext
* @param {Array<X3DExternProtoDeclaration>|Map<string,X3DExternProtoDeclaration>} externprotos
* @param {UndoManager} undoManager
*/
static setExternProtoDeclarations (executionContext, externprotos, undoManager = UndoManager .shared)
{
const oldExternProtos = new Map (Array .from (executionContext .externprotos, p => [p .getName (), p]));
undoManager .beginUndo (_("Update Extern Prototype Declarations"));
for (const name of oldExternProtos .keys ())
executionContext .removeExternProtoDeclaration (name);
if (Array .isArray (externprotos))
{
for (const externproto of externprotos)
executionContext .updateExternProtoDeclaration (externproto .getName (), externproto);
}
else
{
for (const [name, externproto] of externprotos)
executionContext .updateExternProtoDeclaration (name, externproto);
}
undoManager .registerUndo (() =>
{
this .setExternProtoDeclarations (executionContext, oldExternProtos, undoManager);
});
this .requestUpdateInstances (executionContext, undoManager);
undoManager .endUndo ();
}
/**
*
* @param {X3DExecutionContext} executionContext
* @param {X3DExternProtoDeclaration} externproto
* @param {UndoManager} undoManager
* @returns {Promise<void>}
*/
static async turnIntoPrototype (executionContext, externproto, undoManager = UndoManager .shared)
{
const
numProtos = executionContext .protos .length,
x3dSyntax = await this .exportX3D (externproto .getInternalScene (), [externproto .getProtoDeclaration ()]);
undoManager .beginUndo (_("Turn Extern Prototype »%s« into Prototype"), externproto .getName ());
this .removeExternProtoDeclaration (executionContext, externproto .getName (), undoManager);
await this .importX3D (executionContext, x3dSyntax, undoManager);
const
protos = Array .from (executionContext .protos),
importedProtos = protos .splice (numProtos, protos .length - numProtos),
proto = importedProtos .at (-1);
for (const proto of importedProtos .reverse ())
{
protos .unshift (proto);
this .rewriteURLs (executionContext, proto, externproto .getInternalScene () .worldURL, executionContext .worldURL, new UndoManager ());
}
this .setProtoDeclarations (executionContext, protos, undoManager);
this .replaceProtoNodes (executionContext, externproto, proto, undoManager);
undoManager .endUndo ();
}
/**
*
* @param {X3DExecutionContext} executionContext
* @param {X3DProtoDeclarationNode} protoNode
* @returns
*/
static isProtoNodeUsed (executionContext, protoNode)
{
for (const object of executionContext .traverse (Traverse .ROOT_NODES | Traverse .PROTO_DECLARATIONS | Traverse .PROTO_DECLARATION_BODY))
{
if (!(object instanceof X3D .SFNode))
continue;
const node = object .getValue ();
if (!node .getType () .includes (X3D .X3DConstants .X3DPrototypeInstance))
continue;
if (node .getProtoNode () === protoNode)
return true;
}
return false;
}
/**
*
* @param {X3DExecutionContext} executionContext execution context, mostly from proto node argument
* @param {X3DProtoDeclarationNode} protoNode from which the next proto node with the same name is available.
* @returns {X3DProtoDeclarationNode|null}
*/
static getNextAvailableProtoNode (executionContext, protoNode)
{
const name = protoNode instanceof X3D .X3DProtoDeclarationNode ? protoNode .getName () : protoNode
if (protoNode instanceof X3D .X3DProtoDeclaration)
{
const externproto = executionContext .externprotos .get (name)
if (externproto)
return externproto
}
const proto = executionContext .getOuterNode ()
if (!(proto instanceof X3D .X3DProtoDeclaration))
return null
executionContext = proto .getExecutionContext ()
const index = executionContext .protos .indexOf (proto)
for (let i = 0; i < index; ++ i)
{
const proto = executionContext .protos [i]
if (proto .getName () === name)
return proto
}
const externproto = executionContext .externprotos .get (name)
if (externproto)
return externproto
return this .getNextAvailableProtoNode (executionContext, name)
}
/**
*
* @param {X3DExecutionContext} executionContext
* @param {X3DPrototypeInstance} instance
* @param {X3DProtoDeclarationNode} protoNode
* @param {UndoManager} undoManager
*/
static setProtoNode (executionContext, instance, protoNode, undoManager = UndoManager .shared)
{
instance = instance .valueOf ();
const oldProtoNode = instance .getProtoNode ();
undoManager .beginUndo (_("Set Proto Node of Instance to %s"), protoNode .getName ());
const outerNode = executionContext .getOuterNode ();
// Remove references from instance.
const references = new Map ();
if (outerNode && outerNode instanceof X3D .X3DProtoDeclaration)
{
const proto = outerNode;
for (const field of instance .getPredefinedFields ())
{
references .set (field .getName (), new Set (field .getReferences ()));
for (const reference of field .getReferences ())
this .removeReference (proto, reference, instance, field, undoManager);
}
}
// Remove routes from instance.
const
inputRoutes = new Map (),
outputRoutes = new Map ();
for (const field of instance .getPredefinedFields ())
{
inputRoutes .set (field .getName (), new Set (field .getInputRoutes ()));
outputRoutes .set (field .getName (), new Set (field .getOutputRoutes ()));
}
this .deleteRoutes (executionContext, instance, undoManager);
// Set proto node.
instance .setProtoNode (protoNode);
// Restore references.
if (outerNode && outerNode instanceof X3D .X3DProtoDeclaration)
{
const proto = outerNode;
for (const field of instance .getPredefinedFields ())
{
const oldReferences = references .get (field .getName ());
if (oldReferences)
{
for (const oldReference of oldReferences)
{
const reference = proto .getUserDefinedFields () .get (oldReference .getName ());
if (!reference)
continue;
if (reference .getType () !== field .getType ())
continue;
if (!reference .isReference (field .getAccessType ()))
continue;
this .addReference (proto, reference, instance, field, undoManager);
}
}
}
}
// Restore routes.
for (const field of instance .getPredefinedFields ())
{
const
oldInputRoutes = inputRoutes .get (field .getName ()),
oldOutputRoutes = outputRoutes .get (field .getName ());
if (oldInputRoutes)
{
for (const route of oldInputRoutes)
{
this .addRoute (executionContext, route .sourceNode, route .sourceField, route .destinationNode, route .destinationField, undoManager);
}
}
if (oldOutputRoutes)
{
for (const route of oldOutputRoutes)
{
this .addRoute (executionContext, route .sourceNode, route .sourceField, route .destinationNode, route .destinationField, undoManager);
}
}
}
undoManager .registerUndo (() =>
{
this .setProtoNode (executionContext, instance, oldProtoNode, undoManager);
});
this .requestUpdateInstances (executionContext, undoManager);
undoManager .endUndo ();
}
/**
* Replaces in X3DPrototypeInstance nodes protoNode by other proto node.
* @param {X3DExecutionContext} executionContext
* @param {X3DProtoDeclarationNode} protoNode
* @param {X3DProtoDeclarationNode} by
*/
static replaceProtoNodes (executionContext, protoNode, by, undoManager = UndoManager .shared)
{
undoManager .beginUndo (_("Replace Proto Node %s"), protoNode .getName ());
for (const object of executionContext .traverse (Traverse .ROOT_NODES | Traverse .PROTO_DECLARATIONS | Traverse .PROTO_DECLARATION_BODY))
{
if (!(object instanceof X3D .SFNode))
continue;
const node = object .getValue ();
if (node .getType () .includes (X3D .X3DConstants .X3DPrototypeInstance))
{
if (node .getProtoNode () === protoNode)
this .setProtoNode (node .getExecutionContext (), node, by, undoManager);
}
}
this .requestUpdateInstances (executionContext, undoManager);