sunrize
Version:
Sunrize — A Multi-Platform X3D Editor
1,293 lines (995 loc) • 37.7 kB
JavaScript
;
const
$ = window .jQuery = require ("jquery"),
$ui = require ("jquery-ui-dist/jquery-ui"),
electron = require ("electron"),
fs = require ("fs"),
path = require ("path"),
X3D = require ("../X3D"),
Interface = require ("./Interface"),
Splitter = require ("../Controls/Splitter"),
Dashboard = require ("./Dashboard"),
Footer = require ("./Footer"),
Sidebar = require ("./Sidebar"),
ActionKeys = require ("./ActionKeys"),
Editor = require ("../Undo/Editor"),
UndoManager = require ("../Undo/UndoManager"),
ImageParser = require ("../Parser/ImageParser"),
VideoParser = require ("../Parser/VideoParser"),
AudioParser = require ("../Parser/AudioParser"),
_ = require ("./GetText");
module .exports = class Document extends Interface
{
#replaceWorld;
constructor ()
{
super ("Sunrize.Document.");
// Globals
this .config .global .setDefaultValues ({
autoSave: true,
});
// Layout
this .verticalSplitter = new Splitter ($("#vertical-splitter"), "vertical");
this .horizontalSplitter = new Splitter ($("#horizontal-splitter"), "horizontal");
this .secondaryToolbar = new Dashboard ($("#secondary-toolbar"), this);
this .footer = new Footer ($("#footer"));
this .sidebar = new Sidebar ($("#sidebar"));
// Prevent scrolling when Panel becomes larger.
$("#vertical-splitter") .on ("scroll", () => $("#vertical-splitter") .scrollTop (0));
// Additional Parsers
X3D .GoldenGate .addParsers (ImageParser, VideoParser, AudioParser);
}
/**
*
*/
async initialize ()
{
$("html")
.attr ("platform", process .platform)
.addClass ("read-only");
// Actions
electron .ipcRenderer .on ("activate", () => this .activate ());
$(window)
.on ("focusin", () => this .onfocus ())
.on ("focusout", () => this .onfocus ());
$(window) .on ("keydown", event => this .onkeydown (event));
// File Menu
electron .ipcRenderer .on ("open-files", (event, urls) => this .loadURL (urls [0])); // DEBUG
electron .ipcRenderer .on ("save-file", (event) => this .saveFile ());
electron .ipcRenderer .on ("save-file-as", (event, filePath) => this .saveFileAs (filePath));
electron .ipcRenderer .on ("save-copy-as", (event, filePath) => this .saveCopyAs (filePath));
electron .ipcRenderer .on ("auto-save", (event, value) => this .autoSave = value);
electron .ipcRenderer .on ("export-as", (event, filePath) => this .exportAs (filePath));
electron .ipcRenderer .on ("scene-properties", (event) => require ("../Editors/SceneProperties") .open ());
electron .ipcRenderer .on ("close", (event) => this .close ());
$(window) .on ("beforeunload", () => this .close ());
// Edit Menu
electron .ipcRenderer .on ("undo", () => this .undo ());
electron .ipcRenderer .on ("redo", () => this .redo ());
$(window)
.on ("cut", () => this .cut ())
.on ("copy", () => this .copy ())
.on ("paste", () => this .paste ());
electron .ipcRenderer .on ("delete", () => this .delete ());
// View Menu
electron .ipcRenderer .on ("primitive-quality", (event, value) => this .setPrimitiveQuality (value));
electron .ipcRenderer .on ("texture-quality", (event, value) => this .setTextureQuality (value));
electron .ipcRenderer .on ("text-compression", (event, value) => this .setTextCompression (value));
electron .ipcRenderer .on ("color-space", (event, value) => this .setColorSpace (value));
electron .ipcRenderer .on ("tone-mapping", (event, value) => this .setToneMapping (value));
electron .ipcRenderer .on ("order-independent-transparency", (event, value) => this .setOrderIndependentTransparency (value));
electron .ipcRenderer .on ("logarithmic-depth-buffer", (event, value) => this .setLogarithmicDepthBuffer (value));
electron .ipcRenderer .on ("mute", (event, value) => this .setMute (value));
electron .ipcRenderer .on ("display-rubberband", (event, value) => this .setDisplayRubberband (value));
electron .ipcRenderer .on ("display-timings", (event, value) => this .setDisplayTimings (value));
electron .ipcRenderer .on ("show-library", (event) => this .showLibrary ());
// Layout Menu
electron .ipcRenderer .on ("browser-frame", () => this .browserFrame .open ());
electron .ipcRenderer .on ("grid-tool", (event, typeName, visible) => this .activateGridTool (typeName, visible, true));
electron .ipcRenderer .on ("grid-options", () => this .showGridOptions ());
electron .ipcRenderer .on ("activate-snap-target", (event, visible) => this .activateSnapTarget (visible));
electron .ipcRenderer .on ("activate-snap-source", (event, visible) => this .activateSnapSource (visible));
electron .ipcRenderer .on ("center-snap-target-in-selection", () => this .centerSnapTargetInSelection ());
electron .ipcRenderer .on ("move-selection-to-snap-target", () => this .moveSelectionToSnapTarget ());
electron .ipcRenderer .on ("move-selection-center-to-snap-target", () => this .moveSelectionCenterToSnapTarget ());
// Browser Size
this .fullname = await electron .ipcRenderer .invoke ("fullname");
this .browserFrame = require ("../Editors/BrowserFrame");
// Change undo menu items.
UndoManager .shared .addInterest (this, () => this .undoManager ());
// Override replaceWorld and loadURL.
this .#replaceWorld = this .browser .replaceWorld;
this .browser .loadURL = () => Promise .resolve ();
this .browser .replaceWorld = () => Promise .resolve ();
// Connect browser options.
const browserOptions = [
"PrimitiveQuality",
"TextureQuality",
"TextCompression",
"ColorSpace",
"ToneMapping",
"OrderIndependentTransparency",
"LogarithmicDepthBuffer",
"Mute",
"Rubberband",
"Timings",
];
for (const option of browserOptions)
this .browser .getBrowserOptions () .getField (option) .addInterest (`set_${option}`, this);
this .browser .setBrowserOption ("AlwaysUpdateGeometries", true);
this .browser .setBrowserOption ("MetadataReference", require ("../../package.json") .homepage);
// Connect browser events.
this .browser ._activeLayer .addInterest ("toggleGrids", this);
// Connect for Snap Target and Snap Source.
$(this .browser .element)
.on ("mousedown", event => this .onmousedown (event))
.on ("mouseup", event => this .onsnaptool (event))
.on ("mouseup", event => this .onselect (event))
.on ("contextmenu", event => this .showBrowserContextMenu (event));
electron .ipcRenderer .on ("document", (event, key, ... args) => this [key] (... args));
// Load components.
await this .browser .loadComponents (this .browser .getProfile ("Full"),
this .browser .getComponent ("X_ITE"));
// Modify nodes.
this .browser .updateConcreteNode (require ("../Components/Grouping/StaticGroup"));
this .browser .updateConcreteNode (require ("../Components/Grouping/Switch"));
this .browser .updateConcreteNode (require ("../Components/Navigation/Collision"));
this .browser .updateConcreteNode (require ("../Components/Navigation/LOD"));
require ("../Components");
// Restore
const pkg = require ("../../package.json");
console .info (`Welcome to ${pkg .productName} v${pkg .version}.`);
await this .restoreFile ();
if (!this .isInitialScene)
$("html") .removeClass ("read-only");
}
configure ()
{
this .config .file .setDefaultValues ({
inferProfileAndComponents: true,
primitiveQuality: "MEDIUM",
textureQuality: "MEDIUM",
textCompression: "CHAR_SPACING",
colorSpace: "LINEAR_WHEN_PHYSICAL_MATERIAL",
toneMapping: "NONE",
orderIndependentTransparency: false,
logarithmicDepthBuffer: false,
mute: false,
rubberband: true,
timings: false,
});
this .fileSaveFileTypeWarning = false;
// Configure browser options.
this .setPrimitiveQuality (this .config .file .primitiveQuality);
this .setTextureQuality (this .config .file .textureQuality);
this .setTextCompression (this .config .file .textCompression);
this .setColorSpace (this .config .file .colorSpace);
this .setToneMapping (this .config .file .toneMapping);
this .setOrderIndependentTransparency (this .config .file .orderIndependentTransparency);
this .setLogarithmicDepthBuffer (this .config .file .logarithmicDepthBuffer);
this .setMute (this .config .file .mute);
this .setDisplayRubberband (this .config .file .rubberband);
this .setDisplayTimings (this .config .file .timings);
// Configure grids.
this .#grids .forEach (grid => grid .dispose ());
this .#grids .clear ();
this .#gridFields .clear ();
// Remove Snap Target and Snap Source.
this .#snapTarget ?.dispose ();
this .#snapSource ?.dispose ();
this .#snapTarget = null;
this .#snapSource = null;
// Run activate.
this .activate ();
}
/**
* Run actions when tab is activated/selected.
*/
activate ()
{
this .updateMenu ();
electron .ipcRenderer .sendToHost ("focus");
}
updateMenu ()
{
const menu = { };
this .updateEditMenu (menu);
this .updateUndoMenus (menu);
this .updateBrowserOptionsMenus (menu);
this .updateGridMenus (menu);
this .updateSnapToolMenus (menu);
electron .ipcRenderer .send ("update-menu", menu);
}
// Active (Focused) Element
activeElement = null
onfocus ()
{
this .activeElement = document .activeElement ? $(document .activeElement) : null;
if (this .activeElement ?.is ("input, textarea"))
{
this .activeElement
.off ("contextmenu.Document")
.on ("contextmenu.Document", () => this .showContextMenu ());
}
electron .ipcRenderer .send ("update-menu", this .updateEditMenu ({ }));
}
async showContextMenu ()
{
await $.sleep ();
if (this .activeElementIsMonacoEditor ())
return;
const menu = [
{ role: "undo", accelerator: "CmdOrCtrl+Z" },
{ role: "redo", accelerator: "Shift+CmdOrCtrl+Z" },
{ type: "separator" },
{ role: "cut", accelerator: "CmdOrCtrl+X" },
{ role: "copy", accelerator: "CmdOrCtrl+C" },
{ role: "paste", accelerator: "CmdOrCtrl+P" },
{ type: "separator" },
{ role: "selectAll", accelerator: "CmdOrCtrl+A" },
];
electron .ipcRenderer .send ("context-menu", "default-context-menu", menu);
}
updateEditMenu (menu)
{
return Object .assign (menu,
{
defaultEditMenu: this .activeElementIsInputOrOutput (),
monacoEditor: this .activeElementIsMonacoEditor (),
});
}
activeElementIsInputOrOutput ()
{
if (!this .activeElement)
return false;
const activeElement = this .activeElement;
if (activeElement .is ("input:not([type]), input[type=text]"))
return true;
if (activeElement .is ("textarea"))
return true;
if (activeElement .is (".input, .output"))
return true;
return false;
}
activeElementIsMonacoEditor ()
{
if (!this .activeElement)
return false;
if (this .activeElement .attr ("role") !== "textbox")
return false;
if (!this .activeElement .closest (".monaco-editor") .length)
return false;
if (this .activeElement .closest (".ibwrapper") .length)
return false;
return true;
}
// Menu Accelerators Fix for Windows.
onkeydown (event)
{
switch (event .key)
{
case "a":
{
if (this .activeElementIsInputOrOutput ())
break;
if (event .ctrlKey)
{
this .sidebar .outlineEditor .selectAll ();
return false;
}
break;
}
}
}
/*
* File Menu
*/
async restoreFile ()
{
if (this .fileId)
{
const contents = this .config .global .addNameSpace ("unsaved.") [this .fileId];
if (contents)
{
await this .loadURL (encodeURI (`data:model/x3d+xml,${contents}`));
}
else
{
await this .loadURL (encodeURI (`data:model/x3d+vrml,
Viewpoint {
description "Initial View"
position 2.869677 3.854335 8.769781
orientation -0.7765887 0.6177187 0.1238285 0.5052317
}
`));
this .activateGridTool ("GridTool", true, false);
}
}
else
{
const
location = new URL (window .location),
fileURL = location .searchParams .get ("url") || "";
await this .loadURL (fileURL);
}
}
/**
*
* @param {string} fileURL
* @returns {Promise<void>} Promise
*/
async loadURL (fileURL)
{
try
{
const scene = fileURL
? await this .browser .createX3DFromURL (new X3D .MFString (fileURL))
: null;
await this .#replaceWorld .call (this .browser, scene);
this .browser .currentScene .setSpecificationVersion (X3D .LATEST_VERSION);
}
catch (error)
{
// console .error (error);
}
}
/**
*
* @param {boolean} force force save
*/
saveFile ()
{
this .footer .scriptEditor ?.apply ();
const scene = this .browser .currentScene;
// Infer profile and components.
if (this .config .file .inferProfileAndComponents ?? true)
Editor .inferProfileAndComponents (scene, new UndoManager ());
// Add default meta data.
const
pkg = require ("../../package.json"),
generator = scene .getMetaData ("generator") ?.filter (value => !value .startsWith (pkg .productName)) ?? [ ];
generator .unshift (`${pkg .productName} V${pkg .version}, ${pkg .homepage}`);
if (!scene .getMetaData ("creator") ?.some (value => value .includes (this .fullname)))
scene .addMetaData ("creator", this .fullname);
if (!scene .getMetaData ("created"))
scene .setMetaData ("created", new Date () .toUTCString ());
scene .setMetaData ("modified", new Date () .toUTCString ());
scene .setMetaData ("generator", generator);
// Save source code.
if (this .filePath)
{
if (!path .extname (this .filePath) .match (/\.(?:x3dz?|x3dvz?|x3djz?|html)$/i))
{
if (!this .fileSaveFileTypeWarning)
console .warn (`Couldn't save '${this .filePath}'. File type is not supported.`);
this .fileSaveFileTypeWarning = true;
return;
}
fs .writeFile (this .filePath, Editor .getContents (scene, path .extname (this .filePath)), Function .prototype);
}
else
{
const id = this .fileId;
if (!id)
{
if (!this .fileSaveFileTypeWarning)
console .warn (`Couldn't save '${this .browser .getWorldURL ()}'. The file is a remote file.`);
this .fileSaveFileTypeWarning = true;
return;
}
this .config .global .addNameSpace ("unsaved.") [id] = Editor .getContents (scene);
}
UndoManager .shared .saveNeeded = false;
electron .ipcRenderer .sendToHost ("saved", true);
}
/**
*
* @param {string} filePath
*/
saveFileAs (filePath)
{
const
scene = this .browser .currentScene,
oldWorldURL = scene .worldURL;
this .filePath = filePath;
Editor .rewriteURLs (scene, scene, oldWorldURL, scene .worldURL);
this .saveFile ();
}
/**
*
* @param {string} filePath
*/
saveCopyAs (filePath)
{
const
scene = this .browser .currentScene,
oldFilePath = this .filePath,
oldWorldURL = scene .worldURL,
undoManager = new UndoManager ();
this .filePath = filePath;
const newWorldURL = scene .worldURL;
Editor .rewriteURLs (scene, scene, oldWorldURL, newWorldURL, undoManager);
this .saveFile ();
undoManager .undo ();
this .filePath = oldFilePath;
}
get autoSave ()
{
return this .config .global .autoSave;
}
set autoSave (value)
{
this .config .global .autoSave = value;
this .requestAutoSave ();
}
#saveTimeoutId = undefined;
requestAutoSave ()
{
if (!this .autoSave)
return;
clearTimeout (this .#saveTimeoutId);
this .#saveTimeoutId = setTimeout (() => this .saveFile (), 1000);
}
exportAs (filePath)
{
this .saveCopyAs (filePath);
}
close ()
{
this .footer .scriptEditor ?.apply ();
if (UndoManager .shared .saveNeeded)
this .saveFile ();
electron .ipcRenderer .sendToHost ("closed");
}
/*
* Edit Menu
*/
undo ()
{
UndoManager .shared .undo ();
}
redo ()
{
UndoManager .shared .redo ();
}
undoManager ()
{
this .updateMenu ();
if (UndoManager .shared .saveNeeded)
this .requestAutoSave ();
electron .ipcRenderer .sendToHost ("saved", !UndoManager .shared .saveNeeded);
}
updateUndoMenus (menu)
{
Object .assign (menu,
{
undoLabel: UndoManager .shared .undoLabel,
redoLabel: UndoManager .shared .redoLabel,
});
}
cut ()
{
if (this .activeElementIsInputOrOutput ())
return;
if (this .activeElementIsMonacoEditor ())
return;
this .sidebar .outlineEditor .cutNodes ();
return false;
}
copy ()
{
if (this .activeElementIsInputOrOutput ())
return;
if (this .activeElementIsMonacoEditor ())
return;
this .sidebar .outlineEditor .copyNodes ();
return false;
}
paste ()
{
if (this .activeElementIsInputOrOutput ())
return;
if (this .activeElementIsMonacoEditor ())
return;
this .sidebar .outlineEditor .pasteNodes ();
return false;
}
delete ()
{
this .sidebar .outlineEditor .deleteNodes ();
}
/*
* View Menu
*/
/**
*
* @param {string} value
*/
setPrimitiveQuality (value)
{
this .browser .setBrowserOption ("PrimitiveQuality", value);
this .browser .setDescription (`Primitive Quality: ${value .toLowerCase ()}`);
}
set_PrimitiveQuality ()
{
this .config .file .primitiveQuality = this .browser .getBrowserOption ("PrimitiveQuality");
this .updateMenu ();
}
/**
*
* @param {string} value
*/
setTextureQuality (value)
{
this .browser .setBrowserOption ("TextureQuality", value);
this .browser .setDescription (`Texture Quality: ${value .toLowerCase ()}`);
}
set_TextureQuality ()
{
this .config .file .textureQuality = this .browser .getBrowserOption ("TextureQuality");
this .updateMenu ();
}
/**
*
* @param {string} value
*/
setTextCompression (value)
{
this .browser .setBrowserOption ("TextCompression", value);
this .browser .setDescription (`Text Compression: ${value}`);
}
set_TextCompression ()
{
this .config .file .textCompression = this .browser .getBrowserOption ("TextCompression");
this .updateMenu ();
}
/**
*
* @param {string} value
*/
setColorSpace (value)
{
this .browser .setBrowserOption ("ColorSpace", value);
this .browser .setDescription (`Color Space: ${value}`);
}
set_ColorSpace ()
{
this .config .file .colorSpace = this .browser .getBrowserOption ("ColorSpace");
this .updateMenu ();
}
/**
*
* @param {string} value
*/
setToneMapping (value)
{
this .browser .setBrowserOption ("ToneMapping", value);
this .browser .setDescription (`Tone Mapping: ${value}`);
}
set_ToneMapping ()
{
this .config .file .toneMapping = this .browser .getBrowserOption ("ToneMapping");
this .updateMenu ();
}
/**
*
* @param {boolean} value
*/
setOrderIndependentTransparency (value)
{
this .browser .setBrowserOption ("OrderIndependentTransparency", value);
this .browser .setDescription (`OrderIndependentTransparency: ${value ? "on" : "off"}`);
}
set_OrderIndependentTransparency ()
{
this .config .file .orderIndependentTransparency = this .browser .getBrowserOption ("OrderIndependentTransparency");
this .updateMenu ();
}
/**
*
* @param {boolean} value
*/
setLogarithmicDepthBuffer (value)
{
this .browser .setBrowserOption ("LogarithmicDepthBuffer", value);
this .browser .setDescription (`LogarithmicDepthBuffer: ${value ? "on" : "off"}`);
}
set_LogarithmicDepthBuffer ()
{
this .config .file .logarithmicDepthBuffer = this .browser .getBrowserOption ("LogarithmicDepthBuffer");
this .updateMenu ();
}
/**
*
* @param {boolean} value
*/
setMute (value)
{
this .browser .setBrowserOption ("Mute", value);
this .browser .setDescription (`Mute: ${value ? "on" : "off"}`);
}
set_Mute ()
{
this .config .file .mute = this .browser .getBrowserOption ("Mute");
this .updateMenu ();
}
/**
*
* @param {boolean} value
*/
setDisplayRubberband (value)
{
this .browser .setBrowserOption ("Rubberband", value);
this .browser .setDescription (`Rubberband: ${value ? "on" : "off"}`);
}
set_Rubberband ()
{
this .config .file .rubberband = this .browser .getBrowserOption ("Rubberband");
this .updateMenu ();
}
/**
*
* @param {boolean} value
*/
setDisplayTimings (value)
{
this .browser .setBrowserOption ("Timings", value);
}
set_Timings ()
{
this .config .file .timings = this .browser .getBrowserOption ("Timings");
this .updateMenu ();
}
updateBrowserOptionsMenus (menu)
{
Object .assign (menu,
{
primitiveQuality: this .config .file .primitiveQuality,
textureQuality: this .config .file .textureQuality,
textCompression: this .config .file .textCompression,
colorSpace: this .config .file .colorSpace,
toneMapping: this .config .file .toneMapping,
orderIndependentTransparency: this .config .file .orderIndependentTransparency,
logarithmicDepthBuffer: this .config .file .logarithmicDepthBuffer,
mute: this .config .file .mute,
rubberband: this .config .file .rubberband,
timings: this .config .file .timings,
});
}
showLibrary ()
{
require ("../Editors/Library") .open (this .browser .currentScene);
}
/*
* Layout Menu
*/
static #Grids = [
"GridTool",
"AngleGridTool",
"AxonometricGridTool",
];
#grids = new Map ();
#gridFields = new Map ();
toggleGrids ()
{
const configNode = Editor .getConfigNode (this .browser);
for (const typeName of Document .#Grids)
{
const [visible = false] = configNode ?.getMetaData (`Sunrize/${typeName}/visible`) ?? [ ];
if (!this .#grids .has (typeName) && !visible)
continue;
this .activateGridTool (typeName, visible, false);
}
}
async activateGridTool (typeName, visible, undo = true)
{
const
Tool = require (`../Tools/Grids/${typeName}`),
grid = this .#grids .get (typeName) ?? new Tool (this .browser .currentScene),
tool = await grid .getToolInstance ();
grid ._visible .addInterest ("updateMenu", this);
tool .getField ("isActive") .addInterest ("handleUndoForGrid", this, typeName);
UndoManager .shared .beginUndo (_ ("Change Visibility of %s"), typeName);
if (visible)
{
for (const [typeName, grid] of this .#grids)
{
if (undo)
Editor .setFieldValue (this .browser .currentScene, grid .tool .getValue (), grid ._visible, false);
else
grid ._visible = false;
}
}
this .#grids .set (typeName, grid);
if (undo)
Editor .setFieldValue (this .browser .currentScene, grid .tool .getValue (), grid ._visible, visible);
else
grid ._visible = visible;
UndoManager .shared .endUndo ();
}
async handleUndoForGrid (typeName)
{
const
grid = this .#grids .get (typeName),
tool = await grid .getToolInstance ();
if (tool .isActive)
{
this .#gridFields .set (typeName, new Map ([... tool .getValue () .getFields ()]
.filter (field => field .isInitializable ())
.map (field => [field .getName (), field .copy ()])));
}
else
{
const
executionContext = tool .getValue () .getExecutionContext (),
initialValues = this .#gridFields .get (typeName);
UndoManager .shared .beginUndo (_("Change Properties of %s"), typeName);
for (const field of tool .getValue () .getFields ())
{
if (!field .isInitializable ())
continue;
const initialValue = initialValues .get (field .getName ());
if (field .equals (initialValue))
continue;
const value = field .copy ();
field .assign (initialValue);
Editor .setFieldValue (executionContext, tool .getValue (), field, value);
}
UndoManager .shared .endUndo ();
}
}
updateGridMenus (menu)
{
Document .#Grids .forEach (typeName => menu [typeName] = false);
this .#grids .forEach ((grid, typeName) => menu [typeName] = grid ._visible .getValue ());
}
async showGridOptions ()
{
for (const grid of this .#grids .values ())
{
if (!grid ._visible .getValue ())
continue;
const tool = await grid .getToolInstance ();
this .secondaryToolbar .togglePanel (true);
this .secondaryToolbar .panel .setNode (tool .getValue ());
}
}
#select = false;
#pointer = new X3D .Vector2 ();
#snapTarget = null;
#snapSource = null;
async onmousedown (event)
{
this .#select = false;
if (!this .secondaryToolbar .arrowButton .hasClass ("active"))
return;
switch (event .button)
{
case 0:
{
if (event .shiftKey && (event .ctrlKey || event .metaKey))
return;
this .#pointer .assign (this .browser .getPointerFromEvent (event));
if (this .browser .touch (... this .#pointer))
{
if (this .browser .getHit () .sensors .size)
return;
this .#select = true;
}
else
{
this .#select = true;
}
break;
}
case 2:
{
switch (ActionKeys .value)
{
case ActionKeys .Shift:
{
if (this .#snapTarget ?._visible .getValue ())
break;
this .activateSnapTarget (true);
await this .#snapTarget .getToolInstance ();
this .#snapTarget .onmousedown (event, true);
break;
}
case ActionKeys .Shift | ActionKeys .Option:
{
if (this .#snapSource ?._visible .getValue ())
break;
this .activateSnapSource (true);
await this .#snapSource .getToolInstance ();
this .#snapSource .onmousedown (event, true);
break;
}
}
break;
}
}
}
async onsnaptool (event)
{
if (event .button !== 2)
return;
await this .#snapSource ?.getToolInstance ();
await this .#snapTarget ?.getToolInstance ();
this .#snapSource ?.onmouseup (event);
this .#snapTarget ?.onmouseup (event);
}
onselect (event)
{
if (!this .secondaryToolbar .arrowButton .hasClass ("active"))
return;
if (event .button !== 0)
return;
if (!this .#select)
return;
const pointer = this .browser .getPointerFromEvent (event);
if (this .#pointer .distance (pointer) > this .browser .getRenderingProperty ("ContentScale"))
return;
// Select or deselect.
const outlineEditor = this .sidebar .outlineEditor;
if (!this .browser .touch (... pointer))
{
outlineEditor .deselectAll ();
return;
}
// Select.
const
shapeNode = this .browser .getHit () .shapeNode,
geometryTool = shapeNode .getGeometry () ?.getTool (),
tool = geometryTool ?? shapeNode .getExecutionContext () .getOuterNode () ?.getTool (),
node = tool ?? shapeNode .getExecutionContext () .getOuterNode () ?? shapeNode;
outlineEditor .expandTo (node, { expandObject: true, expandAll: true });
let elements = outlineEditor .sceneGraph .find (`.node[node-id=${node .getId ()}]`);
if (!elements .length)
return;
if (outlineEditor .isEditable (elements))
{
if (tool)
{
elements = Array .from (elements);
}
else
{
const parentElements = Array .from (elements) .flatMap (element =>
{
const parentElements = Array .from ($(element) .parent () .closest (".node", outlineEditor .sceneGraph));
return parentElements .length ? parentElements : element;
});
elements = parentElements .map ((element, i) => outlineEditor .getNode ($(element)) .getType () .includes (X3D .X3DConstants .X3DGroupingNode) ? parentElements [i] : elements [i]);
}
}
else
{
while (!outlineEditor .isEditable (elements))
{
elements .jstree ("close_node", elements);
elements = elements .parent () .closest (".node, .scene", outlineEditor .sceneGraph);
}
elements = Array .from (elements);
}
for (const [i, element] of elements .entries ())
outlineEditor .selectNodeElement ($(element), { add: (event .shiftKey || event .metaKey) || i > 0, target: true });
// Scroll element into view.
// Hide scrollbars during scroll to prevent overlay issue.
outlineEditor .treeView .css ("overflow", "hidden");
elements [0] ?.scrollIntoView ({ block: "center", inline: "start", behavior: "smooth" });
$(window) .scrollTop (0);
setTimeout (() => outlineEditor .treeView .css ("overflow", ""), 1000);
}
activateSnapTarget (visible)
{
const SnapTarget = require ("../Tools/SnapTool/SnapTarget");
this .#snapTarget ??= new SnapTarget (this .browser .currentScene);
this .#snapTarget ._visible .addInterest ("updateMenu", this);
this .#snapTarget ._visible = visible;
}
activateSnapSource (visible)
{
const SnapSource = require ("../Tools/SnapTool/SnapSource");
this .#snapSource ??= new SnapSource (this .browser .currentScene);
this .#snapSource ._visible .addInterest ("updateMenu", this);
this .#snapSource ._visible = visible;
}
async centerSnapTargetInSelection ()
{
this .activateSnapTarget (true);
const
selection = require ("./Selection"),
target = await this .#snapTarget .getToolInstance (),
executionContext = this .browser .currentScene,
layerNode = this .browser .getActiveLayer (),
nodes = selection .nodes,
[values, bbox] = Editor .getModelMatricesAndBBoxes (executionContext, layerNode, nodes);
if (!bbox .size .norm ())
return;
UndoManager .shared .beginUndo (_("Center SnapTarget in Selection"));
Editor .setFieldValue (executionContext, target .getValue (), target .position, bbox .center);
UndoManager .shared .endUndo ();
}
async moveSelectionToSnapTarget ()
{
const
selection = require ("./Selection"),
target = await this .#snapTarget .getToolInstance (),
source = this .#snapSource ?._visible .getValue () ? await this .#snapSource .getToolInstance () : null,
executionContext = this .browser .currentScene,
layerNode = this .browser .getActiveLayer (),
nodes = selection .nodes,
targetPosition = target .position .getValue (),
targetNormal = target .normal .getValue (),
sourcePosition = this .#snapSource ?._visible .getValue () ? source ?.position .getValue () : undefined,
sourceNormal = this .#snapSource ?._visible .getValue () ? source ?.normal .getValue () : undefined;
UndoManager .shared .beginUndo (_("Move Selection to SnapTarget"));
Editor .moveNodesToTarget (executionContext, layerNode, nodes, targetPosition, targetNormal, sourcePosition, sourceNormal, false);
if (this .#snapSource ?._visible .getValue ())
{
Editor .setFieldValue (executionContext, source .getValue (), source .position, target .position);
Editor .setFieldValue (executionContext, source .getValue (), source .normal, target .normal .negate ());
}
UndoManager .shared .endUndo ();
}
async moveSelectionCenterToSnapTarget ()
{
const
selection = require ("./Selection"),
target = await this .#snapTarget .getToolInstance (),
source = this .#snapSource ?._visible .getValue () ? await this .#snapSource .getToolInstance () : null,
executionContext = this .browser .currentScene,
layerNode = this .browser .getActiveLayer (),
nodes = selection .nodes,
targetPosition = target .position .getValue (),
targetNormal = target .normal .getValue (),
sourcePosition = this .#snapSource ?._visible .getValue () ? source ?.position .getValue () : undefined,
sourceNormal = this .#snapSource ?._visible .getValue () ? source ?.normal .getValue () : undefined;
UndoManager .shared .beginUndo (_("Move Selection Center to SnapTarget"));
Editor .moveNodesToTarget (executionContext, layerNode, nodes, targetPosition, targetNormal, sourcePosition, sourceNormal, true);
if (this .#snapSource ?._visible .getValue ())
{
Editor .setFieldValue (executionContext, source .getValue (), source .position, target .position);
Editor .setFieldValue (executionContext, source .getValue (), source .normal, target .normal .negate ());
}
UndoManager .shared .endUndo ();
}
updateSnapToolMenus (menu)
{
menu .SnapTarget = this .#snapTarget ?._visible .getValue () ?? false;
menu .SnapSource = this .#snapSource ?._visible .getValue () ?? false;
}
/**
* Context Menu
*/
showBrowserContextMenu (event)
{
if (event .shiftKey || event .ctrlKey || event .metaKey)
return;
const
activeLayer = this .browser .getActiveLayer (),
viewpoints = activeLayer .getViewpoints () .get ();
const menu = [
{
label: _("Viewpoints"),
submenu: viewpoints .filter ((_, index) => index > 0) .map ((viewpointNode, index) => ({
label: `${viewpointNode ._description .getValue () || viewpointNode .getDisplayName () || `VP${index + 1}}`}`,
type: "radio",
checked: viewpointNode ._isBound .getValue (),
args: ["bindViewpoint", index + 1],
})),
},
];
electron .ipcRenderer .send ("context-menu", "document", menu);
}
bindViewpoint (index)
{
const
activeLayer = this .browser .getActiveLayer (),
viewpoints = activeLayer .getViewpoints () .get (),
viewpointNode = viewpoints [index];
if (!viewpointNode)
return;
viewpointNode ._set_bind = true;
}
};