UNPKG

sunrize

Version:
1,619 lines (1,258 loc) 118 kB
"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);