sunrize 1.7.63 → 1.8.0

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.
Files changed (40) hide show
  1. package/package.json +4 -4
  2. package/src/Application/ActionKeys.js +3 -3
  3. package/src/Application/Application.js +1 -1
  4. package/src/Application/Dashboard.js +87 -6
  5. package/src/Application/Document.js +144 -24
  6. package/src/Application/Hierarchy.js +268 -0
  7. package/src/Application/Selection.js +6 -6
  8. package/src/Components/Grouping/StaticGroup.js +1 -1
  9. package/src/Components/Grouping/Switch.js +34 -0
  10. package/src/Components/Navigation/Collision.js +70 -0
  11. package/src/Components/Navigation/LOD.js +34 -0
  12. package/src/Editors/Library.js +1 -1
  13. package/src/Editors/OutlineEditor.js +6 -2
  14. package/src/Editors/OutlineRouteGraph.js +4 -4
  15. package/src/Editors/OutlineView.js +210 -63
  16. package/src/Tools/Core/X3DNodeTool.js +2 -0
  17. package/src/Tools/EnvironmentalSensor/X3DEnvironmentalSensorNodeTool.x3d +1 -0
  18. package/src/Tools/Geometry2D/Arc2DTool.js +1 -0
  19. package/src/Tools/Geometry2D/ArcClose2DTool.js +1 -0
  20. package/src/Tools/Geometry2D/Circle2DTool.js +1 -0
  21. package/src/Tools/Geometry2D/Disk2DTool.js +2 -0
  22. package/src/Tools/Geometry2D/Rectangle2DTool.js +1 -0
  23. package/src/Tools/Geometry3D/BoxTool.js +1 -0
  24. package/src/Tools/Geometry3D/ConeTool.js +1 -0
  25. package/src/Tools/Geometry3D/CylinderTool.js +1 -0
  26. package/src/Tools/Geometry3D/SphereTool.js +1 -0
  27. package/src/Tools/Grouping/X3DBoundedObjectTool.x3d +28 -12
  28. package/src/Tools/Grouping/X3DTransformNodeTool.x3d +30 -12
  29. package/src/Tools/Lighting/X3DLightNodeTool.x3d +1 -0
  30. package/src/Tools/Navigation/X3DViewpointNodeTool.x3d +1 -0
  31. package/src/Tools/SnapTool/X3DSnapNodeTool.js +8 -6
  32. package/src/Tools/Sound/ListenerPointSourceTool.x3d +1 -0
  33. package/src/Tools/Sound/SoundTool.x3d +5 -0
  34. package/src/Tools/Sound/SpatialSoundTool.x3d +2 -1
  35. package/src/Tools/TextureProjection/X3DTextureProjectorNodeTool.x3d +1 -0
  36. package/src/Undo/Editor.js +1 -1
  37. package/src/Undo/UndoManager.js +4 -4
  38. package/src/X3D.js +1 -1
  39. package/src/assets/themes/default-template.css +6 -0
  40. package/src/assets/themes/default.css +6 -0
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "sunrize",
3
3
  "productName": "Sunrize X3D Editor",
4
- "version": "1.7.63",
4
+ "version": "1.8.0",
5
5
  "description": "A Multi-Platform X3D Editor",
6
6
  "main": "src/main.js",
7
7
  "bin": {
@@ -90,7 +90,7 @@
90
90
  "dependencies": {
91
91
  "capitalize": "^2.0.4",
92
92
  "console": "^0.7.2",
93
- "electron": "^35.1.2",
93
+ "electron": "^35.1.4",
94
94
  "electron-prompt": "^1.7.0",
95
95
  "electron-squirrel-startup": "^1.0.1",
96
96
  "electron-tabs": "^1.0.4",
@@ -99,7 +99,7 @@
99
99
  "jquery-ui-dist": "^1.13.3",
100
100
  "jstree": "^3.3.17",
101
101
  "material-icons": "^1.13.14",
102
- "material-symbols": "^0.29.2",
102
+ "material-symbols": "^0.29.3",
103
103
  "md5": "^2.3.0",
104
104
  "mime-types": "^3.0.1",
105
105
  "monaco-editor": "^0.50.0",
@@ -109,7 +109,7 @@
109
109
  "string-similarity": "^4.0.4",
110
110
  "tweakpane": "^3.1.10",
111
111
  "update-electron-app": "^3.1.1",
112
- "x_ite": "^11.5.0",
112
+ "x_ite": "^11.5.1",
113
113
  "x3d-traverse": "^1.0.11"
114
114
  }
115
115
  }
@@ -58,7 +58,7 @@ module .exports = new class ActionKeys
58
58
  if (this .value === value)
59
59
  return;
60
60
 
61
- this .processInterests ();
61
+ this .#processInterests ();
62
62
  }
63
63
 
64
64
  onkeyup (event)
@@ -94,7 +94,7 @@ module .exports = new class ActionKeys
94
94
  if (this .value === value)
95
95
  return;
96
96
 
97
- this .processInterests ();
97
+ this .#processInterests ();
98
98
  }
99
99
 
100
100
  #interests = new Map ();
@@ -109,7 +109,7 @@ module .exports = new class ActionKeys
109
109
  this .#interests .delete (key);
110
110
  }
111
111
 
112
- processInterests ()
112
+ #processInterests ()
113
113
  {
114
114
  for (const callback of this .#interests .values ())
115
115
  callback (this .value);
@@ -288,7 +288,7 @@ module .exports = class Application
288
288
  },
289
289
  { type: "separator" },
290
290
  {
291
- label: _("Default Play Button State"),
291
+ label: _("Enable Browser Update on Load"),
292
292
  type: "checkbox",
293
293
  checked: this .config .browserUpdate,
294
294
  click: () =>
@@ -9,11 +9,12 @@ const
9
9
 
10
10
  module .exports = class Dashboard extends Interface
11
11
  {
12
- constructor (element)
12
+ constructor (element, document)
13
13
  {
14
14
  super ("Sunrize.Dashboard.");
15
15
 
16
- this .toolbar = element;
16
+ this .document = document;
17
+ this .toolbar = element;
17
18
 
18
19
  this .setup ();
19
20
  }
@@ -44,12 +45,41 @@ module .exports = class Dashboard extends Interface
44
45
 
45
46
  $("<span></span>") .addClass ("separator") .appendTo (this .toolbar);
46
47
 
47
- this .viewAllButton = $("<span></span>")
48
+ const hierarchy = require ("./Hierarchy");
49
+
50
+ this .upButton = $("<span></span>")
51
+ .addClass (["material-icons", "disabled"])
52
+ .attr ("title", _("Select parent node(s)."))
53
+ .css ({ transform: "rotate(-90deg) scaleX(0.8)", "margin-top": "-6px", "margin-bottom": "-7px" })
54
+ .text ("play_arrow")
55
+ .appendTo (this .toolbar)
56
+ .on ("click", () => this .selectParent ());
57
+
58
+ this .downButton = $("<span></span>")
59
+ .addClass (["material-icons", "disabled"])
60
+ .attr ("title", _("Select child node(s)."))
61
+ .css ({ transform: "rotate(90deg) scaleX(0.8)", "margin-top": "-7px", "margin-bottom": "-6px" })
62
+ .text ("play_arrow")
63
+ .appendTo (this .toolbar)
64
+ .on ("click", () => this .selectChild ());
65
+
66
+ hierarchy .addInterest (this, () => this .onHierarchy ());
67
+
68
+ $("<span></span>") .addClass ("separator") .appendTo (this .toolbar);
69
+
70
+ this .viewSelectedButton = $("<span></span>")
48
71
  .addClass (["material-symbols-outlined"])
49
72
  .attr ("title", _("Look at selected objects."))
50
73
  .text ("center_focus_strong")
51
74
  .appendTo (this .toolbar)
52
- .on ("click", () => this .viewAll ());
75
+ .on ("click", () => this .viewAll (true));
76
+
77
+ this .viewAllButton = $("<span></span>")
78
+ .addClass (["material-symbols-outlined"])
79
+ .attr ("title", _("Look at all objects in active layer."))
80
+ .text ("zoom_out_map")
81
+ .appendTo (this .toolbar)
82
+ .on ("click", () => this .viewAll (false));
53
83
 
54
84
  this .straightenButton = $("<span></span>")
55
85
  .addClass (["material-symbols-outlined", "active"])
@@ -130,13 +160,64 @@ module .exports = class Dashboard extends Interface
130
160
  this .playButton .removeClass ("active");
131
161
  }
132
162
 
133
- viewAll ()
163
+ selectParent ()
164
+ {
165
+ this .selectHierarchy ("up");
166
+ }
167
+
168
+ selectChild ()
169
+ {
170
+ this .selectHierarchy ("down");
171
+ }
172
+
173
+ selectHierarchy (direction)
174
+ {
175
+ const
176
+ hierarchy = require ("./Hierarchy"),
177
+ outlineEditor = this .document .sidebar .outlineEditor,
178
+ nodes = hierarchy [direction] ();
179
+
180
+ for (const node of nodes)
181
+ outlineEditor .expandTo (node, { expandObject: true, expandAll: true });
182
+
183
+ const elements = nodes .map (node => outlineEditor .sceneGraph .find (`.node[node-id=${node .getId ()}]`));
184
+
185
+ for (const [i, element] of elements .entries ())
186
+ outlineEditor .selectNodeElement (element, { add: i > 0 });
187
+
188
+ // Scroll element into view.
189
+ // Hide scrollbars during scroll to prevent overlay issue.
190
+
191
+ outlineEditor .treeView .css ("overflow", "hidden");
192
+
193
+ elements [0] ?.[0] ?.scrollIntoView ({ block: "center", inline: "start", behavior: "smooth" });
194
+ $(window) .scrollTop (0);
195
+
196
+ setTimeout (() => outlineEditor .treeView .css ("overflow", ""), 1000);
197
+ }
198
+
199
+ onHierarchy ()
200
+ {
201
+ const hierarchy = require ("./Hierarchy");
202
+
203
+ if (hierarchy .canUp ())
204
+ this .upButton .removeClass ("disabled");
205
+ else
206
+ this .upButton .addClass ("disabled");
207
+
208
+ if (hierarchy .canDown ())
209
+ this .downButton .removeClass ("disabled");
210
+ else
211
+ this .downButton .addClass ("disabled");
212
+ }
213
+
214
+ viewAll (selected)
134
215
  {
135
216
  const
136
217
  selection = require ("./Selection"),
137
218
  nodes = selection .nodes;
138
219
 
139
- if (nodes .length)
220
+ if (selected && nodes .length)
140
221
  {
141
222
  const
142
223
  executionContext = this .browser .currentScene,
@@ -28,9 +28,6 @@ module .exports = class Document extends Interface
28
28
  {
29
29
  super ("Sunrize.Document.");
30
30
 
31
- // Add X3D to window to provide access in Script nodes.
32
- window .X3D = X3D;
33
-
34
31
  // Globals
35
32
 
36
33
  this .config .global .setDefaultValues ({
@@ -41,7 +38,7 @@ module .exports = class Document extends Interface
41
38
 
42
39
  this .verticalSplitter = new Splitter ($("#vertical-splitter"), "vertical");
43
40
  this .horizontalSplitter = new Splitter ($("#horizontal-splitter"), "horizontal");
44
- this .secondaryToolbar = new Dashboard ($("#secondary-toolbar"));
41
+ this .secondaryToolbar = new Dashboard ($("#secondary-toolbar"), this);
45
42
  this .footer = new Footer ($("#footer"));
46
43
  this .sidebar = new Sidebar ($("#sidebar"));
47
44
 
@@ -166,9 +163,10 @@ module .exports = class Document extends Interface
166
163
 
167
164
  // Connect for Snap Target and Snap Source.
168
165
 
169
- $(this .browser .element .shadowRoot) .find ("canvas")
166
+ $(this .browser .element)
170
167
  .on ("mousedown", event => this .onmousedown (event))
171
- .on ("mouseup", event => this .onmouseup (event));
168
+ .on ("mouseup", event => this .onsnaptool (event))
169
+ .on ("mouseup", event => this .onselect (event));
172
170
 
173
171
  // Load components.
174
172
 
@@ -178,6 +176,9 @@ module .exports = class Document extends Interface
178
176
  // Modify nodes.
179
177
 
180
178
  this .browser .updateConcreteNode (require ("../Components/Grouping/StaticGroup"));
179
+ this .browser .updateConcreteNode (require ("../Components/Grouping/Switch"));
180
+ this .browser .updateConcreteNode (require ("../Components/Navigation/Collision"));
181
+ this .browser .updateConcreteNode (require ("../Components/Navigation/LOD"));
181
182
 
182
183
  require ("../Components");
183
184
 
@@ -939,48 +940,79 @@ Viewpoint {
939
940
  }
940
941
  }
941
942
 
943
+ #select = false;
944
+ #pointer = new X3D .Vector2 ();
942
945
  #snapTarget = null;
943
946
  #snapSource = null;
944
947
 
945
948
  async onmousedown (event)
946
949
  {
947
- if (event .button !== 2)
950
+ this .#select = false;
951
+
952
+ if (!this .secondaryToolbar .arrowButton .hasClass ("active"))
948
953
  return;
949
954
 
950
- switch (ActionKeys .value)
955
+ switch (event .button)
951
956
  {
952
- case ActionKeys .None:
957
+ case 0:
953
958
  {
954
- if (this .#snapTarget ?._visible .getValue ())
955
- break;
959
+ if (event .shiftKey && (event .ctrlKey || event .metaKey))
960
+ return;
956
961
 
957
- this .activateSnapTarget (true);
962
+ this .#pointer .assign (this .browser .getPointerFromEvent (event));
958
963
 
959
- await this .#snapTarget .getToolInstance ();
964
+ if (this .browser .touch (... this .#pointer))
965
+ {
966
+ if (this .browser .getHit () .sensors .size)
967
+ return;
968
+
969
+ this .#select = true;
970
+ }
971
+ else
972
+ {
973
+ this .#select = true;
974
+ }
960
975
 
961
- this .#snapTarget .onmousedown (event, true);
962
976
  break;
963
977
  }
964
- case ActionKeys .Option:
978
+ case 2:
965
979
  {
966
- if (this .#snapSource ?._visible .getValue ())
967
- break;
980
+ switch (ActionKeys .value)
981
+ {
982
+ case ActionKeys .None:
983
+ {
984
+ if (this .#snapTarget ?._visible .getValue ())
985
+ break;
986
+
987
+ this .activateSnapTarget (true);
968
988
 
969
- this .activateSnapSource (true);
989
+ await this .#snapTarget .getToolInstance ();
970
990
 
971
- await this .#snapSource .getToolInstance ();
991
+ this .#snapTarget .onmousedown (event, true);
992
+ break;
993
+ }
994
+ case ActionKeys .Option:
995
+ {
996
+ if (this .#snapSource ?._visible .getValue ())
997
+ break;
998
+
999
+ this .activateSnapSource (true);
1000
+
1001
+ await this .#snapSource .getToolInstance ();
1002
+
1003
+ this .#snapSource .onmousedown (event, true);
1004
+ break;
1005
+ }
1006
+ }
972
1007
 
973
- this .#snapSource .onmousedown (event, true);
974
1008
  break;
975
1009
  }
976
1010
  }
1011
+
977
1012
  }
978
1013
 
979
- async onmouseup (event)
1014
+ async onsnaptool (event)
980
1015
  {
981
- if (ActionKeys .value & ActionKeys .Control)
982
- event .button = 2;
983
-
984
1016
  if (event .button !== 2)
985
1017
  return;
986
1018
 
@@ -991,6 +1023,94 @@ Viewpoint {
991
1023
  this .#snapTarget ?.onmouseup (event);
992
1024
  }
993
1025
 
1026
+ onselect (event)
1027
+ {
1028
+ if (!this .secondaryToolbar .arrowButton .hasClass ("active"))
1029
+ return;
1030
+
1031
+ if (event .button !== 0)
1032
+ return;
1033
+
1034
+ if (!this .#select)
1035
+ return;
1036
+
1037
+ const pointer = this .browser .getPointerFromEvent (event);
1038
+
1039
+ if (this .#pointer .distance (pointer) > this .browser .getRenderingProperty ("ContentScale"))
1040
+ return;
1041
+
1042
+ // Stop event propagation.
1043
+
1044
+ event .preventDefault ();
1045
+
1046
+ // Select or deselect.
1047
+
1048
+ const outlineEditor = this .sidebar .outlineEditor;
1049
+
1050
+ if (!this .browser .touch (... pointer))
1051
+ {
1052
+ outlineEditor .deselectAll ();
1053
+ return;
1054
+ }
1055
+
1056
+ // Select.
1057
+
1058
+ const
1059
+ shapeNode = this .browser .getHit () .shapeNode,
1060
+ geometryTool = shapeNode .getGeometry () ?.getTool (),
1061
+ tool = geometryTool ?? shapeNode .getExecutionContext () .getOuterNode () ?.getTool (),
1062
+ node = tool ?? shapeNode;
1063
+
1064
+ outlineEditor .expandTo (node, { expandObject: true, expandAll: true });
1065
+
1066
+ let elements = outlineEditor .sceneGraph .find (`.node[node-id=${node .getId ()}]`);
1067
+
1068
+ if (!elements .length)
1069
+ return;
1070
+
1071
+ if (outlineEditor .isEditable (elements))
1072
+ {
1073
+ if (tool)
1074
+ {
1075
+ elements = Array .from (elements);
1076
+ }
1077
+ else
1078
+ {
1079
+ const parentElements = Array .from (elements) .flatMap (element =>
1080
+ {
1081
+ const parentElements = Array .from ($(element) .parent () .closest (".node", outlineEditor .sceneGraph));
1082
+
1083
+ return parentElements .length ? parentElements : element;
1084
+ });
1085
+
1086
+ elements = parentElements .map ((element, i) => outlineEditor .getNode ($(element)) .getType () .includes (X3D .X3DConstants .X3DGroupingNode) ? parentElements [i] : elements [i]);
1087
+ }
1088
+ }
1089
+ else
1090
+ {
1091
+ while (!outlineEditor .isEditable (elements))
1092
+ {
1093
+ elements .jstree ("close_node", elements);
1094
+ elements = elements .parent () .closest (".node, .scene", outlineEditor .sceneGraph);
1095
+ }
1096
+
1097
+ elements = Array .from (elements);
1098
+ }
1099
+
1100
+ for (const [i, element] of elements .entries ())
1101
+ outlineEditor .selectNodeElement ($(element), { add: (event .shiftKey || event .metaKey) || i > 0, target: true });
1102
+
1103
+ // Scroll element into view.
1104
+ // Hide scrollbars during scroll to prevent overlay issue.
1105
+
1106
+ outlineEditor .treeView .css ("overflow", "hidden");
1107
+
1108
+ elements [0] ?.scrollIntoView ({ block: "center", inline: "start", behavior: "smooth" });
1109
+ $(window) .scrollTop (0);
1110
+
1111
+ setTimeout (() => outlineEditor .treeView .css ("overflow", ""), 1000);
1112
+ }
1113
+
994
1114
  activateSnapTarget (visible)
995
1115
  {
996
1116
  const SnapTarget = require ("../Tools/SnapTool/SnapTarget");
@@ -0,0 +1,268 @@
1
+ const
2
+ X3D = require ("../X3D"),
3
+ Interface = require ("./Interface"),
4
+ Traverse = require ("x3d-traverse") (X3D);
5
+
6
+ module .exports = new class Hierarchy extends Interface
7
+ {
8
+ #target = null;
9
+ #nodes = [ ];
10
+ #hierarchies = [ ];
11
+
12
+ constructor ()
13
+ {
14
+ super ("Sunrize.Hierarchy.");
15
+
16
+ this .setup ();
17
+ }
18
+
19
+ configure ()
20
+ {
21
+ this .executionContext ?.sceneGraph_changed .removeInterest ("update", this);
22
+
23
+ this .executionContext = this .browser .currentScene;
24
+
25
+ this .executionContext .sceneGraph_changed .addInterest ("update", this);
26
+ }
27
+
28
+ update ()
29
+ {
30
+ const
31
+ target = this .#target,
32
+ nodes = this .#nodes;
33
+
34
+ this .target (target ?.isLive () ? target : null);
35
+ nodes .forEach (node => this .add (node));
36
+ }
37
+
38
+ #targetTypes = new Set ([
39
+ X3D .X3DConstants .X3DShapeNode,
40
+ X3D .X3DConstants .Inline,
41
+ ]);
42
+
43
+ target (node)
44
+ {
45
+ node = node ?.valueOf () ?? null;
46
+
47
+ this .#target = node;
48
+ this .#nodes = [ ];
49
+
50
+ if (!node)
51
+ {
52
+ this .#hierarchies = [ ];
53
+ }
54
+ else if (!node .getType () .includes (X3D .X3DConstants .X3DShapeNode))
55
+ {
56
+ this .#hierarchies = [ ];
57
+
58
+ let flags = Traverse .NONE;
59
+
60
+ flags |= Traverse .PROTO_DECLARATIONS;
61
+ flags |= Traverse .PROTO_DECLARATION_BODY;
62
+ flags |= Traverse .ROOT_NODES;
63
+
64
+ for (const object of Traverse .traverse (node, flags))
65
+ {
66
+ if (!(object instanceof X3D .SFNode))
67
+ continue;
68
+
69
+ const node = object .getValue () .valueOf ();
70
+
71
+ if (!node .getType () .some (type => this .#targetTypes .has (type)))
72
+ continue;
73
+
74
+ const target = node .getGeometry ?.() ?.valueOf () ?? node;
75
+
76
+ for (const hierarchy of this .#find (target))
77
+ this .#hierarchies .push (hierarchy);
78
+ }
79
+
80
+ if (!this .#hierarchies .length)
81
+ this .#hierarchies = this .#find (node);
82
+ }
83
+ else
84
+ {
85
+ const target = node .getType () .includes (X3D .X3DConstants .X3DShapeNode)
86
+ ? node .getGeometry () ?.valueOf () ?? node
87
+ : node;
88
+
89
+ this .#hierarchies = this .#find (target);
90
+ }
91
+
92
+ this .#processInterests ();
93
+ }
94
+
95
+ set (node)
96
+ {
97
+ node = node ?.valueOf () ?? null;
98
+
99
+ if (!this .#has (node))
100
+ return;
101
+
102
+ this .#nodes = [node];
103
+
104
+ this .#processInterests ();
105
+ }
106
+
107
+ add (node)
108
+ {
109
+ node = node ?.valueOf () ?? null;
110
+
111
+ if (!this .#has (node))
112
+ return;
113
+
114
+ if (this .#nodes .includes (node))
115
+ return;
116
+
117
+ this .#nodes .push (node);
118
+
119
+ this .#processInterests ();
120
+ }
121
+
122
+ remove (node)
123
+ {
124
+ node = node ?.valueOf () ?? null;
125
+
126
+ this .#nodes = this .#nodes .filter (n => n !== node);
127
+
128
+ this .#processInterests ();
129
+ }
130
+
131
+ clear ()
132
+ {
133
+ this .#target = null;
134
+ this .#nodes = [ ];
135
+ this .#hierarchies = [ ];
136
+
137
+ this .#processInterests ();
138
+ }
139
+
140
+ #find (target)
141
+ {
142
+ if (!target)
143
+ return [ ];
144
+
145
+ // Find target node.
146
+
147
+ let flags = Traverse .NONE;
148
+
149
+ flags |= Traverse .PROTO_DECLARATIONS;
150
+ flags |= Traverse .PROTO_DECLARATION_BODY;
151
+ flags |= Traverse .ROOT_NODES;
152
+
153
+ return Array .from (this .executionContext .find (target, flags),
154
+ hierarchy => hierarchy .filter (object => object instanceof X3D .SFNode)
155
+ .map (node => node .getValue () .valueOf ()));
156
+ }
157
+
158
+ #has (node)
159
+ {
160
+ return this .#hierarchies .some (hierarchy => hierarchy .includes (node));
161
+ }
162
+
163
+ #indices (node)
164
+ {
165
+ return this .#hierarchies .map (hierarchy => hierarchy .indexOf (node));
166
+ }
167
+
168
+ up ()
169
+ {
170
+ this .#nodes = this .#nodes .flatMap (node => this .#indices (node) .map (index =>
171
+ {
172
+ return index - 1 >= 0 ? index - 1 : index;
173
+ })
174
+ .map ((index, i) => this .#hierarchies [i] [index])
175
+ .filter (node => node));
176
+
177
+ this .#nodes = Array .from (new Set (this .#nodes));
178
+
179
+ // Combine to most highest node.
180
+
181
+ const indices = Array .from (this .#hierarchies, hierarchy => hierarchy .length);
182
+
183
+ for (const node of this .#nodes)
184
+ {
185
+ for (const [i, index] of this .#indices (node) .entries ())
186
+ {
187
+ if (index < 0)
188
+ continue;
189
+
190
+ indices [i] = Math .min (indices [i], index);
191
+ }
192
+ }
193
+
194
+ this .#nodes = indices .map ((index, i) => this .#hierarchies [i] [index]) .filter (node => node);
195
+ this .#nodes = Array .from (new Set (this .#nodes));
196
+
197
+ // Propagate change.
198
+
199
+ this .#processInterests ();
200
+
201
+ return this .#nodes;
202
+ }
203
+
204
+ down ()
205
+ {
206
+ this .#nodes = this .#nodes .flatMap (node => this .#indices (node) .map ((index, i) =>
207
+ {
208
+ return index >= 0 && index + 1 < this .#hierarchies [i] .length ? index + 1 : index;
209
+ })
210
+ .map ((index, i) => this .#hierarchies [i] [index])
211
+ .filter (node => node));
212
+
213
+ this .#nodes = Array .from (new Set (this .#nodes));
214
+
215
+ // Combine to most lowest node.
216
+
217
+ const indices = Array .from (this .#hierarchies, () => -1);
218
+
219
+ for (const node of this .#nodes)
220
+ {
221
+ for (const [i, index] of this .#indices (node) .entries ())
222
+ {
223
+ if (index < 0)
224
+ continue;
225
+
226
+ indices [i] = Math .max (indices [i], index);
227
+ }
228
+ }
229
+
230
+ this .#nodes = indices .map ((index, i) => this .#hierarchies [i] [index]) .filter (node => node);
231
+ this .#nodes = Array .from (new Set (this .#nodes));
232
+
233
+ // Propagate change.
234
+
235
+ this .#processInterests ();
236
+
237
+ return this .#nodes;
238
+ }
239
+
240
+ canUp ()
241
+ {
242
+ return this .#nodes .some (node => this .#indices (node) .some (index => index > 0));
243
+ }
244
+
245
+ canDown ()
246
+ {
247
+ return this .#nodes .some (node => this .#indices (node)
248
+ .some ((index, i) => index >= 0 && index < this .#hierarchies [i] .length - 1));
249
+ }
250
+
251
+ #interest = new Map ();
252
+
253
+ addInterest (key, callback)
254
+ {
255
+ this .#interest .set (key, callback);
256
+ }
257
+
258
+ removeInterest (key)
259
+ {
260
+ this .#interest .delete (key);
261
+ }
262
+
263
+ #processInterests ()
264
+ {
265
+ for (const callback of this .#interest .values ())
266
+ callback (this .nodes);
267
+ }
268
+ }