UNPKG

sunrize

Version:

Sunrize — A Multi-Platform X3D Editor

661 lines (519 loc) 18 kB
"use strict"; const $ = require ("jquery"), electron = require ("electron"), X3D = require ("../X3D"), Interface = require ("../Application/Interface"), _ = require ("../Application/GetText"); const _expanded = Symbol (); module .exports = class AnimationMembersList extends Interface { #editor; #nodeList; #list; #nodes; #animation; #timeSensor; constructor (editor, element) { super ("Sunrize.AnimationMembersList."); this .#editor = editor; this .#nodeList = element; this .#list = $("<ul></ul>") .appendTo (this .#nodeList); this .#nodes = [ ]; electron .ipcRenderer .on ("animation-members-list", (event, key, ... args) => this [key] (... args)); this .addMain (); this .setup (); } #executionContext; configure () { this .#executionContext ?.sceneGraph_changed .removeInterest ("set_sceneGraph", this); this .#executionContext = this .browser .currentScene; this .#executionContext .sceneGraph_changed .addInterest ("set_sceneGraph", this); this .set_sceneGraph (); } set_sceneGraph () { this .removeNodes (this .#nodes .filter (node => !node .isLive ())); } // Scrollbars Handling #scrollTop; #scrollLeft; saveScrollbars () { this .#scrollTop = this .#nodeList .scrollTop (); this .#scrollLeft = this .#nodeList .scrollLeft (); } restoreScrollbars () { this .#nodeList .scrollTop (this .#scrollTop); this .#nodeList .scrollLeft (this .#scrollLeft); } // Animation Handling setAnimation (animation, timeSensor) { this .#timeSensor ?._isPaused .removeInterest ("connectNodes", this); this .#timeSensor ?._isActive .removeInterest ("connectNodes", this); this .#animation = animation; this .#timeSensor = timeSensor; this .#animation [_expanded] ??= Symbol (); this .#timeSensor ._isPaused .addInterest ("connectNodes", this); this .#timeSensor ._isActive .addInterest ("connectNodes", this); } setAnimationName (name) { this .animationName .text (name); } // List Elements Handling addMain () { const typeNameElement = $("<span></span>") .addClass ("type-name") .text (_("Animation")), nameElement = $("<span></span>") .addClass ("name") .text ("My"), fieldList = $("<ul></ul>"); this .animationName = nameElement; const listItem = $("<li></li>") .addClass ("main") .appendTo (this .#list); const applyButton = $("<span></span>") .addClass (["apply", "material-icons", "button", "off", "disabled"]) .attr ("title", _("Add keyframe(s).")) .text ("check_box") .on ("click", () => this .addKeyframesToMain ()); const removeButton = $("<span></span>") .addClass (["material-icons-outlined", "button"]) .attr ("title", _("Close animation.")) .text ("cancel") .on ("click", () => this .#editor .closeAnimation ()); const item = $("<div></div>") .attr ("type", "main") .addClass (["main", "item"]) .append (typeNameElement) .append (document .createTextNode (" ")) .append (nameElement) .append (document .createTextNode (" ")) .append (applyButton) .append (document .createTextNode (" ")) .append (removeButton) .appendTo (listItem); item .on ("mouseenter", () => item .addClass ("hover")) .on ("mouseleave", () => item .removeClass ("hover")); fieldList .appendTo (listItem); } clearNodes () { this .removeNodes (this .#nodes); } addNodes (nodes) { nodes = nodes .map (node => node .valueOf ()) .filter (node => !this .#nodes .includes (node .valueOf ())); let i = this .#nodes .length; for (const node of nodes) { const typeNameElement = $("<span></span>") .addClass ("type-name") .text (node .getTypeName ()), nameElement = $("<span></span>") .addClass ("name") .text (this .getName (node)), fieldList = $("<ul></ul>"); const listItem = $("<li></li>") .attr ("node-id", node .getId ()) .addClass ("node") .appendTo (this .#list); const applyButton = $("<span></span>") .addClass (["apply", "material-icons", "button", "off", "disabled"]) .attr ("title", _("Add keyframe(s).")) .text ("check_box") .on ("click", () => this .addKeyframesToNode (node)); const expanded = node .getUserData (this .#animation [_expanded]) || !this .hasInterpolators (node); node .setUserData (this .#animation [_expanded], expanded); const expandButton = $("<span></span>") .addClass (["material-icons-outlined", "button"]) .addClass (expanded ? "on" : "off") .attr ("title", _("Show all fields.")) .text ("expand_circle_down") .on ("click", () => this .toggleExpand (expandButton, fieldList, node)); const item = $("<div></div>") .data ("i", i ++) .attr ("type", "node") .addClass (["node", "item"]) .data ("node", node) .append ($("<img></img>") .addClass ("icon") .attr ("src", "../images/OutlineEditor/Node/X3DBaseNode.svg")) .append (typeNameElement) .append (document .createTextNode (" ")) .append (nameElement) .append (document .createTextNode (" ")) .append (applyButton) .append (document .createTextNode (" ")) .append (expandButton) .on ("contextmenu", () => this .contextMenuForNode (node)) .appendTo (listItem); item .on ("mouseenter", () => item .addClass ("hover")) .on ("mouseleave", () => item .removeClass ("hover")); fieldList .appendTo (listItem); this .createFieldElements (fieldList, node); node .typeName_changed .addInterest ("set_typeName", this, typeNameElement, node); node .name_changed .addInterest ("set_name", this, nameElement, node); } this .#nodes .push (... nodes); } #fieldTypes = new Set ([ X3D .X3DConstants .SFBool, X3D .X3DConstants .SFColor, X3D .X3DConstants .SFFloat, X3D .X3DConstants .SFInt32, X3D .X3DConstants .SFRotation, X3D .X3DConstants .SFVec2f, X3D .X3DConstants .SFVec3f, X3D .X3DConstants .MFVec2f, X3D .X3DConstants .MFVec3f ]); createFieldElements (fieldList, node) { const expanded = node .getUserData (this .#animation [_expanded]) || !this .hasInterpolators (node); node .setUserData (this .#animation [_expanded], expanded); let i = 0; for (const field of node .getFields ()) { if (!expanded && !this .#editor .fields .has (field)) continue; if (!field .isInput ()) continue; if (!this .#fieldTypes .has (field .getType ())) continue; const listItem = $("<li></li>") .attr ("node-id", node .getId ()) .attr ("field-id", field .getId ()) .addClass ("field") .appendTo (fieldList); const iconElement = $("<img></img>") .attr ("title", field .getTypeName ()) .addClass ("icon") .attr ("src", `../images/OutlineEditor/Fields/${field .getTypeName()}.svg`); const nameElement = $("<span></span>") .addClass ("field-name") .text (field .getName ()); const applyButton = $("<span></span>") .addClass (["apply", "material-icons", "button", "off"]) .attr ("title", _("Add keyframe.")) .text ("check_box") .on ("click", () => this .addKeyframeToField (node, field)); const item = $("<div></div>") .data ("i", i ++ ) .attr ("type", "field") .addClass (["field", "item"]) .data ("node", node) .data ("field", field) .append (iconElement) .append (nameElement) .append (document .createTextNode (" ")) .append (applyButton) .on ("contextmenu", () => this .contextMenuForField (node, field)) .appendTo (listItem); if (this .#editor .fields .has (field)) item .addClass ("bold"); item .on ("mouseenter", () => item .addClass ("hover")) .on ("mouseleave", () => item .removeClass ("hover")); } this .connectNode (node, !this .isRunning ()); } removeNodes (nodes) { for (const node of nodes .map (node => node .valueOf ())) { this .#list .find (`li[node-id=${node .getId ()}]`) .remove (); node .typeName_changed .removeInterest ("set_typeName", this); node .name_changed .removeInterest ("set_name", this); this .connectNode (node, false); } this .#nodes = this .#nodes .filter (node => !nodes .includes (node)); this .checkApply (); } getName (node) { let name = node .getDisplayName () || _("<unnamed>"), outerNode = node .getExecutionContext () .getOuterNode (); while (outerNode instanceof X3D .X3DProtoDeclaration) { name = outerNode .getName () + "." + name; outerNode = outerNode .getExecutionContext () .getOuterNode (); } return name; } set_typeName (element, node) { element .text (node .getTypeName ()); } set_name (element, node) { element .text (this .getName (node)); } // Timeline Handling getTrackOffsets () { const listTop = Math .floor (this .#nodeList .offset () .top), listHeight = Math .floor (this .#nodeList .parent () .height ()), items = this .#nodeList .find (".item"), offsets = [ ]; for (const element of items) { const item = $(element), height = Math .round (item .outerHeight ()), top = Math .round (item .offset () .top) - listTop, bottom = top + height; if (bottom < 0) continue; if (top > listHeight) continue; offsets .push ({ item, top, bottom, height }); } return offsets; } contextMenuForNode (node) { this .#node = node; const menu = [ { label: _("Remove Member from Animation"), args: ["removeMember"], }, ]; electron .ipcRenderer .send ("context-menu", "animation-members-list", menu); } removeMember () { this .#editor .removeMembers ([this .#node]); } contextMenuForField (node, field) { this .#node = node; this .#field = field; const menu = [ { label: _("Remove Interpolator from Animation"), args: ["removeInterpolator"], }, ]; electron .ipcRenderer .send ("context-menu", "animation-members-list", menu); } removeInterpolator () { this .#editor .removeInterpolator (this .#node, this .#field); } toggleExpand (expandButton, fieldList, node) { node .setUserData (this .#animation [_expanded], !node .getUserData (this .#animation [_expanded])); expandButton .removeClass (["on", "off"]) .addClass (node .getUserData (this .#animation [_expanded]) ? "on" : "off"); fieldList .empty (); this .createFieldElements (fieldList, node); this .#editor .requestDrawTimeline (); } // Apply Button Handling addKeyframesToMain () { const keyframes = [ ]; const mainItem = this .#list .find ("> .main"); if (mainItem .find (".apply.green") .length) { const fieldItems = this .#list .find (`.field > .item`); for (const element of fieldItems) { const fieldItem = $(element); if (!fieldItem .find (".apply.green") .length) continue; const node = fieldItem .data ("node"), field = fieldItem .data ("field"); keyframes .push ({ node, field }); } } else { for (const field of this .#editor .fields .keys ()) { const fieldItem = this .#list .find (`.field[field-id=${field .getId ()}] > .item`), node = fieldItem .data ("node"); keyframes .push ({ node, field }); } } this .#editor .addKeyframes (keyframes); for (const { field } of keyframes) this .toggleApply (field, false); } addKeyframesToNode (node) { const keyframes = [ ]; const nodeItem = this .#list .find (`.node[node-id=${node .getId ()}]`); if (nodeItem .find (".apply.green") .length) { const fieldItems = this .#list .find (`.field[node-id=${node .getId ()}] > .item`); for (const element of fieldItems) { const fieldItem = $(element); if (!fieldItem .find (".apply.green") .length) continue; const field = fieldItem .data ("field"); keyframes .push ({ node, field }); } } else { for (const field of node .getFields ()) { if (!this .#editor .fields .has (field)) continue; keyframes .push ({ node, field }); } } this .#editor .addKeyframes (keyframes); for (const { field } of keyframes) this .toggleApply (field, false); } #node; #field; addKeyframeToField (node, field) { this .#node = node; this .#field = field; if (field .getType () === X3D .X3DConstants .MFVec3f && !this .#editor .fields .has (field)) { const menu = [ { label: _("CoordinateInterpolator"), args: ["addKeyframeForType", "CoordinateInterpolator"], }, { label: _("NormalInterpolator"), args: ["addKeyframeForType", "NormalInterpolator"], }, ]; electron .ipcRenderer .send ("context-menu", "animation-members-list", menu); } else { this .addKeyframeForType (); } } addKeyframeForType (typeName) { this .#editor .addKeyframes ([{ node: this .#node, field: this .#field, typeName }]); this .toggleApply (this .#field, false); } isRunning () { return this .#timeSensor ._isActive .getValue () && !this .#timeSensor ._isPaused .getValue () } connectNodes () { if (this .isRunning ()) { this .removeApply (); } else { for (const node of this .#nodes) this .connectNode (node, true); } } connectNode (node, connect) { if (connect) { node .addInterest ("checkApply", this); this .checkApply (); } else { node .removeInterest ("checkApply", this); } } removeApply () { this .#list .find (".apply") .removeClass ("green") .addClass ("off"); } checkApply () { for (const [field, interpolator] of this .#editor .fields) this .toggleApply (field, !interpolator ._value_changed .equals (field)); if (!this .#editor .fields .size) this .toggleMainApply (); } toggleApply (field, value) { // Update field. const fieldItem = this .#list .find (`[field-id=${field .getId ()}]`); if (value) { fieldItem .find ("> .item .apply") .removeClass ("off") .addClass ("green"); } else { fieldItem .find ("> .item .apply") .removeClass ("green") .addClass ("off"); } if (this .#editor .fields .has (field)) fieldItem .find ("> .item") .addClass ("bold"); else fieldItem .find ("> .item") .removeClass ("bold"); // Update node. const nodeItem = fieldItem .closest (".node"); if (nodeItem .find (".field .apply.green") .length) { nodeItem .find ("> .item .apply") .removeClass (["off", "disabled"]) .addClass ("green"); } else { const enabled = this .hasInterpolators (nodeItem .find ("> .item") .data ("node")); nodeItem .find ("> .item .apply") .removeClass ("green") .addClass ("off"); nodeItem .find ("> .item .apply") .removeClass ("disabled") .addClass (enabled ? [ ] : ["disabled"]); } // Update main. this .toggleMainApply (); } toggleMainApply () { const mainItem = this .#list .find ("> .main"); if (this .#list .find (".field .apply.green") .length) { mainItem .find ("> .item .apply") .removeClass (["off", "disabled"]) .addClass ("green"); } else { const enabled = Array .from (this .#list .find (".node > .item"), item => $(item)) .some (nodeItem => this .hasInterpolators (nodeItem .data ("node"))); mainItem .find ("> .item .apply") .removeClass ("green") .addClass ("off"); mainItem .find ("> .item .apply") .removeClass ("disabled") .addClass (enabled ? [ ] : ["disabled"]); } } hasInterpolators (node) { return node ?.getFields () .some (field => this .#editor .fields .has (field)); } };