three-cad-viewer 4.3.4 → 4.3.6

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 (59) hide show
  1. package/dist/scene/clipping.d.ts +6 -0
  2. package/dist/three-cad-viewer.esm.js +20 -5
  3. package/dist/three-cad-viewer.esm.js.map +1 -1
  4. package/dist/three-cad-viewer.esm.min.js +1 -1
  5. package/dist/three-cad-viewer.js +20 -5
  6. package/dist/three-cad-viewer.min.js +1 -1
  7. package/package.json +2 -3
  8. package/src/_version.ts +0 -1
  9. package/src/camera/camera.ts +0 -445
  10. package/src/camera/controls/CADOrbitControls.ts +0 -241
  11. package/src/camera/controls/CADTrackballControls.ts +0 -598
  12. package/src/camera/controls.ts +0 -380
  13. package/src/core/patches.ts +0 -16
  14. package/src/core/studio-manager.ts +0 -652
  15. package/src/core/types.ts +0 -892
  16. package/src/core/viewer-state.ts +0 -784
  17. package/src/core/viewer.ts +0 -4821
  18. package/src/index.ts +0 -151
  19. package/src/rendering/environment.ts +0 -840
  20. package/src/rendering/light-detection.ts +0 -327
  21. package/src/rendering/material-factory.ts +0 -735
  22. package/src/rendering/material-presets.ts +0 -289
  23. package/src/rendering/raycast.ts +0 -291
  24. package/src/rendering/room-environment.ts +0 -192
  25. package/src/rendering/studio-composer.ts +0 -577
  26. package/src/rendering/studio-floor.ts +0 -108
  27. package/src/rendering/texture-cache.ts +0 -324
  28. package/src/rendering/tree-model.ts +0 -542
  29. package/src/rendering/triplanar.ts +0 -329
  30. package/src/scene/animation.ts +0 -343
  31. package/src/scene/axes.ts +0 -108
  32. package/src/scene/bbox.ts +0 -223
  33. package/src/scene/clipping.ts +0 -640
  34. package/src/scene/grid.ts +0 -864
  35. package/src/scene/nestedgroup.ts +0 -1444
  36. package/src/scene/objectgroup.ts +0 -866
  37. package/src/scene/orientation.ts +0 -259
  38. package/src/scene/render-shape.ts +0 -634
  39. package/src/tools/cad_tools/measure.ts +0 -811
  40. package/src/tools/cad_tools/select.ts +0 -100
  41. package/src/tools/cad_tools/tools.ts +0 -231
  42. package/src/tools/cad_tools/ui.ts +0 -454
  43. package/src/tools/cad_tools/zebra.ts +0 -369
  44. package/src/types/html.d.ts +0 -5
  45. package/src/types/n8ao.d.ts +0 -28
  46. package/src/types/three-augmentation.d.ts +0 -60
  47. package/src/ui/display.ts +0 -3295
  48. package/src/ui/index.html +0 -505
  49. package/src/ui/info.ts +0 -177
  50. package/src/ui/slider.ts +0 -206
  51. package/src/ui/toolbar.ts +0 -347
  52. package/src/ui/treeview.ts +0 -945
  53. package/src/utils/decode-instances.ts +0 -233
  54. package/src/utils/font.ts +0 -60
  55. package/src/utils/gpu-tracker.ts +0 -265
  56. package/src/utils/logger.ts +0 -92
  57. package/src/utils/sizeof.ts +0 -116
  58. package/src/utils/timer.ts +0 -69
  59. package/src/utils/utils.ts +0 -446
@@ -1,945 +0,0 @@
1
- import * as THREE from "three";
2
- import { KeyMapper } from "../utils/utils.js";
3
- import { logger } from "../utils/logger.js";
4
- import { TreeModel, States } from "../rendering/tree-model.js";
5
- import type { TreeNode, TreeData, StateValue, IconIndex } from "../rendering/tree-model.js";
6
-
7
- /** Navigation icons for expand/collapse */
8
- const NAV_ICONS = {
9
- expanded: "\u25BE",
10
- collapsed: "\u25B8",
11
- };
12
-
13
- /** Icon class names for each state [unselected, selected, mixed, disabled] */
14
- const VIEW_ICONS = [
15
- ["shape_no", "shape", "shape_mix", "shape_empty"],
16
- ["mesh_no", "mesh", "mesh_mix", "mesh_empty"],
17
- ];
18
-
19
- /** Offset for visibility calculations */
20
- const SCROLL_OFFSET = 12;
21
-
22
- /** Icon indices for iteration (typed tuple for state access) */
23
- const ICON_INDICES = [0, 1] as const;
24
-
25
- /**
26
- * Callback types for TreeView.
27
- */
28
- type ObjectHandler = (
29
- path: string,
30
- state: StateValue,
31
- iconNumber: number,
32
- propagate: boolean,
33
- notify: boolean
34
- ) => void;
35
-
36
- type PickHandler = (
37
- parentPath: string,
38
- name: string,
39
- meta: boolean,
40
- shift: boolean,
41
- alt: boolean,
42
- extra: THREE.Vector3 | null,
43
- nodeType: "leaf" | "node" | string | null,
44
- fromTree: boolean
45
- ) => void;
46
-
47
- type UpdateHandler = (flag: boolean) => void;
48
- type NotificationHandler = () => void;
49
- type ColorGetter = (path: string) => string | null;
50
-
51
- /**
52
- * A tree viewer component with lazy loading of large trees.
53
- * Uses TreeModel for data management and handles DOM rendering/events.
54
- */
55
- class TreeView {
56
- tree: TreeData | null;
57
- scrollContainer: HTMLElement;
58
- objectHandler: ObjectHandler;
59
- pickHandler: PickHandler;
60
- updateHandler: UpdateHandler;
61
- notificationHandler: NotificationHandler;
62
- colorGetter: ColorGetter;
63
- theme: string;
64
- linkIcons: boolean;
65
- debug: boolean;
66
- model: TreeModel | null;
67
- container: HTMLUListElement | null;
68
- lastLabel: HTMLElement | null;
69
- lastScrollTop: number;
70
-
71
- /**
72
- * Constructs a TreeView object.
73
- * @param tree - The tree structure data.
74
- * @param scrollContainer - The scrollable container element.
75
- * @param objectHandler - Callback for object visibility changes.
76
- * @param pickHandler - Callback for node selection/picking.
77
- * @param updateHandler - Callback for tree updates.
78
- * @param notificationHandler - Callback for notifications.
79
- * @param colorGetter - Function to get color for a path.
80
- * @param theme - The UI theme ('light' or 'dark').
81
- * @param linkIcons - Whether icon 0 and 1 are linked.
82
- * @param debug - Enable debug logging.
83
- */
84
- constructor(
85
- tree: TreeData,
86
- scrollContainer: HTMLElement,
87
- objectHandler: ObjectHandler,
88
- pickHandler: PickHandler,
89
- updateHandler: UpdateHandler,
90
- notificationHandler: NotificationHandler,
91
- colorGetter: ColorGetter,
92
- theme: string,
93
- linkIcons: boolean,
94
- debug: boolean = false,
95
- ) {
96
- this.tree = tree;
97
- this.scrollContainer = scrollContainer;
98
- this.objectHandler = objectHandler;
99
- this.pickHandler = pickHandler;
100
- this.updateHandler = updateHandler;
101
- this.notificationHandler = notificationHandler;
102
- this.colorGetter = colorGetter;
103
- this.theme = theme;
104
- this.linkIcons = linkIcons;
105
- this.debug = debug;
106
-
107
- this.model = null;
108
- this.container = null;
109
- this.lastLabel = null;
110
- this.lastScrollTop = 0;
111
- }
112
-
113
- /**
114
- * Initialize the tree view - creates the model and DOM container.
115
- * @returns The container element.
116
- */
117
- create(): HTMLUListElement {
118
- // Create the data model
119
- this.model = new TreeModel(this.tree!, {
120
- linkIcons: this.linkIcons,
121
- onStateChange: (node, iconNumber) => this._handleStateChange(node, iconNumber),
122
- });
123
-
124
- // Create DOM container
125
- this.container = document.createElement("ul");
126
- this.container.classList.add("tcv_toplevel");
127
-
128
- this.scrollContainer.addEventListener("scroll", this.handleScroll);
129
-
130
- return this.container;
131
- }
132
-
133
- /**
134
- * Handle state changes from the model.
135
- * @param node - The node whose state changed.
136
- * @param iconNumber - Which icon changed (0 or 1).
137
- */
138
- private _handleStateChange(node: TreeNode, iconNumber: IconIndex): void {
139
- const icons: IconIndex[] = iconNumber === 0 && this.linkIcons ? [0, 1] : [iconNumber];
140
- for (const i of icons) {
141
- this.objectHandler(this.getNodePath(node), node.state[i], i, true, false);
142
- }
143
- }
144
-
145
- // ============================================================================
146
- // Delegated Properties (for backward compatibility)
147
- // ============================================================================
148
-
149
- /** @returns The root node of the tree. */
150
- get root(): TreeNode | null {
151
- return this.model ? this.model.root : null;
152
- }
153
-
154
- /** @returns The maximum depth level of the tree. */
155
- get maxLevel(): number {
156
- return this.model ? this.model.maxLevel : 0;
157
- }
158
-
159
- // ============================================================================
160
- // Visible Elements Handling
161
- // ============================================================================
162
-
163
- /**
164
- * Retrieves the visible elements within the container.
165
- * @returns An array of visible elements.
166
- */
167
- getVisibleElements(): Element[] {
168
- if (!this.container) return [];
169
-
170
- const isElementVisible = (el: Element, containerRect: DOMRect): boolean => {
171
- const rect = el.getBoundingClientRect();
172
- return (
173
- rect.height > 0 &&
174
- rect.top >= -SCROLL_OFFSET &&
175
- rect.top <= containerRect.bottom + SCROLL_OFFSET
176
- );
177
- };
178
-
179
- const elements = this.container.querySelectorAll(".tv-tree-node");
180
- const containerRect = this.scrollContainer.getBoundingClientRect();
181
- const visibleElements = Array.from(elements).filter((el) =>
182
- isElementVisible(el, containerRect),
183
- );
184
-
185
- if (this.debug) {
186
- this._logVisibleElements(visibleElements);
187
- }
188
-
189
- return visibleElements;
190
- }
191
-
192
- /**
193
- * Debug logging for visible elements.
194
- * @param elements - The visible elements.
195
- */
196
- private _logVisibleElements(elements: Element[]): void {
197
- logger.debug(`\nVisible elements (${elements.length}):`);
198
- for (const el of elements) {
199
- if (!(el instanceof HTMLElement)) continue;
200
- const node = this.findNodeByPath(el.dataset.path || "");
201
- if (node) {
202
- logger.debug(
203
- node.path,
204
- node.state[0],
205
- node.state[1],
206
- " => ",
207
- el.dataset.state0,
208
- el.dataset.state1,
209
- );
210
- }
211
- }
212
- }
213
-
214
- /************************************************************************************
215
- * Handlers
216
- ************************************************************************************/
217
-
218
- /**
219
- * Handles the scroll event.
220
- */
221
- handleScroll = (): void => {
222
- if (!this.scrollContainer) {
223
- return;
224
- }
225
-
226
- this.lastScrollTop = this.scrollContainer.scrollTop;
227
- if (this.debug) {
228
- logger.debug("update => scroll");
229
- }
230
- this.update();
231
- };
232
-
233
- /**
234
- * Handles the click event on a navigation node.
235
- * @param node - The node associated with the navigation marker.
236
- * @returns The event handler function.
237
- */
238
- handleNavigationClick = (node: TreeNode): ((e: Event) => void) => {
239
- return (e: Event) => {
240
- e.stopPropagation();
241
- node.expanded = !node.expanded;
242
- this.showChildContainer(node);
243
- if (this.debug) {
244
- logger.debug("update => navClick");
245
- }
246
- this.update();
247
- };
248
- };
249
-
250
- /**
251
- * Handles the click event on an icon.
252
- * @param node - The node associated with the icon.
253
- * @param s - The icon index (0 for shapes, 1 for edges).
254
- * @returns The event handler function.
255
- */
256
- handleIconClick = (node: TreeNode, s: IconIndex): ((e: Event) => void) => {
257
- return (e: Event) => {
258
- e.stopPropagation();
259
- this.toggleIcon(node, s);
260
- };
261
- };
262
-
263
- /**
264
- * Handles the click event on a label.
265
- * @param node - The node that was clicked.
266
- * @param e - The event.
267
- */
268
- handleLabelClick(node: TreeNode, e: Event): void {
269
- const isMouse = e instanceof MouseEvent;
270
- this.pickHandler(
271
- this.getNodePath(this.getParent(node)),
272
- node.name,
273
- isMouse ? KeyMapper.get(e, "meta") : false,
274
- isMouse ? KeyMapper.get(e, "shift") : false,
275
- isMouse ? KeyMapper.get(e, "alt") : false,
276
- null,
277
- this.isLeaf(node) ? "leaf" : "node",
278
- true,
279
- );
280
-
281
- if (this.debug) {
282
- logger.debug(`Label clicked: ${this.getNodePath(node)}`);
283
- }
284
- }
285
-
286
- /**
287
- * Updates the tree view with the given prefix.
288
- * @param prefix - The prefix to filter the visible elements.
289
- */
290
- update = (prefix: string | null = null): void => {
291
- if (!this.container || !this.model) return;
292
-
293
- const visibleElements = this.getVisibleElements().filter((p): p is HTMLElement =>
294
- p instanceof HTMLElement && (prefix == null || (p.dataset.path || "").startsWith(prefix)),
295
- );
296
- for (const el of visibleElements) {
297
- const path = el.dataset.path || "";
298
-
299
- const node = this.findNodeByPath(path);
300
- if (node != null) {
301
- // render the actual node
302
- if (!node.rendered) {
303
- this.renderNode(node, el);
304
- node.rendered = true;
305
- }
306
- // render placeholders for all children
307
- if (node.expanded) {
308
- const childrenContainer = el.querySelector(".tv-children");
309
- if (
310
- childrenContainer instanceof HTMLElement &&
311
- childrenContainer.children.length === 0
312
- ) {
313
- for (const key in node.children) {
314
- const child = node.children[key];
315
- this.renderPlaceholder(child, childrenContainer);
316
- }
317
- this.showChildContainer(node);
318
- this.update(path);
319
- }
320
- }
321
-
322
- // and adapt the icons the the state
323
- for (const s of ICON_INDICES) {
324
- const domState = el.dataset[`state${s}`];
325
- const state = node.state[s];
326
- if (domState != String(state)) {
327
- this.updateIconInDOM(node, s);
328
- }
329
- }
330
- this.showChildContainer(node);
331
- } else {
332
- logger.error(`Node not found: ${path}`);
333
- }
334
- }
335
- };
336
-
337
- /************************************************************************************
338
- * Rendering routines
339
- ************************************************************************************/
340
-
341
- /**
342
- * Renders the tree view by clearing the container, rendering the placeholder,
343
- * and updating the tree.
344
- */
345
- render(): void {
346
- if (!this.container || !this.root) return;
347
-
348
- this.container.innerHTML = "";
349
- this.renderPlaceholder(this.root, this.container);
350
- if (this.debug) {
351
- logger.debug("update => render");
352
- }
353
- this.update();
354
- }
355
-
356
- /**
357
- * Renders a placeholder node in the tree view.
358
- * @param node - The node object to render.
359
- * @param container - The container element to append the rendered node to.
360
- * @param openPath - The path of the node to be opened, or null if no node should be opened.
361
- */
362
- renderPlaceholder(node: TreeNode, container: HTMLElement, openPath: string | null = null): void {
363
- if (this.debug) {
364
- logger.debug("renderPlaceholder", node.path, node.level);
365
- }
366
- const nodeElement = document.createElement("div");
367
- nodeElement.className = "tv-tree-node";
368
- nodeElement.dataset.path = this.getNodePath(node);
369
- nodeElement.dataset.openPath = openPath || "";
370
- nodeElement.dataset.state0 = String(node.state[0]);
371
- nodeElement.dataset.state1 = String(node.state[1]);
372
-
373
- const nodeContent = document.createElement("div");
374
- nodeContent.className = "tv-node-content";
375
- if (this.debug) {
376
- nodeContent.innerText = node.path;
377
- }
378
- nodeElement.appendChild(nodeContent);
379
- container.appendChild(nodeElement);
380
- }
381
-
382
- /**
383
- * Renders a node in the tree view to replace the placeholder.
384
- * @param node - The node object to render.
385
- * @param parentElement - The parent element to append the rendered node to.
386
- * @returns The container element for the node's children, if any.
387
- */
388
- renderNode(node: TreeNode, parentElement: HTMLElement): HTMLDivElement | null {
389
- if (this.debug) {
390
- logger.debug("renderNode", node.path, node.level);
391
- }
392
- const nodeContent = document.createElement("div");
393
- nodeContent.className = "tv-node-content";
394
- parentElement.removeChild(parentElement.firstChild!);
395
- parentElement.appendChild(nodeContent);
396
-
397
- const navMarker = document.createElement("span");
398
- navMarker.className = "tv-nav-marker";
399
- navMarker.innerHTML = node.children
400
- ? node.expanded
401
- ? NAV_ICONS.expanded
402
- : NAV_ICONS.collapsed
403
- : "";
404
-
405
- navMarker.onclick = this.handleNavigationClick(node);
406
-
407
- nodeContent.dataset.state0 = String(node.state[0]);
408
- nodeContent.dataset.state1 = String(node.state[1]);
409
-
410
- nodeContent.appendChild(navMarker);
411
-
412
- for (const s of ICON_INDICES) {
413
- const icon = document.createElement("span");
414
- const state = node.state[s];
415
- let className = `tv-icon tv-icon${s}`;
416
- if (state !== States.disabled) {
417
- className += " tv-pointer";
418
- }
419
- icon.className = className;
420
- icon.classList.add("tcv_tree_button");
421
- icon.classList.add(`tcv_button_${VIEW_ICONS[s][state]}`);
422
- if (state !== States.disabled) {
423
- icon.onmousedown = (e) => {
424
- e.preventDefault();
425
- };
426
- icon.onclick = this.handleIconClick(node, s);
427
- }
428
- nodeContent.appendChild(icon);
429
- }
430
-
431
- const label = document.createElement("span");
432
- label.className = "tv-node-label";
433
- label.innerHTML = node.name;
434
- const color = this.colorGetter(node.path);
435
- if (color != null) {
436
- label.innerHTML += `<span style="color:${color}"> ⚈</span>`;
437
- }
438
- label.onmousedown = (e) => {
439
- e.preventDefault();
440
- };
441
- label.oncontextmenu = (e) => {
442
- e.preventDefault();
443
- e.stopPropagation();
444
- this.handleLabelClick(node, e);
445
- };
446
- label.onclick = (e) => {
447
- e.stopPropagation();
448
- this.handleLabelClick(node, e);
449
- };
450
-
451
- nodeContent.appendChild(label);
452
-
453
- let childrenContainer: HTMLDivElement | null = null;
454
- if (node.children) {
455
- childrenContainer = document.createElement("div");
456
- childrenContainer.className = "tv-children";
457
- childrenContainer.style.display = "none";
458
- parentElement.appendChild(childrenContainer);
459
- }
460
- return childrenContainer;
461
- }
462
-
463
- /************************************************************************************
464
- * DOM functions
465
- ************************************************************************************/
466
-
467
- /**
468
- * Retrieves the DOM node with the specified path.
469
- * @param path - The path of the DOM node to retrieve.
470
- * @returns The DOM node with the specified path, or null if not found.
471
- */
472
- getDomNode = (path: string): HTMLElement | null => {
473
- if (!this.container) return null;
474
- const el = this.container.querySelector(`[data-path="${path}"]`);
475
- return el instanceof HTMLElement ? el : null;
476
- };
477
-
478
- /**
479
- * Shows or hides the child container of a node based on the expanded parameter.
480
- * @param node - The node object.
481
- */
482
- showChildContainer(node: TreeNode): void {
483
- if (node.expanded == null) return;
484
-
485
- const path = this.getNodePath(node);
486
- const nodeElement = this.getDomNode(path);
487
- if (nodeElement) {
488
- const childrenContainer = nodeElement.querySelector(".tv-children");
489
- if (childrenContainer instanceof HTMLElement) {
490
- const isExpanded = childrenContainer.style.display !== "none";
491
- if (isExpanded !== node.expanded) {
492
- childrenContainer.style.display = node.expanded ? "block" : "none";
493
- const navMarker = nodeElement.querySelector(".tv-nav-marker");
494
- if (navMarker) {
495
- navMarker.innerHTML = node.expanded
496
- ? NAV_ICONS.expanded
497
- : NAV_ICONS.collapsed;
498
- }
499
- if (!node.expanded) {
500
- if (this.debug) {
501
- logger.debug("update => showChildContainer");
502
- }
503
- this.update();
504
- }
505
- }
506
- }
507
- } else {
508
- logger.error(`Element not found: ${path}`);
509
- }
510
- }
511
-
512
- /**
513
- * Updates the icon in the DOM for a given node.
514
- * @param node - The node to update the icon for.
515
- * @param iconNumber - The icon number to update.
516
- */
517
- updateIconInDOM(node: TreeNode, iconNumber: IconIndex): void {
518
- if (!this.container) return;
519
-
520
- const nodePath = this.getNodePath(node);
521
- const nodeElement = this.container.querySelector(`[data-path="${nodePath}"]`);
522
- if (nodeElement instanceof HTMLElement) {
523
- const icon = nodeElement.querySelector(`.tv-icon${iconNumber}`);
524
- if (icon) {
525
- for (const b of VIEW_ICONS[iconNumber]) {
526
- icon.classList.remove(`tcv_button_${b}`);
527
- }
528
- icon.classList.add(`tcv_button_${VIEW_ICONS[iconNumber][node.state[iconNumber]]}`);
529
- }
530
- nodeElement.dataset[`state${iconNumber}`] = String(node.state[iconNumber]);
531
- }
532
- }
533
-
534
- /**
535
- * Toggles the color of the label for a given node.
536
- * @param node - The node for which to toggle the label color.
537
- * @param path - If node is null, the path for which to toggle the label color.
538
- */
539
- toggleLabelColor(node: TreeNode | null, path: string | null = null): void {
540
- if (!this.container) return;
541
-
542
- const nodePath = path == null && node ? this.getNodePath(node) : path;
543
- if (!nodePath) return;
544
-
545
- const nodeElement = this.container.querySelector(`[data-path="${nodePath}"]`);
546
- if (this.lastLabel) {
547
- this.lastLabel.classList.remove("tv-node-label-highlight");
548
- }
549
- if (nodeElement) {
550
- const label = nodeElement.querySelector(".tv-node-label");
551
- if (label instanceof HTMLElement) {
552
- if (this.lastLabel === label) {
553
- this.lastLabel = null;
554
- } else {
555
- label.classList.toggle("tv-node-label-highlight");
556
- this.lastLabel = label;
557
- }
558
- }
559
- }
560
- }
561
-
562
- // ============================================================================
563
- // Tree Handling - Delegated to TreeModel
564
- // ============================================================================
565
-
566
- /**
567
- * Traverse the tree and call a callback for each node.
568
- * @param node - Starting node.
569
- * @param callback - Function to call for each node.
570
- */
571
- traverse(node: TreeNode, callback: (node: TreeNode) => void): void {
572
- if (this.model) {
573
- this.model.traverse(node, callback);
574
- }
575
- }
576
-
577
- /**
578
- * Check if a node is a leaf (has no children).
579
- * @param node - The node to check.
580
- * @returns True if the node is a leaf.
581
- */
582
- isLeaf(node: TreeNode): boolean {
583
- return this.model ? this.model.isLeaf(node) : true;
584
- }
585
-
586
- /**
587
- * Get the full path of a node.
588
- * @param node - The node.
589
- * @returns The full path.
590
- */
591
- getNodePath(node: TreeNode | null): string {
592
- return this.model ? this.model.getNodePath(node) : "";
593
- }
594
-
595
- /**
596
- * Find a node by its path.
597
- * @param path - The path to find.
598
- * @returns The found node or null.
599
- */
600
- findNodeByPath(path: string): TreeNode | null {
601
- return this.model ? this.model.findNodeByPath(path) : null;
602
- }
603
-
604
- /**
605
- * Get the parent node of a given node.
606
- * @param node - The child node.
607
- * @returns The parent node or null.
608
- */
609
- getParent(node: TreeNode): TreeNode | null {
610
- return this.model ? this.model.getParent(node) : null;
611
- }
612
-
613
- // ============================================================================
614
- // State Management - Uses TreeModel, handles UI updates
615
- // ============================================================================
616
-
617
- /**
618
- * Toggle the state of a node's icon.
619
- * @param node - The node to toggle.
620
- * @param iconNumber - Which icon (0 or 1).
621
- * @param force - Force state (true=selected, false=unselected, null=toggle).
622
- */
623
- toggleIcon(node: TreeNode, iconNumber: IconIndex, force: boolean | null = null): void {
624
- if (!this.model) return;
625
-
626
- const changed = this.model.toggleNodeState(node, iconNumber, force);
627
- if (changed) {
628
- this.update();
629
- this.updateHandler(true);
630
- this.notificationHandler();
631
- }
632
- }
633
-
634
- /**
635
- * Hide all nodes in the tree.
636
- */
637
- hideAll(): void {
638
- if (!this.model) return;
639
-
640
- this.model.hideAll();
641
- this.update();
642
- this.updateHandler(true);
643
- this.notificationHandler();
644
- }
645
-
646
- /**
647
- * Show all nodes in the tree.
648
- */
649
- showAll(): void {
650
- if (!this.model) return;
651
-
652
- this.model.showAll();
653
- this.update();
654
- this.updateHandler(true);
655
- this.notificationHandler();
656
- }
657
-
658
- /**
659
- * Show a specific node by path.
660
- * @param path - The node path.
661
- */
662
- show(path: string): void {
663
- if (!this.model) return;
664
-
665
- this.model.show(path);
666
- this.update();
667
- this.updateHandler(true);
668
- this.notificationHandler();
669
- }
670
-
671
- /**
672
- * Hide a specific node by path.
673
- * @param path - The node path.
674
- */
675
- hide(path: string): void {
676
- if (!this.model) return;
677
-
678
- this.model.hide(path);
679
- this.update();
680
- this.updateHandler(true);
681
- this.notificationHandler();
682
- }
683
-
684
- /**
685
- * Get the state of a node by path.
686
- * @param path - The node path.
687
- * @returns The state array or null.
688
- */
689
- getState(path: string): [StateValue, StateValue] | null {
690
- return this.model ? this.model.getState(path) : null;
691
- }
692
-
693
- /**
694
- * Get all leaf node states.
695
- * @returns Map of path -> state array.
696
- */
697
- getStates(): Record<string, [StateValue, StateValue]> {
698
- return this.model ? this.model.getStates() : {};
699
- }
700
-
701
- /**
702
- * Set the state of a node by path.
703
- * @param path - The node path.
704
- * @param state - The new state.
705
- */
706
- setState(path: string, state: [StateValue, StateValue]): void {
707
- if (!this.model) return;
708
-
709
- this.model.setState(path, state);
710
- this.update();
711
- this.updateHandler(true);
712
- this.notificationHandler();
713
- }
714
-
715
- /**
716
- * Set multiple node states at once.
717
- * @param states - Map of path -> state array.
718
- */
719
- setStates(states: Record<string, [StateValue, StateValue]>): void {
720
- if (!this.model) return;
721
-
722
- this.model.setStates(states);
723
- this.update();
724
- this.updateHandler(true);
725
- this.notificationHandler();
726
- }
727
-
728
- /************************************************************************************
729
- * Tree handling high level API
730
- ************************************************************************************/
731
-
732
- /**
733
- * Scrolls the parent container to center the specified element within the visible area.
734
- * @param element - The DOM element to center within the scroll container.
735
- */
736
- scrollCentered(element: HTMLElement | null): void {
737
- if (element != null) {
738
- const parent = this.scrollContainer;
739
-
740
- // Calculate the center position of the element relative to the parent
741
- const elementHeight = element.offsetHeight;
742
- const parentHeight = parent.clientHeight;
743
-
744
- // Calculate scroll position that would center the element
745
- const elementOffset = element.offsetTop - parent.offsetTop;
746
- const scrollTop = elementOffset - parentHeight / 2 + elementHeight / 2;
747
-
748
- // Ensure we don't scroll beyond the parent's scrollable area
749
- const maxScroll = parent.scrollHeight - parentHeight;
750
- const clampedScrollTop = Math.max(0, Math.min(scrollTop, maxScroll));
751
-
752
- // Perform the scroll
753
- parent.scrollTo({ top: clampedScrollTop, behavior: "smooth" });
754
- }
755
- }
756
-
757
- /**
758
- * Ensures a node and its children are rendered in the DOM.
759
- * @param node - The node to render.
760
- */
761
- private _ensureNodeRendered(node: TreeNode): HTMLElement | null {
762
- const path = this.getNodePath(node);
763
- let el = this.getDomNode(path);
764
-
765
- // If element doesn't exist, we need to ensure parent renders it
766
- if (!el) {
767
- const parent = this.getParent(node);
768
- if (parent) {
769
- this._ensureNodeRendered(parent);
770
- // After parent is rendered, check if our element exists now
771
- el = this.getDomNode(path);
772
- }
773
- }
774
-
775
- if (!el) {
776
- // Element still doesn't exist - create placeholder in parent's children container
777
- const parent = this.getParent(node);
778
- if (parent) {
779
- const parentEl = this.getDomNode(this.getNodePath(parent));
780
- if (parentEl) {
781
- let childrenContainer = parentEl.querySelector(".tv-children");
782
- if (!childrenContainer && parent.children) {
783
- // Need to render the parent node first to create children container
784
- if (!parent.rendered) {
785
- this.renderNode(parent, parentEl);
786
- parent.rendered = true;
787
- }
788
- childrenContainer = parentEl.querySelector(".tv-children");
789
- }
790
- if (childrenContainer instanceof HTMLElement) {
791
- this.renderPlaceholder(node, childrenContainer);
792
- el = this.getDomNode(path);
793
- }
794
- }
795
- }
796
- }
797
-
798
- // Now render the actual node content if it's just a placeholder
799
- if (el && !node.rendered) {
800
- this.renderNode(node, el);
801
- node.rendered = true;
802
- }
803
-
804
- // If node is expanded, ensure children containers are created
805
- if (el && node.expanded && node.children) {
806
- const childrenContainer = el.querySelector(".tv-children");
807
- if (childrenContainer instanceof HTMLElement) {
808
- if (childrenContainer.children.length === 0) {
809
- for (const key in node.children) {
810
- this.renderPlaceholder(node.children[key], childrenContainer);
811
- }
812
- }
813
- childrenContainer.style.display = "block";
814
- }
815
- }
816
-
817
- return el;
818
- }
819
-
820
- /**
821
- * Opens the specified path in the tree view.
822
- * @param path - The path to open in the tree view.
823
- */
824
- openPath(path: string): void {
825
- const parts = path.split("/").filter(Boolean);
826
- let current = "";
827
- let node: TreeNode | null = null;
828
- let el: HTMLElement | null = null;
829
-
830
- for (const part of parts) {
831
- current += "/" + part;
832
- node = this.findNodeByPath(current);
833
-
834
- if (node) {
835
- // Mark as expanded
836
- if (node.children) {
837
- node.expanded = true;
838
- }
839
-
840
- // Ensure this node is rendered (bypassing lazy loading)
841
- el = this._ensureNodeRendered(node);
842
-
843
- // Update nav marker if element exists
844
- if (el) {
845
- const navMarker = el.querySelector(".tv-nav-marker");
846
- if (navMarker && node.children) {
847
- navMarker.innerHTML = NAV_ICONS.expanded;
848
- }
849
- }
850
-
851
- if (this.debug) {
852
- logger.debug("update => openPath", current);
853
- }
854
- } else {
855
- logger.error(`Path not found: ${current}`);
856
- break;
857
- }
858
- }
859
-
860
- // Final update to sync any remaining state
861
- this.update();
862
-
863
- // Scroll to and highlight the target
864
- this.scrollCentered(el);
865
- if (node) {
866
- this.toggleLabelColor(node);
867
- }
868
- }
869
-
870
- /**
871
- * Closes the specified path in the tree view.
872
- * @param path - The path to be closed.
873
- */
874
- closePath(path: string): void {
875
- const node = this.findNodeByPath(path);
876
- if (node) {
877
- node.expanded = false;
878
- this.showChildContainer(node);
879
- const el = this.getDomNode(path);
880
- if (el != null) {
881
- this.scrollContainer.scrollTop = el.offsetTop - this.scrollContainer.offsetTop;
882
- }
883
- if (this.debug) {
884
- logger.debug("update => collapsePath");
885
- }
886
- this.update();
887
- } else {
888
- logger.error(`Path not found: ${path}`);
889
- }
890
- }
891
-
892
- /**
893
- * Open all nodes to a specified level.
894
- * @param level - The level to open (-1 for smart expand).
895
- */
896
- openLevel(level: number): void {
897
- if (!this.model || !this.root) return;
898
-
899
- this.model.setExpandedLevel(level);
900
-
901
- const el = this.getDomNode(this.getNodePath(this.root));
902
- if (el != null) {
903
- this.scrollContainer.scrollTop = el.offsetTop - this.scrollContainer.offsetTop;
904
- }
905
-
906
- // Multiple updates to ensure all levels are rendered
907
- const maxIterations = level === -1 ? this.maxLevel : level;
908
- for (let i = 0; i <= maxIterations; i++) {
909
- if (this.debug) {
910
- logger.debug("update => openLevel", i);
911
- }
912
- this.update();
913
- }
914
- }
915
-
916
- /**
917
- * Collapse all nodes in the tree view.
918
- */
919
- collapseAll(): void {
920
- this.openLevel(0);
921
- }
922
-
923
- /**
924
- * Expand all nodes in the tree view.
925
- */
926
- expandAll(): void {
927
- this.openLevel(this.maxLevel);
928
- }
929
-
930
- /**
931
- * Dispose of resources and clean up.
932
- */
933
- dispose(): void {
934
- if (this.model) {
935
- this.model.dispose();
936
- this.model = null;
937
- }
938
- this.tree = null;
939
- this.container = null;
940
- this.lastLabel = null;
941
- this.scrollContainer.removeEventListener("scroll", this.handleScroll);
942
- }
943
- }
944
-
945
- export { TreeView, States };