sunrize 1.11.0 → 1.11.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "sunrize",
3
3
  "productName": "Sunrize X3D Editor",
4
- "version": "1.11.0",
4
+ "version": "1.11.2",
5
5
  "description": "A Multi-Platform X3D Editor",
6
6
  "main": "src/main.js",
7
7
  "bin": {
@@ -78,19 +78,20 @@
78
78
  "url": "https://patreon.com/X_ITE"
79
79
  },
80
80
  "devDependencies": {
81
- "@electron-forge/cli": "^7.9.0",
82
- "@electron-forge/maker-deb": "^7.9.0",
83
- "@electron-forge/maker-dmg": "^7.9.0",
84
- "@electron-forge/maker-rpm": "^7.9.0",
85
- "@electron-forge/maker-squirrel": "^7.9.0",
86
- "@electron-forge/maker-zip": "^7.9.0",
87
- "@electron-forge/publisher-github": "^7.9.0",
81
+ "@electron-forge/cli": "^7.10.2",
82
+ "@electron-forge/maker-deb": "^7.10.2",
83
+ "@electron-forge/maker-dmg": "^7.10.2",
84
+ "@electron-forge/maker-rpm": "^7.10.2",
85
+ "@electron-forge/maker-squirrel": "^7.10.2",
86
+ "@electron-forge/maker-zip": "^7.10.2",
87
+ "@electron-forge/publisher-github": "^7.10.2",
88
88
  "shell-tools": "^1.1.9"
89
89
  },
90
90
  "dependencies": {
91
+ "@vscode/codicons": "^0.0.41",
91
92
  "capitalize": "^2.0.4",
92
93
  "console": "^0.7.2",
93
- "electron": "^38.1.2",
94
+ "electron": "^38.3.0",
94
95
  "electron-prompt": "^1.7.0",
95
96
  "electron-squirrel-startup": "^1.0.1",
96
97
  "electron-tabs": "^1.0.4",
@@ -99,17 +100,17 @@
99
100
  "jquery-ui-dist": "^1.13.3",
100
101
  "jstree": "^3.3.17",
101
102
  "material-icons": "^1.13.14",
102
- "material-symbols": "^0.36.1",
103
+ "material-symbols": "^0.37.0",
103
104
  "md5": "^2.3.0",
104
105
  "mime-types": "^3.0.1",
105
- "monaco-editor": "^0.53.0",
106
+ "monaco-editor": "^0.54.0",
106
107
  "node-localstorage": "^3.0.5",
107
108
  "qtip2": "^3.0.3",
108
109
  "spectrum-colorpicker2": "^2.0.10",
109
110
  "string-similarity": "^4.0.4",
110
111
  "tweakpane": "^3.1.10",
111
112
  "update-electron-app": "^3.1.1",
112
- "x_ite": "^12.1.0",
113
- "x3d-traverse": "^1.0.13"
113
+ "x_ite": "^12.1.2",
114
+ "x3d-traverse": "^1.0.22"
114
115
  }
115
116
  }
@@ -319,7 +319,7 @@ module .exports = class Application
319
319
  },
320
320
  },
321
321
  {
322
- label: _("Save A Copy..."),
322
+ label: _("Save a Copy..."),
323
323
  click: async () =>
324
324
  {
325
325
  const response = await this .showSaveDialog ({ defaultPath: this .currentFile });
@@ -180,10 +180,19 @@ module .exports = class Dashboard extends Interface
180
180
  for (const node of nodes)
181
181
  outlineEditor .expandTo (node, { expandObject: true, expandAll: true });
182
182
 
183
- const elements = nodes .map (node => outlineEditor .sceneGraph .find (`.node[node-id=${node .getId ()}]`));
183
+ const elements = nodes .map (node => outlineEditor .sceneGraph
184
+ .find (`:is(.node, .imported-node.proxy)[node-id="${node .getId ()}"]`));
184
185
 
185
- for (const [i, element] of elements .entries ())
186
- outlineEditor .selectNodeElement (element, { add: i > 0 });
186
+ outlineEditor .deselectAll ({ target: false });
187
+
188
+ for (const element of elements)
189
+ {
190
+ if (element .is (".node"))
191
+ outlineEditor .selectNodeElement (element, { add: true });
192
+
193
+ else if (element .is (".imported-node.proxy"))
194
+ outlineEditor .selectPrimaryElement (element, { add: true });
195
+ }
187
196
 
188
197
  // Scroll element into view.
189
198
  // Hide scrollbars during scroll to prevent overlay issue.
@@ -55,6 +55,7 @@ module .exports = class Document extends Interface
55
55
  */
56
56
  async initialize ()
57
57
  {
58
+ $("html") .attr ("platform", process .platform);
58
59
  $("body") .addClass ("modal");
59
60
 
60
61
  // Actions
@@ -184,6 +185,10 @@ module .exports = class Document extends Interface
184
185
 
185
186
  // Restore
186
187
 
188
+ const pkg = require ("../../package.json");
189
+
190
+ console .info (`Welcome to ${pkg .productName} v${pkg .version}.`);
191
+
187
192
  await this .restoreFile ();
188
193
 
189
194
  if (!this .isInitialScene)
@@ -319,8 +324,8 @@ module .exports = class Document extends Interface
319
324
 
320
325
  const activeElement = this .activeElement;
321
326
 
322
- if (activeElement .is ("input"))
323
- return activeElement .attr ("type") === undefined || activeElement .attr ("type") === "text";
327
+ if (activeElement .is ("input:not([type]), input[type=text]"))
328
+ return true;
324
329
 
325
330
  if (activeElement .is ("textarea"))
326
331
  return true;
@@ -451,16 +456,16 @@ Viewpoint {
451
456
  pkg = require ("../../package.json"),
452
457
  generator = scene .getMetaData ("generator") ?.filter (value => !value .startsWith (pkg .productName)) ?? [ ];
453
458
 
454
- generator .push (`${pkg .productName} V${pkg .version}, ${pkg .homepage}`);
455
-
456
- if (!scene .getMetaData ("created"))
457
- scene .setMetaData ("created", new Date () .toUTCString ());
459
+ generator .unshift (`${pkg .productName} V${pkg .version}, ${pkg .homepage}`);
458
460
 
459
461
  if (!scene .getMetaData ("creator") ?.some (value => value .includes (this .fullname)))
460
462
  scene .addMetaData ("creator", this .fullname);
461
463
 
462
- scene .setMetaData ("generator", generator);
464
+ if (!scene .getMetaData ("created"))
465
+ scene .setMetaData ("created", new Date () .toUTCString ());
466
+
463
467
  scene .setMetaData ("modified", new Date () .toUTCString ());
468
+ scene .setMetaData ("generator", generator);
464
469
 
465
470
  // Save source code.
466
471
 
@@ -630,7 +635,7 @@ Viewpoint {
630
635
  if (this .activeElementIsMonacoEditor ())
631
636
  return;
632
637
 
633
- this .sidebar .outlineEditor .copyNodes (true);
638
+ this .sidebar .outlineEditor .copyNodes ();
634
639
  return false;
635
640
  }
636
641
 
@@ -36,8 +36,8 @@ module .exports = new class Hierarchy extends Interface
36
36
  }
37
37
 
38
38
  #targetTypes = new Set ([
39
- X3D .X3DConstants .X3DShapeNode,
40
39
  X3D .X3DConstants .Inline,
40
+ X3D .X3DConstants .X3DShapeNode,
41
41
  ]);
42
42
 
43
43
  target (node)
@@ -70,10 +70,17 @@ module .exports = new class Hierarchy extends Interface
70
70
 
71
71
  const node = object .getValue () .valueOf ();
72
72
 
73
- if (!node .getType () .some (type => this .#targetTypes .has (type)))
73
+ if (!node .getType () .some (type => this .#targetTypes .has (type)) &&
74
+ !(node instanceof X3D .X3DImportedNodeProxy))
75
+ {
74
76
  continue;
77
+ }
78
+
79
+ const target = node .getType () .includes (X3D .X3DConstants .X3DShapeNode)
80
+ ? node .getGeometry () ?.valueOf () ?? node
81
+ : node;
75
82
 
76
- targets .push (node .getGeometry ?.() ?.valueOf () ?? node);
83
+ targets .push (target);
77
84
  }
78
85
 
79
86
  for (const hierarchy of this .#find (targets))
@@ -40,7 +40,7 @@ $.fn.addPrototypePopover = function (executionContext, type)
40
40
  .appendTo (content);
41
41
 
42
42
  const nameInput = $("<input></input>")
43
- .attr ("placeholder", _("Enter name"))
43
+ .attr ("placeholder", _("Enter a name"))
44
44
  .appendTo (content);
45
45
 
46
46
  // Create tooltip.
@@ -55,7 +55,7 @@ $.fn.addPrototypePopover = function (executionContext, type)
55
55
  electron .shell .beep ();
56
56
  nameInput .highlight ();
57
57
  })
58
- .on ("keydown.addPrototypePopover", event =>
58
+ .on ("keydown", event =>
59
59
  {
60
60
  if (event .key !== "Enter")
61
61
  return;
@@ -12,7 +12,8 @@ $.fn.animationPropertiesPopover = function (editor)
12
12
  {
13
13
  // Create content.
14
14
 
15
- const content = $("<div></div>");
15
+ const content = $("<div></div>")
16
+ .css ("width", "200px");
16
17
 
17
18
  $("<span></span>")
18
19
  .text (_("Frames"))
@@ -35,6 +36,7 @@ $.fn.animationPropertiesPopover = function (editor)
35
36
  .attr ("type", "number")
36
37
  .attr ("step", 1)
37
38
  .attr ("min", 1)
39
+ .attr ("max", 60)
38
40
  .attr ("placeholder", _("Enter frame rate"))
39
41
  .val (editor .getFrameRate ())
40
42
  .on ("change input", updateTime)
@@ -84,6 +86,15 @@ $.fn.animationPropertiesPopover = function (editor)
84
86
  events: {
85
87
  show: (event, api) =>
86
88
  {
89
+ content .children () .off () .on ("keydown", (event) =>
90
+ {
91
+ if (event .key !== "Enter")
92
+ return;
93
+
94
+ applyButton .trigger ("click");
95
+ api .toggle (false);
96
+ })
97
+
87
98
  applyButton .on ("click", (event) =>
88
99
  {
89
100
  api .toggle (false);
@@ -47,9 +47,12 @@ module .exports = class Dialog extends Interface
47
47
  {
48
48
  // Set default config values.
49
49
 
50
+ defaults .minSize ??= defaults .size;
51
+
50
52
  this .config .file .setDefaultValues (Object .assign ({
51
53
  position: undefined,
52
54
  size: [400, 250],
55
+ minSize: [400, 250],
53
56
  },
54
57
  defaults));
55
58
  }
@@ -58,8 +61,8 @@ module .exports = class Dialog extends Interface
58
61
  {
59
62
  this .element .dialog ({
60
63
  position: { ... this .config .file .position, of: $("body") },
61
- minWidth: this .config .file .getDefaultValue ("size") [0],
62
- minHeight: this .config .file .getDefaultValue ("size") [1],
64
+ minWidth: this .config .file .minSize [0],
65
+ minHeight: this .config .file .minSize [1],
63
66
  width: this .config .file .size [0],
64
67
  height: this .config .file .size [1],
65
68
  })
@@ -19,7 +19,7 @@ $.fn.editNodePopover = function (node)
19
19
  .appendTo (content);
20
20
 
21
21
  const nameInput = $("<input></input>")
22
- .attr ("placeholder", _("Enter name"))
22
+ .attr ("placeholder", _("Enter a name"))
23
23
  .appendTo (content);
24
24
 
25
25
  if (node instanceof X3D .X3DProtoDeclaration)
@@ -97,7 +97,7 @@ $.fn.editUserDefinedFieldPopover = function (executionContext, node, field = -1)
97
97
  .appendTo (content);
98
98
 
99
99
  const nameInput = $("<input></input>")
100
- .attr ("placeholder", _("Enter name"))
100
+ .attr ("placeholder", _("Enter a name"))
101
101
  .appendTo (content);
102
102
 
103
103
  if (field instanceof X3D .X3DField)
@@ -139,7 +139,7 @@ module .exports = class AnimationEditor extends Interface
139
139
  .attr ("min", 0)
140
140
  .attr ("max", 0)
141
141
  .attr ("title", _("Current frame."))
142
- .css ("width", "55px")
142
+ .css ("width", "70px")
143
143
  .appendTo (this .toolbar)
144
144
  .on ("change input", () => this .setCurrentFrame (this .getCurrentFrame ()));
145
145
 
@@ -166,7 +166,7 @@ module .exports = class AnimationEditor extends Interface
166
166
  this .timeElement = $("<span></span>")
167
167
  .addClass (["text", "right"])
168
168
  .attr ("title", _("Current frame time (hours:minutes:seconds:frames)."))
169
- .css ("top", "7px")
169
+ .css ("top", "7.5px")
170
170
  .css ("margin-right", "6px")
171
171
  .text (this .formatFrames (0, 10))
172
172
  .appendTo (this .toolbar);
@@ -315,8 +315,10 @@ module .exports = class AnimationEditor extends Interface
315
315
  this .timeSensor ._isActive .removeInterest ("set_active", this);
316
316
  this .timeSensor ._fraction_changed .removeInterest ("set_fraction", this);
317
317
 
318
- this .timeSensor ._evenLive = false;
319
- this .timeSensor ._range = [0, 0, 1];
318
+ this .timeSensor ._evenLive = false;
319
+ this .timeSensor ._range = [0, 0, 1];
320
+ this .timeSensor ._resumeTime = 0;
321
+ this .timeSensor ._pauseTime = 0;
320
322
 
321
323
  if (this .timeSensor ._loop .getValue () && this .timeSensor ._isActive .getValue ())
322
324
  {
@@ -358,8 +360,6 @@ module .exports = class AnimationEditor extends Interface
358
360
  this .set_loop (this .timeSensor ._loop);
359
361
  this .set_active (this .timeSensor ._isActive);
360
362
 
361
- this .updateRange ();
362
-
363
363
  // Show Member List
364
364
 
365
365
  this .animation ._children .addInterest ("updateMemberList", this);
@@ -405,8 +405,9 @@ module .exports = class AnimationEditor extends Interface
405
405
 
406
406
  this .setSelection (require ("../Application/Selection"));
407
407
  this .zoomFit ();
408
- this .setCurrentFrame (0);
409
408
  this .requestDrawTimeline ();
409
+
410
+ this .browser .nextFrame () .then (() => this .setCurrentFrame (0));
410
411
  }
411
412
 
412
413
  enableIcons (enabled)
@@ -704,9 +705,12 @@ module .exports = class AnimationEditor extends Interface
704
705
 
705
706
  for (const route of interpolator ._value_changed .getOutputRoutes ())
706
707
  {
707
- const
708
- node = route .getDestinationNode (),
709
- field = node .getField (route .getDestinationField ());
708
+ const node = route .getDestinationNode ();
709
+
710
+ if (!(node instanceof X3D .X3DNode))
711
+ continue;
712
+
713
+ const field = node .getField (route .getDestinationField ());
710
714
 
711
715
  this .members .add (node);
712
716
  this .fields .set (field, interpolator);
@@ -1916,15 +1920,24 @@ module .exports = class AnimationEditor extends Interface
1916
1920
 
1917
1921
  if (selectionRange [0] === selectionRange [1])
1918
1922
  {
1919
- this .timeSensor ._range [1] = 0;
1920
- this .timeSensor ._range [2] = 1;
1923
+ if (this .timeSensor ._range [1] !== 0 && this .timeSensor ._range [2] !== 1)
1924
+ {
1925
+ this .timeSensor ._range [1] = 0;
1926
+ this .timeSensor ._range [2] = 1;
1927
+ }
1921
1928
  }
1922
1929
  else
1923
1930
  {
1924
- const duration = this .getDuration ();
1931
+ const
1932
+ duration = this .getDuration (),
1933
+ begin = selectionRange [0] / duration,
1934
+ end = selectionRange [1] / duration;
1925
1935
 
1926
- this .timeSensor ._range [1] = selectionRange [0] / duration;
1927
- this .timeSensor ._range [2] = selectionRange [1] / duration;
1936
+ if (this .timeSensor ._range [1] !== begin && this .timeSensor ._range [2] !== end)
1937
+ {
1938
+ this .timeSensor ._range [1] = begin;
1939
+ this .timeSensor ._range [2] = end;
1940
+ }
1928
1941
  }
1929
1942
  }
1930
1943
 
@@ -2319,10 +2332,7 @@ module .exports = class AnimationEditor extends Interface
2319
2332
 
2320
2333
  const pickedKeyframes = this .pickKeyframes ();
2321
2334
 
2322
- if (!pickedKeyframes .length)
2323
- this .setCurrentFrame (this .getFrameFromPointer (this .pointer .x));
2324
-
2325
- this .startMovingFrame = this .getFrameFromPointer (this .pointer .x);
2335
+ this .startMovingFrame = this .getFrameFromPointer ();
2326
2336
 
2327
2337
  if (event .shiftKey && pickedKeyframes .length)
2328
2338
  {
@@ -2330,34 +2340,49 @@ module .exports = class AnimationEditor extends Interface
2330
2340
  }
2331
2341
  else if (event .shiftKey)
2332
2342
  {
2343
+ const frame = this .getFrameFromPointer ();
2344
+
2345
+ this .timeSensor ._pauseTime = Date .now () / 1000;
2346
+
2333
2347
  this .setPickedKeyframes ([ ]);
2334
2348
  this .setSelectedKeyframes ([ ]);
2335
- this .expandSelectionRange (this .getCurrentFrame ());
2349
+ this .expandSelectionRange (frame);
2336
2350
  }
2337
2351
  else
2338
2352
  {
2353
+ this .timeSensor ._pauseTime = Date .now () / 1000;
2354
+
2339
2355
  if (!pickedKeyframes .length || !pickedKeyframes .every (p => this .getSelectedKeyframes () .some (s => this .equalKeyframe (p, s))))
2340
2356
  {
2357
+ // There are not picked keyframes or an unselected keyframe is picked.
2358
+
2359
+ const frame = this .getFrameFromPointer ();
2360
+
2361
+ if (!pickedKeyframes .length)
2362
+ this .setCurrentFrame (frame);
2363
+
2341
2364
  this .setPickedKeyframes (pickedKeyframes);
2342
2365
  this .setSelectedKeyframes (pickedKeyframes);
2343
- this .setSelectionRange (this .getCurrentFrame (), this .getCurrentFrame ());
2366
+ this .setSelectionRange (frame, frame);
2344
2367
  }
2345
2368
  else
2346
2369
  {
2370
+ // A selected keyframe is picked.
2371
+
2347
2372
  this .setPickedKeyframes (this .getSelectedKeyframes ());
2348
2373
  }
2349
2374
  }
2350
2375
 
2351
- this .timeSensor ._pauseTime = Date .now () / 1000;
2352
2376
  break;
2353
2377
  }
2354
2378
  }
2355
2379
  }
2356
2380
 
2357
- on_mouseup ()
2381
+ on_mouseup (event)
2358
2382
  {
2359
2383
  $(document) .off (".AnimationEditor");
2360
2384
 
2385
+ this .updatePointer (event);
2361
2386
  this .removeAutoScroll ();
2362
2387
 
2363
2388
  if (this .#movingKeyframesOffset)
@@ -2425,9 +2450,9 @@ module .exports = class AnimationEditor extends Interface
2425
2450
  this .requestDrawTimeline ();
2426
2451
  }
2427
2452
 
2428
- getFrameFromPointer (pointerX)
2453
+ getFrameFromPointer (x = this .pointer .x)
2429
2454
  {
2430
- const frame = Math .round ((pointerX - this .getTranslation ()) / this .getScale ());
2455
+ const frame = Math .round ((x - this .getTranslation ()) / this .getScale ());
2431
2456
 
2432
2457
  return X3D .Algorithm .clamp (frame, 0, this .getDuration ());
2433
2458
  }
@@ -2492,10 +2517,6 @@ module .exports = class AnimationEditor extends Interface
2492
2517
 
2493
2518
  this .#defaultIntegers .length = 0;
2494
2519
 
2495
- const
2496
- translation = this .getTranslation (),
2497
- scale = this .getScale ();
2498
-
2499
2520
  const
2500
2521
  key = interpolator .getMetaData ("Interpolator/key", this .#defaultIntegers),
2501
2522
  first = X3D .Algorithm .lowerBound (key, 0, key .length, firstFrame),
@@ -2627,10 +2648,13 @@ module .exports = class AnimationEditor extends Interface
2627
2648
 
2628
2649
  setSelectionRange (start, end)
2629
2650
  {
2651
+ if (start !== end || this .#selectionRange [0] !== this .#selectionRange [1])
2652
+ this .setCurrentFrame (this .getFrameFromPointer ());
2653
+
2630
2654
  this .#selectionRange = [start, end];
2631
2655
 
2632
- this .selectKeyframesInRange ();
2633
2656
  this .updateRange ();
2657
+ this .selectKeyframesInRange ();
2634
2658
  this .requestDrawTimeline ();
2635
2659
  }
2636
2660
 
@@ -2674,20 +2698,20 @@ module .exports = class AnimationEditor extends Interface
2674
2698
  {
2675
2699
  // Move keyframes or select range.
2676
2700
 
2701
+ const frame = this .getFrameFromPointer ();
2702
+
2677
2703
  if (this .getPickedKeyframes () .length)
2678
2704
  {
2679
- this .#movingKeyframesOffset = this .getFrameFromPointer (this .pointer .x) - this .startMovingFrame;
2705
+ this .#movingKeyframesOffset = frame - this .startMovingFrame;
2680
2706
  }
2681
2707
  else
2682
2708
  {
2683
2709
  // Select range.
2684
2710
 
2685
- this .setCurrentFrame (this .getFrameFromPointer (this .pointer .x));
2686
-
2687
2711
  if (event ?.shiftKey)
2688
- this .expandSelectionRange (this .getCurrentFrame ());
2712
+ this .expandSelectionRange (frame);
2689
2713
  else
2690
- this .setSelectionRange (this .#selectionRange [0], this .getCurrentFrame ());
2714
+ this .setSelectionRange (this .#selectionRange [0], frame);
2691
2715
  }
2692
2716
  }
2693
2717
 
@@ -2952,43 +2976,25 @@ module .exports = class AnimationEditor extends Interface
2952
2976
  {
2953
2977
  case "main":
2954
2978
  {
2955
- const allSelected = Array .from (this .fields .keys ())
2956
- .every (field => this .getSelectedKeyframes () .some (keyframe => field === keyframe .field));
2957
-
2958
- if (allSelected)
2959
- {
2960
- for (const field of this .fields .keys ())
2961
- {
2962
- this .drawSelectedKeyframes (context, field, bottom - this .TRACK_PADDING, red);
2963
- break;
2964
- }
2965
- }
2979
+ const fields = new Set (this .fields .keys ())
2966
2980
 
2981
+ this .drawSelectedKeyframes (context, fields, bottom - this .TRACK_PADDING, red);
2967
2982
  break;
2968
2983
  }
2969
2984
  case "node":
2970
2985
  {
2971
2986
  const
2972
2987
  node = item .data ("node"),
2973
- fields = node .getFields () .filter (field => this .fields .has (field));
2974
-
2975
- const allSelected = fields
2976
- .every (field => this .getSelectedKeyframes () .some (keyframe => field === keyframe .field));
2977
-
2978
- if (allSelected)
2979
- {
2980
- for (const field of fields)
2981
- {
2982
- this .drawSelectedKeyframes (context, field, bottom - this .TRACK_PADDING, red);
2983
- break;
2984
- }
2985
- }
2988
+ fields = new Set (node .getFields ());
2986
2989
 
2990
+ this .drawSelectedKeyframes (context, fields, bottom - this .TRACK_PADDING, red);
2987
2991
  break;
2988
2992
  }
2989
2993
  case "field":
2990
2994
  {
2991
- this .drawSelectedKeyframes (context, item .data ("field"), bottom - this .TRACK_PADDING, red);
2995
+ const fields = new Set ([item .data ("field")]);
2996
+
2997
+ this .drawSelectedKeyframes (context, fields, bottom - this .TRACK_PADDING, red);
2992
2998
  break;
2993
2999
  }
2994
3000
  }
@@ -3012,6 +3018,7 @@ module .exports = class AnimationEditor extends Interface
3012
3018
  }
3013
3019
 
3014
3020
  #defaultIntegers = new X3D .MFInt32 ();
3021
+ #frames = [ ];
3015
3022
 
3016
3023
  drawKeyframes (context, field, firstFrame, lastFrame, bottom, color)
3017
3024
  {
@@ -3022,10 +3029,7 @@ module .exports = class AnimationEditor extends Interface
3022
3029
 
3023
3030
  this .#defaultIntegers .length = 0;
3024
3031
 
3025
- const
3026
- left = this .getLeft (),
3027
- translation = this .getTranslation (),
3028
- scale = this .getScale ();
3032
+ const left = this .getLeft ();
3029
3033
 
3030
3034
  const
3031
3035
  key = interpolator .getMetaData ("Interpolator/key", this .#defaultIntegers),
@@ -3044,24 +3048,52 @@ module .exports = class AnimationEditor extends Interface
3044
3048
  }
3045
3049
  }
3046
3050
 
3047
- drawSelectedKeyframes (context, currentField, bottom, selectedColor)
3051
+ drawSelectedKeyframes (context, fields, bottom, selectedColor)
3048
3052
  {
3053
+ if (!this .getSelectedKeyframes () .length)
3054
+ return;
3055
+
3049
3056
  const
3050
- left = this .getLeft (),
3051
- translation = this .getTranslation (),
3052
- scale = this .getScale ();
3057
+ left = this .getLeft (),
3058
+ frames = this .#frames;
3053
3059
 
3054
- for (const { field, interpolator, index } of this .getSelectedKeyframes ())
3060
+ frames .length = 0;
3061
+
3062
+ // Count keyframes to avoid overdrawing.
3063
+
3064
+ for (const field of fields)
3055
3065
  {
3056
- if (field !== currentField)
3066
+ const interpolator = this .fields .get (field);
3067
+
3068
+ if (!interpolator)
3057
3069
  continue;
3058
3070
 
3059
3071
  this .#defaultIntegers .length = 0;
3060
3072
 
3073
+ const key = interpolator .getMetaData ("Interpolator/key", this .#defaultIntegers);
3074
+
3075
+ for (const frame of key)
3076
+ frames [frame] = (frames [frame] ?? 0) + 1;
3077
+ }
3078
+
3079
+ // Draw keyframes.
3080
+
3081
+ for (const { field, interpolator, index } of this .getSelectedKeyframes ())
3082
+ {
3083
+ if (!fields .has (field))
3084
+ continue
3085
+
3086
+ this .#defaultIntegers .length = 0;
3087
+
3061
3088
  const key = interpolator .getMetaData ("Interpolator/key", this .#defaultIntegers);
3062
- const frame = key [index] + this .#movingKeyframesOffset;
3063
- const x = Math .floor (left + this .getPointerFromFrame (frame));
3064
- const x1 = x - (this .FRAME_SIZE / 2) + 0.5;
3089
+ const frame = key [index];
3090
+
3091
+ if (-- frames [frame])
3092
+ continue;
3093
+
3094
+ const moving = frame + this .#movingKeyframesOffset;
3095
+ const x = Math .floor (left + this .getPointerFromFrame (moving));
3096
+ const x1 = x - (this .FRAME_SIZE / 2) + 0.5;
3065
3097
 
3066
3098
  context .fillStyle = selectedColor;
3067
3099
 
@@ -142,7 +142,7 @@ module .exports = class AnimationMembersList extends Interface
142
142
 
143
143
  addNodes (nodes)
144
144
  {
145
- nodes = nodes .filter (node => !this .#nodes .includes (node));
145
+ nodes = nodes .map (node => node .valueOf ()) .filter (node => !this .#nodes .includes (node .valueOf ()));
146
146
 
147
147
  let i = this .#nodes .length;
148
148
 
@@ -286,7 +286,7 @@ module .exports = class AnimationMembersList extends Interface
286
286
 
287
287
  removeNodes (nodes)
288
288
  {
289
- for (const node of nodes)
289
+ for (const node of nodes .map (node => node .valueOf ()))
290
290
  {
291
291
  this .#list .find (`li[node-id=${node .getId ()}]`) .remove ();
292
292