UNPKG

sunrize

Version:

Sunrize — A Multi-Platform X3D Editor

1,510 lines (1,145 loc) 95.8 kB
"use strict"; const $ = require ("jquery"), electron = require ("electron"), capitalize = require ("capitalize"), X3D = require ("../X3D"), Interface = require ("../Application/Interface"), Splitter = require ("../Controls/Splitter"), NodeList = require ("./NodeList"), MemberList = require ("./AnimationMemberList"), Editor = require ("../Undo/Editor"), _ = require ("../Application/GetText"); require ("../Bits/Validate"); module .exports = class AnimationEditor extends Interface { constructor (element) { super (`Sunrize.AnimationEditor.${element .attr ("id")}.`); this .animationEditor = element; this .verticalSplitter = $("<div></div>") .attr ("id", "animation-editor-content") .addClass (["animation-editor-content", "vertical-splitter"]) .appendTo (this .animationEditor) .on ("mouseleave", () => this .requestDrawTimeline ()); this .verticalSplitterLeft = $("<div></div>") .addClass ("vertical-splitter-left") .css ("width", "30%") .appendTo (this .verticalSplitter) .on ("mouseleave", () => this .requestDrawTimeline ()); this .timelineElement = $("<div></div>") .attr ("tabindex", 0) .addClass (["timeline", "vertical-splitter-right"]) .css ("width", "70%") .on ("mouseleave", () => this .clearPointer ()) .on ("mousedown", event => this .on_mousedown (event)) .on ("mouseup", event => this .on_mouseup (event)) .on ("mousemove", event => this .on_mousemove (event)) .on ("wheel", event => this .on_wheel (event)) .on ("keydown", event => this .on_keydown (event)) .appendTo (this .verticalSplitter); this .scrollbarElement = $("<div></div>") .addClass ("scrollbar") .css ("width", "100%") .on ("mousedown", event => this .on_mousedown_scrollbar (event)) .on ("mouseup", event => this .on_mouseup_scrollbar (event)) .on ("mousemove", event => this .on_mousemove_scrollbar (event)) .appendTo (this .timelineElement); this .vSplitter = new Splitter (this .verticalSplitter, "vertical"); // Toolbar this .toolbar = $("<div></div>") .attr ("id", "animation-editor-toolbar") .addClass (["animation-editor-toolbar", "toolbar", "horizontal-toolbar"]) .appendTo (this .animationEditor); this .createAnimationIcon = $("<span></span>") .addClass (["material-symbols-outlined", "disabled"]) .attr ("title", _("Create animation.")) .text ("animation") .appendTo (this .toolbar) .on ("click", () => this .createAnimation ()); $("<span></span>") .addClass ("separator") .appendTo (this .toolbar); this .addMembersIcon = $("<span></span>") .addClass ("material-icons") .attr ("title", _("Add member(s) to animation.")) .text ("add") .appendTo (this .toolbar) .on ("click", () => this .addMembers ()); $("<span></span>") .addClass ("separator") .appendTo (this .toolbar); this .cutFrameIcon = $("<span></span>") .addClass ("material-icons") .attr ("title", _("Cut selected keyframes.")) .text ("content_cut") .appendTo (this .toolbar) .on ("click", () => this .cutKeyframes ()); this .copyFrameIcon = $("<span></span>") .addClass ("material-icons") .attr ("title", _("Copy selected keyframes.")) .text ("content_copy") .appendTo (this .toolbar) .on ("click", () => this .copyKeyframes ()); this .pasteFrameIcon = $("<span></span>") .addClass ("material-icons") .attr ("title", _("Paste keyframes at current frame.")) .text ("content_paste") .appendTo (this .toolbar) .on ("click", () => this .pasteKeyframes ()); $("<span></span>") .addClass ("separator") .appendTo (this .toolbar); this .firstFrameIcon = $("<span></span>") .addClass ("material-icons") .attr ("title", _("Go to first frame.")) .text ("first_page") .appendTo (this .toolbar) .on ("click", () => this .firstFrame ()); this .toggleAnimationIcon = $("<span></span>") .addClass ("material-icons") .attr ("title", _("Start animation.")) .text ("play_arrow") .appendTo (this .toolbar) .on ("click", () => this .toggleAnimation ()); this .lastFrameIcon = $("<span></span>") .addClass ("material-icons") .attr ("title", _("Go to last frame.")) .text ("last_page") .appendTo (this .toolbar) .on ("click", () => this .lastFrame ()); this .loopIcon = $("<span></span>") .addClass ("material-icons") .attr ("title", _("Loop animation.")) .text ("loop") .appendTo (this .toolbar) .on ("click", () => this .toggleLoop ()); this .frameInput = $("<input></input>") .addClass ("input") .attr ("type", "number") .attr ("step", 1) .attr ("min", 0) .attr ("max", 0) .attr ("title", _("Current frame.")) .css ("width", "70px") .appendTo (this .toolbar) .on ("change input", () => this .setCurrentFrame (this .getCurrentFrame ())); this .propertiesIcon = $("<span></span>") .addClass ("material-icons") .attr ("title", _("Edit animation properties.")) .text ("access_time") .appendTo (this .toolbar) .on ("click", () => this .showProperties ()); $("<span></span>") .addClass ("separator") .appendTo (this .toolbar); this .keyTypeElement = $("<select></select>") .addClass ("select") .attr ("title", _("Select keyframe type.")) .append ($("<option></option>") .text ("CONSTANT")) .append ($("<option></option>") .text ("LINEAR") .attr ("selected", "")) .append ($("<option></option>") .text ("SPLINE")) .append ($("<option></option>") .text ("SPLIT")) .append ($("<option></option>") .text ("MIXED") .hide ()) .appendTo (this .toolbar) .on ("change", () => this .setKeyType ()); this .timeElement = $("<span></span>") .addClass (["text", "right"]) .attr ("title", _("Current frame time (hours:minutes:seconds:frames).")) .css ("top", "7.5px") .css ("margin-right", "6px") .text (this .formatFrames (0, 10)) .appendTo (this .toolbar); // Navigation toolbar this .navigation = $("<div></div>") .attr ("id", "animation-editor-navigation") .addClass (["animation-editor-navigation", "toolbar", "vertical-toolbar"]) .appendTo (this .animationEditor); this .zoomOutIcon = $("<span></span>") .addClass ("material-icons") .attr ("title", _("Zoom timeline out.")) .css ("transform", "scale(1.4)") .css ("margin-bottom", "15px") .text ("zoom_out") .appendTo (this .navigation) .on ("click", () => this .zoomOut ()); this .zoomInIcon = $("<span></span>") .addClass ("material-icons") .attr ("title", _("Zoom timeline in.")) .css ("transform", "scale(1.4)") .css ("margin-bottom", "15px") .text ("zoom_in") .appendTo (this .navigation) .on ("click", () => this .zoomIn ()); this .zoomFitIcon = $("<span></span>") .addClass ("material-icons") .attr ("title", _("Zoom timeline to fit in window.")) .css ("transform", "scale(1.4)") .css ("margin-bottom", "15px") .text ("fit_screen") .appendTo (this .navigation) .on ("click", () => this .zoomFit ()); this .zoom100Icon = $("<span></span>") .addClass ("material-icons") .attr ("title", _("Default timeline zoom.")) .css ("transform", "scale(1.4)") .css ("margin-bottom", "15px") .text ("1x_mobiledata") .appendTo (this .navigation) .on ("click", () => this .zoom100 ()); // Animations List this .nodeListElement = $("<div></div>") .addClass (["alternating", "node-list"]) .appendTo (this .verticalSplitterLeft); this .membersListElement = $("<div></div>") .addClass ("node-list") .appendTo (this .verticalSplitterLeft) .on ("scroll mousemove", () => this .drawTimeline ()); this .animationName = $("<input></input>") .addClass ("node-name") .attr ("title", _("Rename animation.")) .attr ("placeholder", _("Enter animation name.")) .appendTo (this .verticalSplitterLeft) .validate (Editor .Id, () => { electron .shell .beep (); this .highlight (); }) .on ("keydown", event => this .renameAnimation (event)); // Tracks this .tracks = $("<canvas></canvas>") .addClass ("tracks") .prependTo (this .animationEditor); this .tracksResizer = new ResizeObserver (() => this .resizeTimeline ()); this .tracksResizer .observe (this .timelineElement [0]); // Lists this .memberList = new MemberList (this, this .membersListElement); this .nodeList = new NodeList (this .nodeListElement, { filter: node => this .isAnimation (node), callback: animation => this .setAnimation (animation), }); // Selection const selection = require ("../Application/Selection"); selection .addInterest (this, () => this .setSelection (selection)); this .setSelection (selection); // Setup this .setup (); } configure () { this .config .file .setDefaultValues ({ scaleKeyframes: true, keyType: "LINEAR", }); this .keyTypeElement .val (this .config .file .keyType); } colorScheme (shouldUseDarkColors) { this .requestDrawTimeline (); } isAnimation (node) { if (!node .getType () .includes (X3D .X3DConstants .Group)) return false; if (!node .hasMetaData ("Animation/duration")) return false; if (!node ._children .find (node => node .getValue () .getType () .includes (X3D .X3DConstants .TimeSensor))) return false; return true; } setAnimation (animation) { // Remove this .setPickedKeyframes ([ ]); this .setSelectedKeyframes ([ ]); this .setSelectionRange (0, 0); this .animation ?._children .removeInterest ("updateMemberList", this); this .animation ?.name_changed .removeInterest ("updateAnimationName", this); if (this .timeSensor) { this .timeSensor ._loop .removeInterest ("set_loop", this); this .timeSensor ._isActive .removeInterest ("set_active", this); this .timeSensor ._fraction_changed .removeInterest ("set_fraction", this); this .timeSensor ._evenLive = false; this .timeSensor ._range = [0, 0, 1]; this .timeSensor ._resumeTime = 0; this .timeSensor ._pauseTime = 0; if (this .timeSensor ._loop .getValue () && this .timeSensor ._isActive .getValue ()) { this .timeSensor ._stopTime = 0; this .timeSensor ._startTime = 0; } else { this .timeSensor ._startTime = 0; this .timeSensor ._stopTime = 1; } for (const interpolator of this .interpolators) interpolator ._set_fraction = 0; } // Set this .animation = animation; // Add this .enableIcons (this .animation); if (this .animation) { // TimeSensor this .timeSensor = this .animation ._children .find (node => node .getValue () .getType () .includes (X3D .X3DConstants .TimeSensor)) .getValue (); this .timeSensor ._loop .addInterest ("set_loop", this); this .timeSensor ._isActive .addInterest ("set_active", this); this .timeSensor ._fraction_changed .addInterest ("set_fraction", this); this .timeSensor ._evenLive = true; this .timeSensor ._range = [0, 0, 1]; this .set_loop (this .timeSensor ._loop); this .set_active (this .timeSensor ._isActive); // Show Member List this .animation ._children .addInterest ("updateMemberList", this); this .nodeListElement .hide (); this .membersListElement .show (); this .memberList .setAnimation (this .animation, this .timeSensor); this .updateMemberList (); // Animation Name this .animationName .removeAttr ("disabled"); this .animation .name_changed .addInterest ("updateAnimationName", this); this .updateAnimationName (); // Timeline this .frameInput .attr ("max", this .getDuration ()); } else { // Show Animations List this .updateMemberList (); this .membersListElement .hide (); this .nodeListElement .show (); // Animation Name this .animationName .val (""); this .animationName .attr ("disabled", ""); // Timeline this .frameInput .attr ("max", 0); } // Timeline this .setSelection (require ("../Application/Selection")); this .zoomFit (); this .requestDrawTimeline (); this .browser .nextFrame () .then (() => this .setCurrentFrame (0)); } enableIcons (enabled) { $([ this .addMembersIcon, this .cutFrameIcon, this .copyFrameIcon, this .pasteFrameIcon, this .firstFrameIcon, this .toggleAnimationIcon, this .lastFrameIcon, this .loopIcon, this .frameInput, this .propertiesIcon, this .keyTypeElement, this .timeElement, ] .flatMap (object => [... object])) .removeClass (enabled ? "disabled" : [ ]) .addClass (enabled ? [ ] : "disabled"); } setSelection (selection) { if (this .isGroupingNodeLike (selection .nodes .at (-1))) this .createAnimationIcon .removeClass ("disabled"); else this .createAnimationIcon .addClass ("disabled"); if (!this .animation) return; if (selection .nodes .at (-1)) this .addMembersIcon .removeClass ("disabled"); else this .addMembersIcon .addClass ("disabled"); } #groupingNodes = new Set ([ X3D .X3DConstants .X3DLayerNode, X3D .X3DConstants .X3DGroupingNode, X3D .X3DConstants .ViewpointGroup, ]); isGroupingNodeLike (node) { if (!node) return true; // X3DScene if (node .getType () .some (type => this .#groupingNodes .has (type))) return true; return false; } createAnimation () { Editor .undoManager .beginUndo (_("Add Animation")); const selection = require ("../Application/Selection"), group = selection .nodes .at (-1), executionContext = group ?.getExecutionContext () ?? this .browser .currentScene, node = group ?? executionContext, field = group ?._children ?? executionContext ._rootNodes; Editor .addComponent (executionContext .getLocalScene (), "Grouping"); Editor .addComponent (executionContext .getLocalScene (), "Time"); const animation = executionContext .createNode ("Group", false), timeSensor = executionContext .createNode ("TimeSensor", false); animation ._children .push (timeSensor); timeSensor ._description = "New Animation"; timeSensor .setup (); animation .setup (); executionContext .addNamedNode (executionContext .getUniqueName ("NewAnimation"), animation); executionContext .addNamedNode (executionContext .getUniqueName ("NewAnimationTimer"), timeSensor); animation .setMetaData ("Animation/duration", new X3D .SFInt32 (10)); animation .setMetaData ("Animation/frameRate", new X3D .SFInt32 (10)); Editor .insertValueIntoArray (executionContext, node, field, 0, animation); Editor .undoManager .endUndo (); // Wait until NodeList knows animation, to have it restored after reload. setTimeout (() => this .nodeList .setNode (animation)); } resizeAnimation (newDuration, newFrameRate, scaleKeyframes) { this .config .file .scaleKeyframes = scaleKeyframes; const duration = this .getDuration (), frameRate = this .getFrameRate (); if (newDuration === duration && newFrameRate === frameRate) return; if (newDuration < 1) return; Editor .undoManager .beginUndo (_("Resize Animation")); const timeSensor = this .timeSensor, executionContext = timeSensor .getExecutionContext () Editor .setFieldValue (executionContext, timeSensor, timeSensor ._cycleInterval, newDuration / newFrameRate); Editor .setNodeMetaData (this .animation, "Animation/duration", new X3D .SFInt32 (newDuration)); Editor .setNodeMetaData (this .animation, "Animation/frameRate", new X3D .SFInt32 (newFrameRate)); if (scaleKeyframes) { const scale = newDuration / duration; for (const interpolator of this .interpolators) { const key = interpolator .getMetaData ("Interpolator/key", new X3D .MFInt32 ()) .map (value => value * scale); Editor .setNodeMetaData (interpolator, "Interpolator/key", key); } this .setCurrentFrame (Math .floor (this .getCurrentFrame () * scale)); } else { // Remove keyframes greater than duration. for (const interpolator of this .interpolators) { const key = interpolator .getMetaData ("Interpolator/key", new X3D .MFInt32 ()); const keyValue = interpolator .getMetaData ("Interpolator/keyValue", new X3D .MFDouble ()); const keyType = interpolator .getMetaData ("Interpolator/keyType", new X3D .MFString ()); const index = X3D .Algorithm .upperBound (key, 0, key .length, newDuration); Editor .setNodeMetaData (interpolator, "Interpolator/key", key .slice (0, index)); Editor .setNodeMetaData (interpolator, "Interpolator/keyValue", keyValue .slice (0, index)); Editor .setNodeMetaData (interpolator, "Interpolator/keyType", keyType .slice (0, index)); } this .setCurrentFrame (Math .min (this .getCurrentFrame (), newDuration)); } this .updateInterpolators () this .registerZoomFit (); Editor .undoManager .endUndo (); this .frameInput .attr ("max", newDuration); } closeAnimation () { this .nodeList .setNode (null); } updateAnimationName () { const name = this .animation .getDisplayName (); this .animationName .val (name); this .memberList .setAnimationName (name); } renameAnimation (event) { if (event .key !== "Enter") return; const getDescription = (animation) => { return animation .getDisplayName () .replace (/(\d+)/g, " $1") .replace (/([A-Z]+[a-z\d ]+)/g, " $1") .replace (/([A-Z][a-z]+)/g, " $1") .replace (/\s+/g, " ") .trim (); } Editor .undoManager .beginUndo (_("Rename Animation")); const { animation, timeSensor } = this; const executionContext = animation .getExecutionContext (); const name = this .animationName .val (); const oldDescription = getDescription (animation); Editor .updateNamedNode (executionContext, executionContext .getUniqueName (`${name}`), animation); Editor .updateNamedNode (executionContext, executionContext .getUniqueName (`${name}Timer`), timeSensor); // Don't update description if manually set. if (!timeSensor ._description .getValue () || timeSensor ._description .getValue () === oldDescription) Editor .setFieldValue (executionContext, timeSensor, timeSensor ._description, getDescription (animation)); for (const interpolator of this .interpolators) { const name = this .getInterpolatorName (interpolator); if (!name) continue; Editor .updateNamedNode (executionContext, executionContext .getUniqueName (name), interpolator); } Editor .undoManager .endUndo (); } // Members Handling addMembers () { const selection = require ("../Application/Selection"); this .memberList .addNodes (selection .nodes); this .requestDrawTimeline (); } removeMembers (nodes) { const animation = this .animation, executionContext = animation .getExecutionContext (); Editor .undoManager .beginUndo (_("Remove Member from »%s«"), animation .getDisplayName ()); for (const node of nodes) { const interpolators = Array .from (node .getFields (), field => this .fields .get (field)), children = animation ._children .filter (node => !interpolators .includes (node .getValue ())); Editor .setFieldValue (executionContext, animation, animation ._children, children); } this .registerRequestDrawTimeline (); Editor .undoManager .endUndo (); // Update member list. this .updateMembers (); this .memberList .removeNodes (nodes); // Prevent losing members without interpolator. this .#changing = true; this .browser .nextFrame () .then (() => this .#changing = false); } #interpolatorTypes = new Set ([ X3D .X3DConstants .BooleanSequencer, X3D .X3DConstants .IntegerSequencer, X3D .X3DConstants .ColorInterpolator, X3D .X3DConstants .ScalarInterpolator, X3D .X3DConstants .OrientationInterpolator, X3D .X3DConstants .PositionInterpolator2D, X3D .X3DConstants .PositionInterpolator, X3D .X3DConstants .CoordinateInterpolator2D, X3D .X3DConstants .CoordinateInterpolator, X3D .X3DConstants .NormalInterpolator, ]); members = new Set (); fields = new Map (); // [field, interpolator] interpolators = new Set (); updateMembers () { for (const interpolator of this .interpolators) interpolator ._value_changed .removeRouteCallback (this); this .members .clear (); this .fields .clear (); this .interpolators .clear (); for (const node of this .animation ?._children ?? [ ]) { const interpolator = node .getValue (); if (!interpolator .getType () .some (type => this .#interpolatorTypes .has (type))) continue; this .interpolators .add (interpolator); for (const route of interpolator ._value_changed .getOutputRoutes ()) { const node = route .getDestinationNode (); if (!(node instanceof X3D .X3DNode)) continue; const field = node .getField (route .getDestinationField ()); this .members .add (node); this .fields .set (field, interpolator); } } for (const interpolator of this .interpolators) interpolator ._value_changed .addRouteCallback (this, () => this .updateMemberList ()); } updateMemberList () { if (this .#changing) return; this .updateMembers (); this .memberList .saveScrollbars (); this .memberList .clearNodes (); this .memberList .addNodes (Array .from (this .members)); this .memberList .restoreScrollbars (); this .requestDrawTimeline (); } // Interpolators #interpolatorTypeNames = new Map ([ [X3D .X3DConstants .SFBool, "BooleanSequencer"], [X3D .X3DConstants .SFInt32, "IntegerSequencer"], [X3D .X3DConstants .SFColor, "ColorInterpolator"], [X3D .X3DConstants .SFFloat, "ScalarInterpolator"], [X3D .X3DConstants .SFRotation, "OrientationInterpolator"], [X3D .X3DConstants .SFVec2f, "PositionInterpolator2D"], [X3D .X3DConstants .SFVec3f, "PositionInterpolator"], [X3D .X3DConstants .MFVec2f, "CoordinateInterpolator2D"], [X3D .X3DConstants .MFVec3f, "CoordinateInterpolator"], // NormalInterpolator ]); #components = new Map ([ [X3D .X3DConstants .BooleanSequencer, 1], [X3D .X3DConstants .IntegerSequencer, 1], [X3D .X3DConstants .ColorInterpolator, 3], [X3D .X3DConstants .ScalarInterpolator, 1], [X3D .X3DConstants .OrientationInterpolator, 4], [X3D .X3DConstants .PositionInterpolator2D, 2], [X3D .X3DConstants .PositionInterpolator, 3], [X3D .X3DConstants .CoordinateInterpolator2D, 2], [X3D .X3DConstants .CoordinateInterpolator, 3], [X3D .X3DConstants .NormalInterpolator, 3], ]); getKeyType () { return this .keyTypeElement .val (); } setKeyType () { const value = this .getKeyType (); this .config .file .keyType = value; // Update interpolators. const keyframes = this .getSelectedKeyframes (); if (keyframes .length) { Editor .undoManager .beginUndo (_("Change Key Type of Selected Keyframes")); for (const { field, interpolator, index } of keyframes) { const keyType = interpolator .getMetaData ("Interpolator/keyType", new X3D .MFString ()); keyType [index] = this .restrictKeyType (field, interpolator, value); Editor .setNodeMetaData (interpolator, "Interpolator/keyType", keyType); } for (const interpolator of new Set (keyframes .map (({ interpolator }) => interpolator))) this .updateInterpolator (interpolator); Editor .undoManager .endUndo (); } } updateKeyType () { if (this .getSelectedKeyframes () .length) { const keyTypes = { CONSTANT: 0, LINEAR: 0, SPLINE: 0, SPLIT: 0, }; for (const { interpolator, index } of this .getSelectedKeyframes ()) { const keyType = interpolator .getMetaData ("Interpolator/keyType", new X3D .MFString ()); ++ keyTypes [keyType [index]]; } const keyType = Object .entries (keyTypes) .find (([key, value]) => value === this .getSelectedKeyframes () .length); this .keyTypeElement .val (keyType ?.[0] ?? "MIXED"); } else { this .keyTypeElement .val (this .config .file .keyType); } } restrictKeyType (field, interpolator, keyType) { switch (field .getType ()) { case X3D .X3DConstants .SFBool: case X3D .X3DConstants .SFInt32: { return "CONSTANT"; } case X3D .X3DConstants .SFColor: { if (keyType .match (/^(?:SPLINE|SPLIT)$/)) return "LINEAR"; return keyType; } case X3D .X3DConstants .MFVec3f: { if (keyType .match (/^(?:SPLINE|SPLIT)$/)) { if (interpolator instanceof X3D .NormalInterpolator) return "LINEAR"; } return keyType; } default: { return keyType; } } } addKeyframes (keyframes) { // Create interpolators. const count = keyframes .reduce ((p, { field }) => p + !this .fields .has (field), 0); if (count === 1) Editor .undoManager .beginUndo (_("Add Interpolator to »%s«"), this .animation .getDisplayName ()); else Editor .undoManager .beginUndo (_("Add Interpolators to »%s«"), this .animation .getDisplayName ()); for (const { node, field, typeName } of keyframes) this .getInterpolator (node, field, typeName) Editor .undoManager .endUndo (); // Add keyframes. if (keyframes .length === 1) Editor .undoManager .beginUndo (_("Add Keyframe to »%s«"), this .animation .getDisplayName ()); else Editor .undoManager .beginUndo (_("Add Keyframes to »%s«"), this .animation .getDisplayName ()); for (const { node, field, typeName } of keyframes) this .addKeyframe (node, field, typeName); Editor .undoManager .endUndo (); } addKeyframe (node, field, typeName) { Editor .undoManager .beginUndo (_("Add Keyframe to »%s«"), this .animation .getDisplayName ()); const interpolator = this .getInterpolator (node, field, typeName), frame = this .getCurrentFrame (), type = this .restrictKeyType (field, interpolator, this .getKeyType ()); switch (field .getType ()) { case X3D .X3DConstants .SFBool: case X3D .X3DConstants .SFInt32: case X3D .X3DConstants .SFColor: case X3D .X3DConstants .SFFloat: case X3D .X3DConstants .SFRotation: case X3D .X3DConstants .SFVec2f: case X3D .X3DConstants .SFVec3f: { this .addKeyframeToInterpolator (interpolator, frame, type, field); break; } case X3D .X3DConstants .MFVec2f: case X3D .X3DConstants .MFVec3f: { if (field .length === 0) break; const keySize = interpolator .getMetaData ("Interpolator/keySize", new X3D .SFInt32 ()); if (keySize .getValue () !== 0 && keySize .getValue () !== field .length) { this .showArraySizeErrorDialog (keySize .getValue ()); break; } keySize .setValue (field .length); Editor .setNodeMetaData (interpolator, "Interpolator/keySize", keySize); const value = Array .from (field) .flatMap (value => Array .from (value)); this .addKeyframeToInterpolator (interpolator, frame, type, value); break; } } this .updateInterpolator (interpolator); Editor .undoManager .endUndo (); } #changing = false; getInterpolator (node, field, typeName) { if (this .fields .has (field)) return this .fields .get (field); typeName ??= this .#interpolatorTypeNames .get (field .getType ()); Editor .undoManager .beginUndo (_("Add Interpolator")); const executionContext = this .animation .getExecutionContext (); if (typeName .includes ("Sequencer")) Editor .addComponent (executionContext .getLocalScene (), "EventUtilities"); else if (typeName .includes ("Interpolator")) Editor .addComponent (executionContext .getLocalScene (), "Interpolation"); const interpolator = executionContext .createNode (typeName, false); interpolator .setup (); this .fields .set (field, interpolator); this .interpolators .add (interpolator); Editor .appendValueToArray (executionContext, this .animation, this .animation ._children, interpolator); Editor .addRoute (executionContext, this .timeSensor, "fraction_changed", interpolator, "set_fraction"); Editor .addRoute (executionContext, interpolator, "value_changed", node, field .getName ()); const name = this .getInterpolatorName (interpolator); Editor .updateNamedNode (executionContext, executionContext .getUniqueName (name), interpolator); Editor .undoManager .endUndo (); // Prevent losing members without interpolator. this .#changing = true; this .browser .nextFrame () .then (() => this .#changing = false); return interpolator; } getInterpolatorName (interpolator) { const route = Array .from (interpolator ._value_changed .getOutputRoutes ()) [0]; if (!route) return; const destinationNode = route .getDestinationNode (), destinationField = route .getDestinationField (), nodeName = destinationNode .getDisplayName () || destinationNode .getTypeName (), fieldName = capitalize (destinationField .replace (/^set_|_changed$/g, ""), true), typeName = interpolator .getTypeName () .match (/(Sequencer|Interpolator)$/) [1]; return `${nodeName}${fieldName}${typeName}`; } removeInterpolator (node, field) { const animation = this .animation, executionContext = animation .getExecutionContext (), interpolator = this .fields .get (field), children = animation ._children .filter (node => node .getValue () !== interpolator); Editor .undoManager .beginUndo (_("Remove Interpolator from »%s«"), animation .getDisplayName ()); Editor .setFieldValue (executionContext, animation, animation ._children, children); this .registerRequestDrawTimeline (); Editor .undoManager .endUndo (); } updateInterpolators () { Editor .undoManager .beginUndo (_("Update Interpolators")); for (const interpolator of this .interpolators) this .updateInterpolator (interpolator) Editor .undoManager .endUndo (); } updateInterpolator (interpolator) { Editor .undoManager .beginUndo (_("Update Interpolator")); switch (interpolator .getType () .at (-1)) { case X3D .X3DConstants .BooleanSequencer: case X3D .X3DConstants .IntegerSequencer: { this .updateSequencer (interpolator); break; } case X3D .X3DConstants .ColorInterpolator: case X3D .X3DConstants .ScalarInterpolator: case X3D .X3DConstants .OrientationInterpolator: case X3D .X3DConstants .PositionInterpolator2D: case X3D .X3DConstants .PositionInterpolator: { this .updateScalarInterpolator (interpolator); break; } case X3D .X3DConstants .CoordinateInterpolator2D: case X3D .X3DConstants .CoordinateInterpolator: case X3D .X3DConstants .NormalInterpolator: { this .updateArrayInterpolator (interpolator); break; } } interpolator ._set_fraction .addEvent (); Editor .undoManager .endUndo (); } updateSequencer (interpolator) { this .resizeInterpolator (interpolator); const components = this .#components .get (interpolator .getType () .at (-1)); const key = interpolator .getMetaData ("Interpolator/key", new X3D .MFInt32 ()); const keyValue = interpolator .getMetaData ("Interpolator/keyValue", new X3D .MFDouble ()); const keyType = interpolator .getMetaData ("Interpolator/keyType", new X3D .MFString ()); keyValue .length = key .length * components; keyType .length = key .length; const size = key .length; const duration = this .getDuration (); const keys = [ ]; const keyValues = [ ]; let i = 0; // index in key let iN = 0; // index in meta data keyValue while (i < size) { if (key [i] < 0 || key [i] > duration) continue; const fraction = key [i] / duration; const value = keyValue [iN]; keys .push (fraction); keyValues .push (value); ++ i; iN += components; } const executionContext = interpolator .getExecutionContext (); Editor .setFieldValue (executionContext, interpolator, interpolator ._key, keys); Editor .setFieldValue (executionContext, interpolator, interpolator ._keyValue, keyValues); this .registerRequestDrawTimeline (); } #vectors = new Map ([ [2, X3D .Vector2], [3, X3D .Vector3], [4, X3D .Vector4], ]); updateScalarInterpolator (interpolator) { this .resizeInterpolator (interpolator); const components = this .#components .get (interpolator .getType () .at (-1)); const key = interpolator .getMetaData ("Interpolator/key", new X3D .MFInt32 ()); const keyValue = interpolator .getMetaData ("Interpolator/keyValue", new X3D .MFDouble ()); const keyType = interpolator .getMetaData ("Interpolator/keyType", new X3D .MFString ()); keyValue .length = key .length * components; keyType .length = key .length; const size = key .length; const duration = this .getDuration (); const keys = [ ]; const keyValues = [ ]; let i = 0; // index in key let iN = 0; // index in meta data keyValue while (i < size) { if (key [i] < 0 || key [i] > duration) { ++ i; continue; } const value = this .getValue (keyValue, iN, components); const fraction = key [i] / duration; let iT = i; if (keyType [iT] === "SPLIT" && iT + 1 < size) ++ iT; switch (keyType [iT]) { case "CONSTANT": { keys .push (fraction); keyValues .push (... value); if (key [i] < duration) { const nextFraction = i === size - 1 ? 1 : key [i + 1] / duration; keys .push (nextFraction); keyValues .push (... value); } break; } case "LINEAR": case "SPLIT": { keys .push (fraction); keyValues .push (... value); break; } case "SPLINE": { const currentKeys = new X3D .MFFloat (); const currentKeyValues = interpolator instanceof X3D .OrientationInterpolator ? new X3D .MFRotation () : components === 1 ? new X3D .MFFloat () : new X3D [`MFVec${components}f`] (); const currentKeyVelocities = currentKeyValues .create (); const Vector = this .#vectors .get (components); for (; i < size; ++ i, iN += components) { let value = this .getValue (keyValue, iN, components); if (interpolator instanceof X3D .ColorInterpolator) value = new X3D .Color3 (... value) .getHSV (); currentKeys .push (key [i]); currentKeyValues .push (components === 1 ? value [0] : new Vector (... value)); if (currentKeys .length === 1) continue; if (keyType [i] !== "SPLINE") break; } if (currentKeys .length < 2) { // This can happen if only the last frame is of type SPLINE. keys .push (fraction); keyValues .push (... value); break; } // currentKeyVelocities .length = currentKeys .length; const closed = currentKeys .at (0) === 0 && currentKeys .at (-1) === duration && (components === 1 ? currentKeyValues .at (0) === currentKeyValues .at (-1) : currentKeyValues .at (0) .equals (currentKeyValues .at (-1))); const normalizeVelocity = false; const spline = interpolator instanceof X3D .OrientationInterpolator ? new X3D .SquadInterpolator () : new X3D [`CatmullRomSplineInterpolator${components}`] (); spline .generate (closed, currentKeys, currentKeyValues, currentKeyVelocities, normalizeVelocity); const length = currentKeys .length - 1; for (let k = 0; k < length; ++ k) { const frames = currentKeys [k + 1] - currentKeys [k]; const fraction = currentKeys [k] / duration; const distance = frames / duration; const framesN = frames + (k + 1 === length && i === key .length); for (let f = 0; f < framesN; ++ f) { const weight = f / frames; let value = spline .interpolate (k, k + 1, weight, currentKeyValues); if (interpolator instanceof X3D .ColorInterpolator) value = new X3D .Color3 () .setHSV (... value); keys .push (fraction + weight * distance); keyValues .push (... (components === 1 ? [value] : value)); } } if (i + 1 !== size) { i -= 1; iN -= components; } break; } } i += 1; iN += components; } const executionContext = interpolator .getExecutionContext (); Editor .setFieldValue (executionContext, interpolator, interpolator ._key, keys); Editor .setFieldValue (executionContext, interpolator, interpolator ._keyValue, keyValues); this .registerRequestDrawTimeline (); } updateArrayInterpolator (interpolator) { this .resizeInterpolator (interpolator); const components = this .#components .get (interpolator .getType () .at (-1)); const key = interpolator .getMetaData ("Interpolator/key", new X3D .MFInt32 ()); const keyValue = interpolator .getMetaData ("Interpolator/keyValue", new X3D .MFDouble ()); const keyType = interpolator .getMetaData ("Interpolator/keyType", new X3D .MFString ()); const keySize = interpolator .getMetaData ("Interpolator/keySize", new X3D .SFInt32 ()); keyValue .length = key .length * components * keySize; keyType .length = key .length; const size = key .length; const duration = this .getDuration (); const keys = [ ]; const keyValues = [ ]; let i = 0; // index in key let iN = 0; // index in meta data keyValue while (i < size) { if (key [i] < 0 && key [i] > duration) { ++ i; continue; } const fraction = key [i] / duration; let iT = i; if (keyType [iT] === "SPLIT" && iT + 1 < size) ++ iT; switch (keyType [iT]) { case "CONSTANT": { const length = components * keySize; keys .push (fraction); for (let a = 0; a < length; a += components) keyValues .push (... this .getValue (keyValue, iN + a, components)); if (key [i] < duration) { const nextFraction = (i === size - 1 ? 1 : key [i + 1] / duration); keys .push (nextFraction); for (let a = 0; a < length; a += components) keyValues .push (... this .getValue (keyValue, iN + a, components)); } break; } case "LINEAR": case "SPLIT": { const length = components * keySize; keys .push (fraction); for (let a = 0; a < length; a += components) keyValues .push (... this .getValue (keyValue, iN + a, components)); break; } case "SPLINE": { const first = keyValues .length; // Generate key. const currentKeys = interpolator ._key .create (); for (; i < size; ++ i) { currentKeys .push (key [i]); if (currentKeys .length === 1) continue; if (keyType [i] !== "SPLINE") break; } if (currentKeys .length < 2) { // This can happen if only the last frame is of type SPLINE. const length = components * keySize; keys .push (fraction); for (let a = 0; a < length; a += components) keyValues .push (... this .getValue (keyValue, iN + a, components)); break; } const length = currentKeys .length - 1; for (let k = 0; k < length; ++ k) { const frames = currentKeys [k + 1] - currentKeys [k]; const fraction = currentKeys [k] / duration; const distance = frames / duration; const framesN = k + 1 === length && i === key .length ? frames + 1 : frames; for (let f = 0; f < framesN; ++ f) { const weight = f / frames; keys .push (fraction + weight * distance); } } // Generate keyValue. for (let a = 0; a < keySize; ++ a) { const currentKeyValues = interpolator ._keyValue .create (); const currentKeyVelocities = interpolator ._keyValue .create (); const Vector = this .#vectors .get (components); for (let i = 0, aiN = iN + a * components; i < currentKeys .length; ++ i, aiN += components * keySize) currentKeyValues .push (new Vector (... this .getValue (keyValue, aiN, components))); // currentKeyVelocities .length = currentKeys .length; const closed = currentKeys .at (0) === 0 && currentKeys .at (-1) === duration && currentKeyValues .at (0) .equals (currentKeyValues .at (-1)); const normalizeVelocity = false; const spline = new X3D [`CatmullRomSplineInterpolator${components}`] (); spline .generate (closed, currentKeys, currentKeyValues, currentKeyVelocities, normalizeVelocity); const length = currentKeys .length - 1; let totalFrames = 0; for (let k = 0; k < length; ++ k) { const frames = currentKeys [k + 1] - currentKeys [k]; const framesN = frames + (k + 1 === length && i === key .length); for (let f = 0; f < framesN; ++ f) { const weight = f / frames; const value = spline .interpolate (k, k + 1, weight, currentKeyValues); const index = first + (a + (totalFrames + f) * keySize) * components; if (index >= keyValues .length) keyValues .length = index + 1; keyValues .splice (index, components, ... value); } totalFrames += frames; } } if (i + 1 !== size) i -= 1; iN += components * keySize * (currentKeys .length - 2); break; } } i += 1; iN += components * keySize; } const executionContext = interpolator .getExecutionContext (); Editor .setFieldValue (executionContext, interpolator, interpolator ._key, keys); Editor .setFieldValue (executionContext, interpolator, interpolator ._keyValue, keyValues); this .registerRequestDrawTimeline (); } getValue (keyValue, index, components) { const value = [ ]; for (let i = 0; i < components; ++ i) value .push (keyValue [index + i]); return value; } resizeInterpolator (interpolator) { const components = this .#components .get (interpolator .getType () .at (-1)); const key = interpolator .getMetaData ("Interpolator/key", new X3D .MFInt32 ()); const keyValue = interpolator .getMetaData ("Interpolator/keyValue", new X3D .MFDouble ()); const keyType = interpolator .getMetaData ("Interpolator/keyType", new X3D .MFString ()); const keySize = interpolator .getMetaData ("Interpolator/keySize", new X3D .SFInt32 (1)); const size = X3D .Algorithm .upperBound (key, 0, key .length, this .getDuration ()); const sizeN = size * components * keySize; // Remove frames greater than duration. key .length = size; keyValue .length = sizeN; keyType .length = size; Editor .setNodeMetaData (interpolator, "Interpolator/key", key); Editor .setNodeMetaData (interpolator, "Interpolator/keyValue", keyValue); Editor .setNodeMetaData (interpolator, "Interpolator/keyType", keyType); if (key .length === 0) Editor .removeNodeMetaData (interpolator, "Interpolator/keySize", new X3D .SFInt32 ()); this .registerRequestDrawTimeline (); } addKeyframeToInterpolator (interpolator, frame, type, value) { const components = this .#components .get (interpolator .getType ()