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,4821 +0,0 @@
1
- // =============================================================================
2
- // IMPORTS
3
- // =============================================================================
4
-
5
- import * as THREE from "three";
6
-
7
- // Extend window to include THREE for debugging/external access
8
- declare global {
9
- interface Window {
10
- THREE?: typeof THREE;
11
- }
12
- }
13
-
14
- import { NestedGroup, ObjectGroup, isObjectGroup, isCompoundGroup } from "../scene/nestedgroup.js";
15
- import { Grid } from "../scene/grid.js";
16
- import { AxesHelper } from "../scene/axes.js";
17
- import { OrientationMarker } from "../scene/orientation.js";
18
- import { TreeView } from "../ui/treeview.js";
19
- // TreeData and StateValue available if needed for tree manipulation
20
- import { Timer } from "../utils/timer.js";
21
- import { Clipping } from "../scene/clipping.js";
22
- import { Animation } from "../scene/animation.js";
23
- import {
24
- isEqual,
25
- KeyMapper,
26
- scaleLight,
27
- deepDispose,
28
- isOrthographicCamera,
29
- isLineSegments2,
30
- toVector3Tuple,
31
- toQuaternionTuple,
32
- } from "../utils/utils.js";
33
- import type { DisposableTree } from "../utils/utils.js";
34
- import { ShapeRenderer } from "../scene/render-shape.js";
35
- import type { ShapeTreeData, RenderResult } from "../scene/render-shape.js";
36
- import type { KeyMappingConfig } from "../utils/utils.js";
37
- import { Controls } from "../camera/controls.js";
38
- import { Camera, type CameraDirection } from "../camera/camera.js";
39
- import { BoundingBox, BoxHelper } from "../scene/bbox.js";
40
- import { Tools, type ToolResponse } from "../tools/cad_tools/tools.js";
41
- import { version } from "../_version.js";
42
- import { PickedObject, Raycaster, TopoFilter } from "../rendering/raycast.js";
43
- import { StudioManager } from "./studio-manager.js";
44
- import { ViewerState } from "./viewer-state.js";
45
- import { logger } from "../utils/logger.js";
46
- import { isInstancedFormat, decodeInstancedFormat, decodeInlineBuffers } from "../utils/decode-instances.js";
47
- import type { Display } from "../ui/display.js";
48
- import type { Vector3Tuple, QuaternionTuple } from "three";
49
- import {
50
- CollapseState,
51
- type ZebraColorScheme,
52
- type ZebraMappingMode,
53
- type StudioToneMapping,
54
- type StudioTextureMapping,
55
- type StudioBackground,
56
- type NotificationCallback,
57
- type RenderOptions,
58
- type ViewerOptions,
59
- type Shapes,
60
- type VisibilityState,
61
- type StateChange,
62
- type ActiveTab,
63
- type Axis,
64
- type ClipIndex,
65
- type ThemeInput,
66
- type BoundingBoxFlat,
67
- type Keymap,
68
- } from "./types.js";
69
-
70
- // =============================================================================
71
- // TYPE DEFINITIONS
72
- // =============================================================================
73
-
74
- /**
75
- * Material settings for the viewer.
76
- */
77
- interface MaterialSettings {
78
- ambientIntensity: number;
79
- directIntensity: number;
80
- metalness: number;
81
- roughness: number;
82
- }
83
-
84
- /**
85
- * Bounding box tracking for the last selected object.
86
- */
87
- interface LastBboxInfo {
88
- id: string;
89
- bbox: BoxHelper;
90
- needsUpdate: boolean;
91
- }
92
-
93
- /**
94
- * Camera location settings.
95
- */
96
- interface CameraLocationSettings {
97
- position: number[];
98
- quaternion: number[];
99
- target: number[];
100
- zoom: number;
101
- }
102
-
103
- /**
104
- * Reset location settings from controls.
105
- */
106
- interface ResetLocation {
107
- target0: THREE.Vector3;
108
- position0: THREE.Vector3;
109
- quaternion0: THREE.Quaternion;
110
- zoom0: number;
111
- }
112
-
113
- /**
114
- * Type guard to check if a tree node is a leaf (VisibilityState)
115
- */
116
- function isVisibilityState(
117
- node: ShapeTreeData | VisibilityState,
118
- ): node is VisibilityState {
119
- return Array.isArray(node);
120
- }
121
-
122
- /**
123
- * Type guard to check if a tree node is a branch (ShapeTreeData)
124
- */
125
- function isShapeTreeData(node: ShapeTreeData | VisibilityState): node is ShapeTreeData {
126
- return !Array.isArray(node);
127
- }
128
-
129
- /**
130
- * Keymap configuration - re-export from utils for API compatibility.
131
- */
132
- type KeymapConfig = Partial<KeyMappingConfig>;
133
-
134
- /**
135
- * Mesh with an index property (used for clipping plane meshes).
136
- */
137
- interface IndexedMesh extends THREE.Mesh {
138
- index: ClipIndex;
139
- }
140
-
141
- /**
142
- * Type guard to check if an Object3D is an IndexedMesh.
143
- */
144
- function isIndexedMesh(obj: THREE.Object3D): obj is IndexedMesh {
145
- return (
146
- "isMesh" in obj &&
147
- obj.isMesh === true &&
148
- "index" in obj &&
149
- typeof (obj as IndexedMesh).index === "number"
150
- );
151
- }
152
-
153
- /**
154
- * Material with clippingPlanes property.
155
- */
156
- interface ClippableMaterial extends THREE.Material {
157
- clippingPlanes: THREE.Plane[];
158
- }
159
-
160
- /**
161
- * Type guard to check if a material has clippingPlanes.
162
- */
163
- function isClippableMaterial(mat: THREE.Material | THREE.Material[]): mat is ClippableMaterial {
164
- return !Array.isArray(mat) && "clippingPlanes" in mat;
165
- }
166
-
167
- /**
168
- * Image capture result.
169
- */
170
- interface ImageResult {
171
- task: string;
172
- dataUrl: string | ArrayBuffer | null;
173
- }
174
-
175
- /**
176
- * Raycast event from keyboard or mouse.
177
- */
178
- interface RaycastEvent {
179
- key?: string;
180
- mouse?: "left" | "right";
181
- shift?: boolean;
182
- }
183
-
184
- /**
185
- * Backend response structure.
186
- */
187
- interface BackendResponse {
188
- subtype: string;
189
- [key: string]: unknown;
190
- }
191
-
192
- /**
193
- * Type guard to check if a BackendResponse is a ToolResponse.
194
- */
195
- function isToolResponse(
196
- response: BackendResponse,
197
- ): response is BackendResponse & ToolResponse {
198
- return response.subtype === "tool_response" && "tool_type" in response;
199
- }
200
-
201
- /**
202
- * Display options for viewer construction.
203
- */
204
- interface DisplayOptionsInternal {
205
- measureTools?: boolean;
206
- measurementDebug?: boolean;
207
- selectTool?: boolean;
208
- explodeTool?: boolean;
209
- zscaleTool?: boolean;
210
- zebraTool?: boolean;
211
- glass?: boolean;
212
- tools?: boolean;
213
- canvas?: HTMLCanvasElement;
214
- gl?: WebGLRenderingContext | WebGL2RenderingContext;
215
- keymap?: KeymapConfig;
216
- [key: string]: unknown;
217
- }
218
-
219
- /**
220
- * State that exists only after render() and before clear().
221
- * Groups all resources that are created together during rendering.
222
- */
223
- interface RenderedState {
224
- // Core THREE.js objects
225
- scene: THREE.Scene;
226
- ambientLight: THREE.AmbientLight;
227
- directLight: THREE.DirectionalLight;
228
-
229
- // Camera and controls
230
- camera: Camera;
231
- controls: Controls;
232
-
233
- // Helpers
234
- gridHelper: Grid;
235
- axesHelper: AxesHelper;
236
- clipping: Clipping;
237
- orientationMarker: OrientationMarker;
238
-
239
- // These can change during lifetime (via toggleGroup)
240
- nestedGroup: NestedGroup;
241
- treeview: TreeView;
242
- }
243
-
244
- // =============================================================================
245
- // VIEWER CLASS
246
- // =============================================================================
247
-
248
- /**
249
- * Main CAD viewer class that manages the 3D scene, rendering, and user interaction.
250
- *
251
- * The Viewer is created by Display and handles:
252
- * - WebGL rendering with Three.js
253
- * - Camera management (orthographic/perspective)
254
- * - Scene graph with CAD objects (NestedGroup/ObjectGroup)
255
- * - Clipping planes
256
- * - Material settings
257
- * - Animation playback
258
- * - Object picking and selection
259
- *
260
- * ## Lifecycle
261
- * 1. Created by Display constructor
262
- * 2. `render()` called to display CAD shapes
263
- * 3. User interacts via UI (calls setter methods)
264
- * 4. `clear()` to remove shapes (optional)
265
- * 5. `dispose()` for cleanup
266
- *
267
- * ## State Management
268
- * All state is centralized in `ViewerState`. Use getter/setter methods
269
- * rather than accessing state directly.
270
- *
271
- * @example
272
- * ```typescript
273
- * // Access via Display
274
- * const display = new Display(container, options);
275
- * display.render(shapes, states, options);
276
- *
277
- * // Access viewer methods
278
- * display.viewer.setAxes(true);
279
- * display.viewer.switchCamera(false); // perspective
280
- * ```
281
- *
282
- * @public
283
- */
284
- class Viewer {
285
- // ---------------------------------------------------------------------------
286
- // Properties
287
- // ---------------------------------------------------------------------------
288
-
289
- // State management
290
- state: ViewerState;
291
- notifyCallback: NotificationCallback | null;
292
- pinAsPngCallback: ((data: ImageResult) => void) | null;
293
- updateMarker: boolean;
294
- ready: boolean;
295
-
296
- // Always available (set in constructor)
297
- display!: Display;
298
- renderer!: THREE.WebGLRenderer;
299
- private _externalGl: boolean;
300
- onAfterRender: (() => void) | null;
301
- mouse!: THREE.Vector2;
302
- cadTools!: Tools;
303
- animation!: Animation;
304
- clipNormals!: [THREE.Vector3, THREE.Vector3, THREE.Vector3];
305
-
306
- // Render-time state: created in render(), cleared in clear()
307
- private _rendered: RenderedState | null;
308
-
309
- /**
310
- * Get rendered state, throwing if not yet rendered.
311
- */
312
- get rendered(): RenderedState {
313
- if (!this._rendered) {
314
- throw new Error("Viewer.render() must be called before this operation");
315
- }
316
- return this._rendered;
317
- }
318
-
319
- // Data objects (set in render, cleared in clear)
320
- tree: ShapeTreeData | null;
321
- bbox: BoundingBox | null;
322
- bb_max: number;
323
- bb_radius!: number;
324
- private _stencilCSize: number;
325
- private _treeNeedsRebuild: boolean;
326
- private _pendingDisposal: THREE.Object3D[];
327
- shapes: Shapes | null;
328
- gridSize!: number;
329
-
330
- // Animation
331
- hasAnimationLoop: boolean;
332
- mixer: THREE.AnimationMixer | null;
333
- continueAnimation: boolean;
334
- clipAction: THREE.AnimationAction | null;
335
-
336
- // Shape rendering
337
- shapeRenderer: ShapeRenderer | null;
338
-
339
- // Camera
340
- camera_distance: number;
341
-
342
- // Material settings
343
- materialSettings: MaterialSettings | null;
344
- renderOptions: RenderOptions | null;
345
-
346
- // Selection tracking
347
- lastNotification: Record<string, unknown>;
348
- lastBbox: LastBboxInfo | null;
349
- lastObject: PickedObject | null;
350
- lastSelection: PickedObject | null;
351
- lastPosition: THREE.Vector3 | null;
352
- bboxNeedsUpdate: boolean;
353
- keepHighlight: boolean;
354
-
355
- // Tree structures for expanded/compact views
356
- expandedTree: ShapeTreeData | null;
357
- compactTree: ShapeTreeData | null;
358
- expandedNestedGroup: NestedGroup | null;
359
- compactNestedGroup: NestedGroup | null;
360
-
361
- // Raycaster
362
- raycaster: Raycaster | null;
363
-
364
- // Studio mode orchestration (owns composer, floor, shadow lights, env manager)
365
- private _studioManager!: StudioManager;
366
-
367
- /** Environment manager — proxied from StudioManager for display.ts access. */
368
- get envManager() { return this._studioManager.envManager; }
369
- // Z-scale
370
- zScale!: number;
371
-
372
- // Deprecated properties (kept for compatibility)
373
- clipNormal0: Vector3Tuple | null;
374
- clipNormal1: Vector3Tuple | null;
375
- clipNormal2: Vector3Tuple | null;
376
- keymap: KeymapConfig | null;
377
- info: DisposableTree | null;
378
-
379
- // ---------------------------------------------------------------------------
380
- // Constructor & Initialization
381
- // ---------------------------------------------------------------------------
382
-
383
- /**
384
- * Create Viewer.
385
- * @param display - The Display object.
386
- * @param options - configuration parameters.
387
- * @param notifyCallback - The callback to receive changes of viewer parameters.
388
- * @param pinAsPngCallback - Optional callback for PNG pinning.
389
- * @param updateMarker - enforce to redraw orientation marker after every ui activity
390
- */
391
- constructor(
392
- display: Display,
393
- options: DisplayOptionsInternal,
394
- notifyCallback: NotificationCallback | null,
395
- pinAsPngCallback: ((data: ImageResult) => void) | null = null,
396
- updateMarker: boolean = true,
397
- ) {
398
- // Create centralized state from options (single source of truth)
399
- this.state = new ViewerState(options);
400
-
401
- // Register callback for external notifications from state changes during runtime
402
- // Initial config sync is handled explicitly in render() via notifyCallback
403
- this.state.setExternalNotifyCallback((input) => {
404
- const notifications = Array.isArray(input) ? input : [input];
405
- const changes: Record<string, unknown> = {};
406
- for (const { key, change } of notifications) {
407
- // Convert THREE.Vector3 to array for external notification
408
- const value = change.new instanceof THREE.Vector3
409
- ? change.new.toArray()
410
- : change.new;
411
- changes[key] = value;
412
- }
413
- this.checkChanges(changes, true);
414
- });
415
-
416
- this.notifyCallback = notifyCallback;
417
- this.pinAsPngCallback = pinAsPngCallback;
418
- this.updateMarker = updateMarker;
419
- this.onAfterRender = null;
420
-
421
- this.hasAnimationLoop = false;
422
-
423
- this.display = display;
424
-
425
- if (options.keymap) {
426
- this.setKeyMap({ ...ViewerState.DISPLAY_DEFAULTS.keymap, ...options.keymap });
427
- } else {
428
- this.setKeyMap(ViewerState.DISPLAY_DEFAULTS.keymap);
429
- }
430
-
431
- window.THREE = THREE;
432
-
433
- // Render-time state starts as null
434
- this._rendered = null;
435
-
436
- this.tree = null;
437
- this.bbox = null;
438
- this.bb_max = 0;
439
- this._stencilCSize = 0;
440
- this._treeNeedsRebuild = false;
441
- this._pendingDisposal = [];
442
- this.cadTools = new Tools(this, options.measurementDebug ?? false);
443
-
444
- this.ready = false;
445
- this.mixer = null;
446
- this.clipAction = null;
447
- this.animation = new Animation("|");
448
- this.continueAnimation = true;
449
- this.shapeRenderer = null;
450
- this.materialSettings = null;
451
- this.renderOptions = null;
452
-
453
- this.clipNormals = [
454
- new THREE.Vector3(-1, 0, 0),
455
- new THREE.Vector3(0, -1, 0),
456
- new THREE.Vector3(0, 0, -1),
457
- ];
458
-
459
- this.camera_distance = 0;
460
-
461
- this.mouse = new THREE.Vector2();
462
-
463
- // setup renderer — support externally provided canvas and/or WebGL context
464
- const rendererParams: THREE.WebGLRendererParameters = {
465
- alpha: true,
466
- antialias: true,
467
- stencil: true,
468
- };
469
- if (options.canvas) {
470
- rendererParams.canvas = options.canvas;
471
- }
472
- if (options.gl) {
473
- rendererParams.context = options.gl;
474
- }
475
- this._externalGl = !!(options.canvas || options.gl);
476
- this.renderer = new THREE.WebGLRenderer(rendererParams);
477
- this.renderer.outputColorSpace = THREE.SRGBColorSpace;
478
- this.renderer.setPixelRatio(window.devicePixelRatio);
479
- this.renderer.setSize(this.state.get("cadWidth"), this.state.get("height"));
480
- this.renderer.setClearColor(0xffffff, 0);
481
- this.renderer.autoClear = false;
482
-
483
- // Create studio manager (env, floor, composer created lazily inside)
484
-
485
- this.lastNotification = {};
486
- this.lastBbox = null;
487
-
488
- // measure supporting exploded shapes and compact shapes
489
- this.expandedTree = null;
490
- this.compactTree = null;
491
- this.expandedNestedGroup = null;
492
- this.compactNestedGroup = null;
493
-
494
- // If fromSolid is true, this means the selected object is from the solid
495
- // This is the obj that has been picked but the actual selected obj is the solid
496
- // Since we cannot directly pick a solid this is the solution
497
- this.lastObject = null;
498
- this.lastSelection = null;
499
- this.lastPosition = null;
500
- this.bboxNeedsUpdate = false;
501
-
502
- this.keepHighlight = false;
503
-
504
- this.shapes = null;
505
- this.raycaster = null;
506
-
507
- // Deprecated properties
508
- this.clipNormal0 = null;
509
- this.clipNormal1 = null;
510
- this.clipNormal2 = null;
511
- this.keymap = null;
512
- this.info = null;
513
-
514
- this.setPickHandler(true);
515
-
516
- this.renderer.domElement.addEventListener("contextmenu", (e: Event) =>
517
- e.stopPropagation(),
518
- );
519
-
520
- this.display.setupUI(this, this.renderer.domElement);
521
-
522
- // Create studio manager (owns env, floor, composer, shadows, subscriptions)
523
- this._studioManager = new StudioManager({
524
- renderer: this.renderer,
525
- state: this.state,
526
- isRendered: () => this._rendered !== null,
527
- getScene: () => this.rendered.scene,
528
- getCamera: () => this.rendered.camera,
529
- getAmbientLight: () => this.rendered.ambientLight,
530
- getDirectLight: () => this.rendered.directLight,
531
- getNestedGroup: () => this.rendered.nestedGroup,
532
- getClipping: () => this.rendered.clipping,
533
- getBbox: () => this.bbox,
534
- getLastBboxId: () => this.lastBbox?.id ?? null,
535
- setAxes: (flag, notify) => this.setAxes(flag, notify),
536
- setGrids: (grids, notify) => this.setGrids(grids, notify),
537
- setOrtho: (flag, notify) => this.setOrtho(flag, notify),
538
- update: (updateMarker, notify) => this.update(updateMarker, notify),
539
- dispatchEvent: (event) => this.display.container.dispatchEvent(event),
540
- onSelectionChanged: (id) => this.display.onSelectionChanged(id),
541
- });
542
-
543
- console.debug("three-cad-viewer: WebGL Renderer created");
544
- }
545
-
546
-
547
- /**
548
- * Return three-cad-viewer version as semver string.
549
- * @returns semver version
550
- * @public
551
- */
552
- version(): string {
553
- return version;
554
- }
555
-
556
- /**
557
- * Apply render options and build materialSettings object.
558
- * Called by render() after state is populated with render options.
559
- * @param options - The provided options object for rendering.
560
- */
561
- setRenderDefaults(options: RenderOptions): void {
562
- // Update state with any render-specific options
563
- this.state.updateRenderState(options, true);
564
-
565
- // Build materialSettings from current state
566
- this.materialSettings = {
567
- ambientIntensity: this.state.get("ambientIntensity"),
568
- directIntensity: this.state.get("directIntensity"),
569
- metalness: this.state.get("metalness"),
570
- roughness: this.state.get("roughness"),
571
- };
572
- }
573
-
574
- /**
575
- * Apply view options to state.
576
- * Called by render() after state is populated.
577
- * @param options - The provided options object for the view.
578
- */
579
- setViewerDefaults(options: ViewerOptions): void {
580
- // Update state with view-specific options
581
- // updateViewerState handles conversion from Vector3Tuple to THREE.Vector3
582
- this.state.updateViewerState(options);
583
- }
584
-
585
- /**
586
- * @deprecated Use state properties directly. Kept for backwards compatibility.
587
- */
588
- setDisplayDefaults(): void {
589
- // No-op: ViewerState now handles all defaults in its constructor
590
- // This method is kept only for API compatibility
591
- }
592
-
593
- dumpOptions(): void {
594
- this.state.dump();
595
- }
596
-
597
- // ---------------------------------------------------------------------------
598
- // Shape Tessellation & Decomposition
599
- // ---------------------------------------------------------------------------
600
-
601
- /**
602
- * Get or create the ShapeRenderer instance with current configuration.
603
- */
604
- private getShapeRenderer(): ShapeRenderer {
605
- const config = {
606
- cadWidth: this.state.get("cadWidth"),
607
- height: this.state.get("height"),
608
- edgeColor: this.state.get("edgeColor"),
609
- transparent: this.state.get("transparent"),
610
- defaultOpacity: this.state.get("defaultOpacity"),
611
- metalness: this.state.get("metalness"),
612
- roughness: this.state.get("roughness"),
613
- normalLen: this.state.get("normalLen"),
614
- };
615
-
616
- if (!this.shapeRenderer) {
617
- this.shapeRenderer = new ShapeRenderer(config);
618
- } else {
619
- this.shapeRenderer.updateConfig(config);
620
- }
621
-
622
- return this.shapeRenderer;
623
- }
624
-
625
- /**
626
- * Render the shapes of the CAD object.
627
- * @param exploded - Whether to render the compact or exploded version
628
- * @param shapes - The Shapes object.
629
- * @returns A nested THREE.Group object and navigation tree.
630
- */
631
- renderTessellatedShapes(exploded: boolean, shapes: Shapes): RenderResult {
632
- const renderer = this.getShapeRenderer();
633
- const result = renderer.render(exploded, shapes);
634
-
635
- // Update bbox if the renderer computed one
636
- if (renderer.bbox) {
637
- this.bbox = renderer.bbox;
638
- }
639
-
640
- return result;
641
- }
642
-
643
- // ---------------------------------------------------------------------------
644
- // Animation Control
645
- // ---------------------------------------------------------------------------
646
-
647
- /**
648
- * Add a position animation track (full 3D translation).
649
- * @param selector - path/id of group to be animated.
650
- * @param times - array of keyframe times.
651
- * @param positions - array of [x, y, z] position offsets.
652
- */
653
- addPositionTrack(
654
- selector: string,
655
- times: number[],
656
- positions: number[][],
657
- ): void {
658
- this.animation.addPositionTrack(
659
- selector,
660
- this.rendered.nestedGroup.groups[selector],
661
- times,
662
- positions,
663
- );
664
- }
665
-
666
- /**
667
- * Add a single-axis translation animation track.
668
- * @param selector - path/id of group to be animated.
669
- * @param axis - which axis to translate along ("x", "y", or "z").
670
- * @param times - array of keyframe times.
671
- * @param values - array of translation values along the axis.
672
- */
673
- addTranslationTrack(
674
- selector: string,
675
- axis: Axis,
676
- times: number[],
677
- values: number[],
678
- ): void {
679
- this.animation.addTranslationTrack(
680
- selector,
681
- this.rendered.nestedGroup.groups[selector],
682
- axis,
683
- times,
684
- values,
685
- );
686
- }
687
-
688
- /**
689
- * Add a quaternion rotation animation track.
690
- * @param selector - path/id of group to be animated.
691
- * @param times - array of keyframe times.
692
- * @param quaternions - array of [x, y, z, w] quaternion values.
693
- */
694
- addQuaternionTrack(
695
- selector: string,
696
- times: number[],
697
- quaternions: number[][],
698
- ): void {
699
- this.animation.addQuaternionTrack(
700
- selector,
701
- this.rendered.nestedGroup.groups[selector],
702
- times,
703
- quaternions,
704
- );
705
- }
706
-
707
- /**
708
- * Add a single-axis rotation animation track.
709
- * @param selector - path/id of group to be animated.
710
- * @param axis - which axis to rotate around ("x", "y", or "z").
711
- * @param times - array of keyframe times.
712
- * @param angles - array of rotation angles in degrees.
713
- */
714
- addRotationTrack(
715
- selector: string,
716
- axis: Axis,
717
- times: number[],
718
- angles: number[],
719
- ): void {
720
- this.animation.addRotationTrack(
721
- selector,
722
- this.rendered.nestedGroup.groups[selector],
723
- axis,
724
- times,
725
- angles,
726
- );
727
- }
728
-
729
- /**
730
- * Initialize the animation.
731
- * @param duration - overall duration of the animation.
732
- * @param speed - speed of the animation.
733
- * @param label - animation label.
734
- * @param repeat - whether to repeat the animation.
735
- */
736
- initAnimation(
737
- duration: number,
738
- speed: number,
739
- label: string = "A",
740
- repeat: boolean = true,
741
- ): void {
742
- if (this.animation == null || this.animation.tracks.length === 0) {
743
- logger.error("Animation does not have tracks");
744
- return;
745
- }
746
- logger.debug("Animation initialized");
747
- if (!this.hasAnimationLoop) {
748
- this.toggleAnimationLoop(true);
749
- }
750
-
751
- this.state.set("animationMode", label === "E" ? "explode" : "animation");
752
- this.clipAction = this.animation.animate(
753
- this.rendered.nestedGroup.rootGroup!,
754
- duration,
755
- speed,
756
- repeat,
757
- );
758
- // Reset animation slider to start
759
- this.state.set("animationSliderValue", 0);
760
- }
761
-
762
- /**
763
- * Check whether animation object exists
764
- */
765
- hasAnimation(): boolean {
766
- return !!this.animation.clipAction;
767
- }
768
-
769
- /**
770
- * Clear the animation object and dispose dependent objects
771
- */
772
- clearAnimation(): void {
773
- if (this.animation) {
774
- deepDispose(this.animation);
775
- }
776
- this.state.set("animationMode", "none");
777
- this.toggleAnimationLoop(false);
778
- }
779
-
780
- /**
781
- * Set the animation to a specific relative time (0-1).
782
- * Pauses the animation at that point.
783
- * @param fraction - relative time between 0 and 1.
784
- */
785
- setRelativeTime(fraction: number): void {
786
- this.animation.setRelativeTime(fraction);
787
- this.state.set("animationSliderValue", fraction * 1000);
788
- }
789
-
790
- /**
791
- * Get the current relative animation time (0-1).
792
- * @returns relative time between 0 and 1.
793
- */
794
- getRelativeTime(): number {
795
- return this.animation.getRelativeTime();
796
- }
797
-
798
- // ---------------------------------------------------------------------------
799
- // Render Loop & Scene Updates
800
- // ---------------------------------------------------------------------------
801
-
802
- /**
803
- * Creates ChangeNotification object if new value != old value and sends change notifications via viewer.notifyCallback.
804
- * @param changes - change information.
805
- * @param notify - whether to send notification or not.
806
- */
807
- checkChanges = (
808
- changes: Record<string, unknown>,
809
- notify: boolean = true,
810
- ): void => {
811
- const changed: Record<string, StateChange<unknown>> = {};
812
- Object.keys(changes).forEach((key) => {
813
- if (!isEqual(this.lastNotification[key], changes[key])) {
814
- const change = structuredClone(changes[key]);
815
- changed[key] = {
816
- new: change,
817
- // map undefined in lastNotification to null to enable JSON exchange
818
- old:
819
- this.lastNotification[key] == null
820
- ? null
821
- : structuredClone(this.lastNotification[key]),
822
- };
823
- this.lastNotification[key] = change;
824
- }
825
- });
826
-
827
- if (Object.keys(changed).includes("position")) {
828
- if (this.keepHighlight) {
829
- this.keepHighlight = false;
830
- } else {
831
- this.state.set("highlightedButton", null);
832
- }
833
- }
834
-
835
- if (notify && this.notifyCallback && Object.keys(changed).length) {
836
- this.notifyCallback(changed);
837
- }
838
- };
839
-
840
- /**
841
- * Notifies the states by checking for changes and passing the states to the checkChanges method.
842
- */
843
- notifyStates = (): void => {
844
- this.checkChanges({ states: this.getStates() }, true);
845
- };
846
-
847
- /**
848
- * Render scene and update orientation marker
849
- * If no animation loop exists, this needs to be called manually after every camera/scene change
850
- * @param updateMarker - whether to update the orientation marker
851
- * @param notify - whether to send notification or not.
852
- */
853
- update = (updateMarker: boolean, notify: boolean = true): void => {
854
- if (!this.ready) return;
855
-
856
- if (this._externalGl) {
857
- this.renderer.resetState();
858
- }
859
- // When the composer is active, its RenderPass handles clearing;
860
- // skip manual clear to avoid double-clear artifacts.
861
- if (!this._studioManager.hasComposer) {
862
- this.renderer.clear();
863
- }
864
-
865
- if (
866
- this.raycaster &&
867
- this.raycaster.raycastMode &&
868
- !this.rendered.controls.isInteracting()
869
- ) {
870
- this.handleRaycast();
871
- }
872
-
873
- this.rendered.gridHelper.update(this.rendered.camera.getZoom());
874
-
875
- this.renderer.setViewport(
876
- 0,
877
- 0,
878
- this.state.get("cadWidth"),
879
- this.state.get("height"),
880
- );
881
-
882
- // Env background: render HDRI to 2D render target (fixed-FOV bgCamera)
883
- if (this._studioManager.isEnvBackgroundActive) {
884
- this._studioManager.updateEnvBackground(this.renderer, this.rendered.camera.getCamera());
885
- }
886
-
887
- // Render: use composer pipeline when available (AO + tone mapping + SMAA),
888
- // otherwise fall back to direct renderer.render().
889
- if (this._studioManager.hasComposer) {
890
- this._studioManager.render();
891
- } else {
892
- this.renderer.render(this.rendered.scene, this.rendered.camera.getCamera());
893
- }
894
- this.cadTools.update();
895
-
896
- this.rendered.directLight.position.copy(this.rendered.camera.getCamera().position);
897
-
898
- if (
899
- this.lastBbox != null &&
900
- (this.lastBbox.needsUpdate || this.bboxNeedsUpdate)
901
- ) {
902
- console.debug("updated bbox");
903
- this.lastBbox.bbox.update();
904
- this.lastBbox.needsUpdate = false;
905
- }
906
-
907
- if (updateMarker) {
908
- this.renderer.clearDepth(); // ensure orientation Marker is at the top
909
-
910
- this.rendered.orientationMarker.update(
911
- this.rendered.camera.getPosition().clone().sub(this.rendered.controls.getTarget()),
912
- this.rendered.camera.getQuaternion(),
913
- );
914
- this.rendered.orientationMarker.render(this.renderer);
915
- }
916
-
917
- if (this.animation) {
918
- this.animation.update();
919
- }
920
-
921
- this.checkChanges(
922
- {
923
- zoom: this.rendered.camera.getZoom(),
924
- position: this.rendered.camera.getPosition().toArray(),
925
- quaternion: this.rendered.camera.getQuaternion().toArray(),
926
- target: this.rendered.controls.getTarget().toArray(),
927
- },
928
- notify,
929
- );
930
-
931
- // In shared/external WebGL mode, clean up renderer state before external
932
- // renderers/hooks (overlays, etc.) draw on the same context.
933
- if (this._externalGl) {
934
- this.renderer.resetState();
935
- }
936
-
937
- if (this.onAfterRender) {
938
- this.onAfterRender();
939
- }
940
- };
941
-
942
- /**
943
- * Start the animation loop
944
- */
945
- animate = (): void => {
946
- if (this.continueAnimation) {
947
- requestAnimationFrame(this.animate);
948
- this.rendered.controls.update();
949
- this.update(true, true);
950
- } else {
951
- console.debug("three-cad-viewer: Animation loop stopped");
952
- }
953
- };
954
-
955
- toggleAnimationLoop(flag: boolean): void {
956
- if (flag) {
957
- this.continueAnimation = true;
958
- this.hasAnimationLoop = true;
959
- this.rendered.controls.removeChangeListener();
960
- console.debug("three-cad-viewer: Change listener removed");
961
- this.animate();
962
- console.debug("three-cad-viewer: Animation loop started");
963
- } else {
964
- if (this.hasAnimationLoop) {
965
- console.debug("three-cad-viewer: Turning animation loop off");
966
- }
967
- this.continueAnimation = false;
968
- this.hasAnimationLoop = false;
969
- this.rendered.controls.addChangeListener(() => this.update(true, true));
970
- console.debug("three-cad-viewer: Change listener registered");
971
-
972
- // ensure last animation cycle has finished
973
- setTimeout(() => this.update(true, true), 50);
974
- }
975
- }
976
-
977
- // ---------------------------------------------------------------------------
978
- // Cleanup & Disposal
979
- // ---------------------------------------------------------------------------
980
-
981
- /**
982
- * Remove all assets and event handlers. Call when done with the viewer.
983
- *
984
- * This disposes:
985
- * - WebGL renderer and context
986
- * - All Three.js objects (geometries, materials, textures)
987
- * - Event listeners
988
- * - CAD tools and raycaster
989
- *
990
- * After calling dispose(), the viewer instance should not be used.
991
- *
992
- * @public
993
- */
994
- dispose(): void {
995
- this.clear();
996
-
997
- // dispose studio resources (composer, floor, env, shadows — must be before renderer)
998
- this._studioManager.dispose();
999
- // dispose renderer
1000
- this.renderer.renderLists.dispose();
1001
- this.renderer.dispose();
1002
- // Skip context loss for externally provided WebGL contexts
1003
- if (!this._externalGl && typeof this.renderer.forceContextLoss === "function") {
1004
- this.renderer.forceContextLoss();
1005
- }
1006
- console.debug("three-cad-viewer: WebGL context disposed");
1007
-
1008
- this.materialSettings = null;
1009
- this.compactTree = null;
1010
- deepDispose(this.cadTools);
1011
- this.clipAction = null;
1012
- this.lastNotification = {};
1013
- this.clipNormal0 = null;
1014
- this.clipNormal1 = null;
1015
- this.clipNormal2 = null;
1016
- this.renderOptions = null;
1017
- this.tree = null;
1018
- // Info is owned by Display
1019
- this.bbox = null;
1020
- this._stencilCSize = 0;
1021
- this._treeNeedsRebuild = false;
1022
-
1023
- // Flush any pending deferred disposals
1024
- for (const obj of this._pendingDisposal) {
1025
- deepDispose(obj);
1026
- }
1027
- this._pendingDisposal = [];
1028
-
1029
- this.keymap = null;
1030
- if (this.raycaster) {
1031
- this.raycaster.dispose();
1032
- this.raycaster = null;
1033
- }
1034
- }
1035
-
1036
- /**
1037
- * Clear the current CAD view without disposing the renderer.
1038
- *
1039
- * Use this to remove shapes before rendering new ones.
1040
- * The viewer remains usable after clear().
1041
- *
1042
- * @public
1043
- */
1044
- clear(): void {
1045
- if (this._rendered) {
1046
- // stop animation
1047
- this.hasAnimationLoop = false;
1048
- this.continueAnimation = false;
1049
-
1050
- // remove change listener if exists
1051
- this._rendered.controls.removeChangeListener();
1052
- console.debug("three-cad-viewer: Change listener removed");
1053
-
1054
- this.hasAnimationLoop = false;
1055
- this.state.set("animationMode", "none");
1056
-
1057
- if (this.animation != null) {
1058
- deepDispose(this.animation);
1059
- }
1060
-
1061
- // Reset zscale state
1062
- if (this.shapes?.format === "GDS") {
1063
- this.state.set("zscaleActive", false);
1064
- }
1065
-
1066
- // Reset to tree tab for next render.
1067
- // IMPORTANT: This fires the activeTab subscription synchronously,
1068
- // which calls switchToTab("tree", oldTab). If oldTab was "studio",
1069
- // leaveStudioMode() runs here, while _rendered and scene are still valid.
1070
- // Do NOT move this after deepDispose(scene).
1071
- this.state.set("activeTab", "tree");
1072
-
1073
- // clear render canvas
1074
- this.renderer.clear();
1075
-
1076
- // deselect measurement tools
1077
- if (this.cadTools) {
1078
- this.cadTools.disable();
1079
- const currentTool = this.state.get("activeTool");
1080
- if (currentTool != null) {
1081
- this.state.set("activeTool", null);
1082
- this.display.setTool(currentTool, false);
1083
- }
1084
- }
1085
-
1086
- // dispose all rendered state objects
1087
- deepDispose(this._rendered.scene);
1088
-
1089
- // Studio lights were children of the scene and have been disposed by
1090
- deepDispose(this._rendered.gridHelper);
1091
- deepDispose(this._rendered.clipping);
1092
- deepDispose(this._rendered.camera);
1093
- deepDispose(this._rendered.controls);
1094
- deepDispose(this._rendered.treeview);
1095
-
1096
- // clear tree view
1097
- this.display.clearCadTree();
1098
-
1099
- // clear info
1100
- deepDispose(this.info);
1101
-
1102
- this._rendered = null;
1103
- this.ready = false;
1104
- }
1105
-
1106
- if (this.shapes != null) {
1107
- // Shapes is data (not THREE.js objects), setting to null allows GC
1108
- this.shapes = null;
1109
- }
1110
-
1111
- if (this.expandedNestedGroup != null) {
1112
- deepDispose(this.expandedNestedGroup);
1113
- this.expandedNestedGroup = null;
1114
- }
1115
- if (this.compactNestedGroup != null) {
1116
- deepDispose(this.compactNestedGroup);
1117
- this.compactNestedGroup = null;
1118
- }
1119
- }
1120
-
1121
- // ---------------------------------------------------------------------------
1122
- // Scene Rendering & Tree Management
1123
- // ---------------------------------------------------------------------------
1124
-
1125
- /**
1126
- * Synchronizes the states of two tree structures recursively.
1127
- *
1128
- * @param compactTree - The compact tree structure.
1129
- * @param expandedTree - The expanded tree structure.
1130
- * @param exploded - Whether rendering in exploded mode.
1131
- * @param path - The current path in the tree structure.
1132
- */
1133
- syncTreeStates = (
1134
- compactTree: ShapeTreeData | VisibilityState,
1135
- expandedTree: ShapeTreeData | VisibilityState,
1136
- exploded: boolean,
1137
- path: string,
1138
- ): void => {
1139
- // Leaf case: compactTree is a VisibilityState, expandedTree has type/label structure
1140
- if (isVisibilityState(compactTree)) {
1141
- // expandedTree must be ShapeTreeData at this point (type level: shapes/edges/vertices)
1142
- if (!isShapeTreeData(expandedTree)) return;
1143
- const expandedData = expandedTree;
1144
-
1145
- if (exploded) {
1146
- // Apply compact state to all expanded children
1147
- for (const typeKey in expandedData) {
1148
- const typeNode = expandedData[typeKey];
1149
- if (!isShapeTreeData(typeNode)) continue;
1150
-
1151
- for (const labelKey in typeNode) {
1152
- const leafState = typeNode[labelKey];
1153
- if (!isVisibilityState(leafState)) continue;
1154
-
1155
- const id = `${path}/${typeKey}/${labelKey}`;
1156
- const objectGroup = this.expandedNestedGroup!.groups[id];
1157
- if (!isObjectGroup(objectGroup)) continue;
1158
-
1159
- objectGroup.setShapeVisible(compactTree[0] === 1);
1160
- objectGroup.setEdgesVisible(compactTree[1] === 1);
1161
-
1162
- // Sync state (unless disabled = 3)
1163
- if (leafState[0] !== 3) leafState[0] = compactTree[0];
1164
- if (leafState[1] !== 3) leafState[1] = compactTree[1];
1165
- }
1166
- }
1167
- } else {
1168
- // Compute visibility from expanded children
1169
- const objectGroup = this.compactNestedGroup!.groups[path];
1170
- if (!isObjectGroup(objectGroup)) return;
1171
-
1172
- let shapeVisible = false;
1173
- let edgeVisible = false;
1174
-
1175
- for (const typeKey in expandedData) {
1176
- const typeNode = expandedData[typeKey];
1177
- if (!isShapeTreeData(typeNode)) continue;
1178
-
1179
- for (const labelKey in typeNode) {
1180
- const leafState = typeNode[labelKey];
1181
- if (!isVisibilityState(leafState)) continue;
1182
-
1183
- if (leafState[0] === 1) shapeVisible = true;
1184
- if (leafState[1] === 1) edgeVisible = true;
1185
- }
1186
- }
1187
-
1188
- objectGroup.setShapeVisible(shapeVisible);
1189
- objectGroup.setEdgesVisible(edgeVisible);
1190
-
1191
- // Sync compact state (unless disabled = 3)
1192
- if (compactTree[0] !== 3) compactTree[0] = shapeVisible ? 1 : 0;
1193
- if (compactTree[1] !== 3) compactTree[1] = edgeVisible ? 1 : 0;
1194
- }
1195
- } else {
1196
- // Branch case: recurse into children
1197
- if (!isShapeTreeData(expandedTree)) return;
1198
- const expandedData = expandedTree;
1199
- for (const key in compactTree) {
1200
- const id = `${path}/${key}`;
1201
- this.syncTreeStates(compactTree[key], expandedData[key], exploded, id);
1202
- }
1203
- }
1204
- };
1205
-
1206
- /**
1207
- * Get the color of a node from its path
1208
- * @param path - path of the CAD object
1209
- */
1210
- getNodeColor = (path: string): string | null => {
1211
- // Use _rendered directly since this may be called during initial render
1212
- // before _rendered is fully set up
1213
- if (!this._rendered) {
1214
- return null;
1215
- }
1216
- const group = this._rendered.nestedGroup.groups["/" + path];
1217
- if (group instanceof ObjectGroup) {
1218
- if (group.front) {
1219
- return "#" + group.front.material.color.getHexString();
1220
- }
1221
- if (group.originalColor) {
1222
- return "#" + group.originalColor.getHexString();
1223
- }
1224
- }
1225
- return null;
1226
- };
1227
-
1228
- /**
1229
- * Build nestedGroup and treeview for initial render.
1230
- * @param scene - The scene to add the group to
1231
- * @param expanded - whether to render the exploded or compact version
1232
- * @returns The nestedGroup and treeview
1233
- */
1234
- private buildInitialGroup(
1235
- scene: THREE.Scene,
1236
- expanded: boolean,
1237
- ): { nestedGroup: NestedGroup; treeview: TreeView } {
1238
- const timer = new Timer("buildInitialGroup", this.state.get("timeit"));
1239
-
1240
- this.setRenderDefaults(this.renderOptions!);
1241
- const result = this.renderTessellatedShapes(expanded, this.shapes!);
1242
- const nestedGroup = result.group;
1243
-
1244
- if (expanded) {
1245
- this.expandedNestedGroup = result.group;
1246
- this.expandedTree = result.tree;
1247
- } else {
1248
- this.compactNestedGroup = result.group;
1249
- this.compactTree = result.tree;
1250
- }
1251
-
1252
- // Configure the nested group
1253
- nestedGroup.setTransparent(this.state.get("transparent"));
1254
- nestedGroup.setBlackEdges(this.state.get("blackEdges"));
1255
- nestedGroup.setMetalness(this.state.get("metalness"));
1256
- nestedGroup.setRoughness(this.state.get("roughness"));
1257
- nestedGroup.setPolygonOffset(2);
1258
-
1259
- timer.split(`rendered${expanded ? " exploded" : " compact"} shapes`);
1260
-
1261
- this.tree = expanded ? this.expandedTree : this.compactTree;
1262
- scene.children[0] = nestedGroup.rootGroup!;
1263
- timer.split("added shapes to scene");
1264
-
1265
- if (!this.tree) {
1266
- throw new Error("Tree not initialized");
1267
- }
1268
- const treeview = new TreeView(
1269
- this.tree,
1270
- this.display.cadTreeScrollContainer,
1271
- this.setObject,
1272
- this.handlePick,
1273
- this.update,
1274
- this.notifyStates,
1275
- this.getNodeColor,
1276
- this.state.get("theme"),
1277
- this.state.get("newTreeBehavior"),
1278
- false,
1279
- );
1280
-
1281
- this.display.clearCadTree();
1282
- const t = treeview.create();
1283
- timer.split("created tree");
1284
-
1285
- this.display.addCadTree(t);
1286
- treeview.render();
1287
- timer.split("rendered tree");
1288
- timer.stop();
1289
-
1290
- return { nestedGroup, treeview };
1291
- }
1292
-
1293
- /**
1294
- * Toggle the two version of the NestedGroup.
1295
- * Must only be called after render() has completed.
1296
- * @param expanded - whether to render the exploded or compact version
1297
- */
1298
- toggleGroup(expanded: boolean): void {
1299
- if (!this.rendered) {
1300
- throw new Error("toggleGroup called before render()");
1301
- }
1302
-
1303
- const timer = new Timer("toggleGroup", this.state.get("timeit"));
1304
-
1305
- const _config = (group: NestedGroup): void => {
1306
- group.setTransparent(this.state.get("transparent"));
1307
- group.setBlackEdges(this.state.get("blackEdges"));
1308
- group.setMetalness(this.state.get("metalness"));
1309
- group.setRoughness(this.state.get("roughness"));
1310
- group.setPolygonOffset(2);
1311
- };
1312
-
1313
- let nestedGroup: NestedGroup;
1314
-
1315
- if (
1316
- (this.compactNestedGroup == null && !expanded) ||
1317
- (this.expandedNestedGroup == null && expanded)
1318
- ) {
1319
- this.setRenderDefaults(this.renderOptions!);
1320
- const result = this.renderTessellatedShapes(expanded, this.shapes!);
1321
- nestedGroup = result.group;
1322
-
1323
- if (expanded) {
1324
- this.expandedNestedGroup = result.group;
1325
- this.expandedTree = result.tree;
1326
- } else {
1327
- this.compactNestedGroup = result.group;
1328
- this.compactTree = result.tree;
1329
- }
1330
- _config(nestedGroup);
1331
- timer.split(`rendered${expanded ? " exploded" : " compact"} shapes`);
1332
- } else {
1333
- nestedGroup = expanded
1334
- ? this.expandedNestedGroup!
1335
- : this.compactNestedGroup!;
1336
- _config(nestedGroup);
1337
- }
1338
-
1339
- // only sync if both trees exist
1340
- if (this.expandedTree) {
1341
- this.syncTreeStates(this.compactTree!, this.expandedTree, expanded, "");
1342
- }
1343
- timer.split("synched tree states");
1344
-
1345
- this.tree = expanded ? this.expandedTree : this.compactTree;
1346
- this.rendered.scene.children[0] = nestedGroup.rootGroup!;
1347
- this.rendered.nestedGroup = nestedGroup;
1348
- timer.split("added shapes to scene");
1349
-
1350
- deepDispose(this.rendered.treeview);
1351
- if (!this.tree) {
1352
- throw new Error("Tree not initialized");
1353
- }
1354
- const treeview = new TreeView(
1355
- this.tree,
1356
- this.display.cadTreeScrollContainer,
1357
- this.setObject,
1358
- this.handlePick,
1359
- this.update,
1360
- this.notifyStates,
1361
- this.getNodeColor,
1362
- this.state.get("theme"),
1363
- this.state.get("newTreeBehavior"),
1364
- false,
1365
- );
1366
- this.rendered.treeview = treeview;
1367
-
1368
- this.display.clearCadTree();
1369
- const t = treeview.create();
1370
- timer.split("created tree");
1371
-
1372
- this.display.addCadTree(t);
1373
- treeview.render();
1374
- timer.split("rendered tree");
1375
- timer.stop();
1376
- }
1377
-
1378
- /**
1379
- * Set the active sidebar tab.
1380
- * @param tabName - Tab name: "tree", "clip", "material", "zebra", or "studio"
1381
- * @param notify - whether to send notification or not.
1382
- */
1383
- setActiveTab(tabName: ActiveTab, notify: boolean = true): void {
1384
- this.state.set("activeTab", tabName, notify);
1385
- }
1386
-
1387
- toggleTab(disable: boolean): void {
1388
- const timer = new Timer("toggleTab", this.state.get("timeit"));
1389
- this.setActiveTab("tree", false);
1390
- timer.split("collapse tree");
1391
- switch (this.state.get("collapse")) {
1392
- case CollapseState.COLLAPSED:
1393
- this.rendered.treeview.collapseAll();
1394
- break;
1395
- case CollapseState.ROOT:
1396
- this.rendered.treeview.openLevel(1);
1397
- break;
1398
- case CollapseState.EXPANDED:
1399
- this.rendered.treeview.expandAll();
1400
- break;
1401
- case CollapseState.LEAVES:
1402
- this.rendered.treeview.openLevel(-1);
1403
- break;
1404
- default:
1405
- break;
1406
- }
1407
- this.checkChanges({ states: this.getStates() }, true);
1408
- timer.split("notify state changes");
1409
- timer.stop();
1410
- this.display.toggleClippingTab(!disable);
1411
- }
1412
-
1413
- /**
1414
- * Render a CAD object and build the navigation tree.
1415
- *
1416
- * This is the main entry point for displaying CAD geometry. It:
1417
- * - Creates the Three.js scene with lights, camera, and controls
1418
- * - Tessellates and renders the shape geometry
1419
- * - Builds the navigation tree UI
1420
- * - Sets up clipping planes and helpers
1421
- *
1422
- * @param shapes - the Shapes object representing the tessellated CAD object
1423
- * @param renderOptions - the render options (edge color, opacity, etc.)
1424
- * @param viewerOptions - the viewer options (camera position, clipping, etc.)
1425
- * @public
1426
- */
1427
- render(
1428
- shapes: Shapes,
1429
- renderOptions: RenderOptions,
1430
- viewerOptions: ViewerOptions,
1431
- ): void {
1432
- // Decode instanced/compressed format if detected
1433
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
1434
- if (isInstancedFormat(shapes as any)) {
1435
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
1436
- shapes = decodeInstancedFormat(shapes as any);
1437
- }
1438
- // Decode any remaining inline base64 buffers (e.g., edge/vertex-only objects)
1439
- decodeInlineBuffers(shapes);
1440
- this.shapes = shapes;
1441
- this.renderOptions = renderOptions;
1442
- this.setViewerDefaults(viewerOptions);
1443
-
1444
- // Backward compat: studioOptions on shapes root is deprecated
1445
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
1446
- if ((shapes as any).studioOptions) {
1447
- logger.warn("shapes.studioOptions is deprecated — pass studio settings in viewerOptions instead");
1448
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
1449
- this.state.updateStudioState((shapes as any).studioOptions);
1450
- }
1451
-
1452
- this.animation.cleanBackup();
1453
-
1454
- const timer = new Timer("viewer", this.state.get("timeit"));
1455
-
1456
- const scene = new THREE.Scene();
1457
-
1458
- //
1459
- // add shapes and cad tree
1460
- //
1461
-
1462
- const { nestedGroup, treeview } = this.buildInitialGroup(scene, false);
1463
- timer.split("scene and tree done");
1464
-
1465
- if (!this.bbox) {
1466
- this.bbox = nestedGroup.boundingBox();
1467
- }
1468
- const center = new THREE.Vector3();
1469
- this.bbox.getCenter(center);
1470
- this.bb_max = this.bbox.max_dist_from_center();
1471
- this.bb_radius = Math.max(
1472
- this.bbox.boundingSphere().radius,
1473
- center.length(),
1474
- );
1475
- timer.split("bounding box");
1476
-
1477
- //
1478
- // create cameras
1479
- //
1480
- const camera = new Camera(
1481
- this.state.get("cadWidth"),
1482
- this.state.get("height"),
1483
- this.bb_radius,
1484
- viewerOptions.target ?? this.bbox.center(),
1485
- this.state.get("ortho"),
1486
- viewerOptions.up ?? this.state.get("up"),
1487
- );
1488
-
1489
- //
1490
- // build mouse/touch controls
1491
- //
1492
- const controls = new Controls(
1493
- this.state.get("control"),
1494
- camera.getCamera(),
1495
- new THREE.Vector3(...(viewerOptions.target ?? this.bbox.center())),
1496
- this.renderer.domElement,
1497
- this.state.get("rotateSpeed"),
1498
- this.state.get("zoomSpeed"),
1499
- this.state.get("panSpeed"),
1500
- this.state.get("holroyd"),
1501
- );
1502
- // Disable keyboard controls (these properties exist on THREE.js controls internally)
1503
- controls.controls.enableKeys = false;
1504
-
1505
- // ensure panning works for screen coordinates (only exists on OrbitControls)
1506
- if ("screenSpacePanning" in controls.controls) {
1507
- controls.controls.screenSpacePanning = true;
1508
- }
1509
-
1510
- //
1511
- // add lights
1512
- //
1513
-
1514
- const ambientLight = new THREE.AmbientLight(
1515
- 0xffffff,
1516
- scaleLight(this.state.get("ambientIntensity")),
1517
- );
1518
- scene.add(ambientLight);
1519
-
1520
- const directLight = new THREE.DirectionalLight(
1521
- 0xffffff,
1522
- scaleLight(this.state.get("directIntensity")),
1523
- );
1524
- scene.add(directLight);
1525
-
1526
- //
1527
- // add grid helpers
1528
- //
1529
-
1530
- const gridHelper = new Grid({
1531
- bbox: this.bbox,
1532
- ticks: this.state.get("ticks"),
1533
- gridFontSize: this.state.get("gridFontSize"),
1534
- centerGrid: this.state.get("centerGrid"),
1535
- axes0: this.state.get("axes0"),
1536
- grid: [...this.state.get("grid")],
1537
- flipY: viewerOptions.up === "Z",
1538
- theme: this.state.get("theme"),
1539
- cadWidth: this.state.get("cadWidth"),
1540
- height: this.state.get("height"),
1541
- maxAnisotropy: this.renderer.capabilities.getMaxAnisotropy(),
1542
- ...(this.state.get("tools")
1543
- ? {
1544
- tickValueElement: this.display.tickValueElement,
1545
- tickInfoElement: this.display.tickInfoElement,
1546
- }
1547
- : {}),
1548
- getCamera: () => this._rendered?.camera.getCamera() ?? null,
1549
- getAxes0: () => this.state?.get("axes0") ?? false,
1550
- });
1551
- gridHelper.computeGrid();
1552
-
1553
- scene.add(gridHelper);
1554
-
1555
- this.gridSize = gridHelper.size;
1556
-
1557
- //
1558
- // add axes helper
1559
- //
1560
-
1561
- const axesHelper = new AxesHelper(
1562
- this.bbox.center(),
1563
- this.gridSize / 2,
1564
- 2,
1565
- this.state.get("cadWidth"),
1566
- this.state.get("height"),
1567
- this.state.get("axes0"),
1568
- this.state.get("axes"),
1569
- this.state.get("theme"),
1570
- );
1571
- scene.add(axesHelper);
1572
-
1573
- //
1574
- // set up clipping planes and helpers
1575
- //
1576
- const cSize =
1577
- 1.1 *
1578
- Math.max(
1579
- Math.abs(this.bbox.min.length()),
1580
- Math.abs(this.bbox.max.length()),
1581
- );
1582
- this._stencilCSize = cSize;
1583
- const clipping = new Clipping(
1584
- this.bbox.center(),
1585
- 2 * cSize,
1586
- nestedGroup,
1587
- {
1588
- onNormalChange: (index, normalArray) =>
1589
- this.display.setNormalLabel(index, normalArray),
1590
- },
1591
- this.state.get("theme"),
1592
- );
1593
-
1594
- scene.add(clipping);
1595
-
1596
- // Add studio floor group to scene (hidden by default, shown in enterStudioMode)
1597
- scene.add(this._studioManager.floor.group);
1598
-
1599
- // Theme is already resolved ("light" or "dark") by ViewerState constructor
1600
- const theme = this.state.get("theme");
1601
-
1602
- //
1603
- // set up the orientation marker
1604
- //
1605
-
1606
- const orientationMarker = new OrientationMarker(
1607
- 80,
1608
- 80,
1609
- camera.getCamera(),
1610
- theme,
1611
- );
1612
- orientationMarker.create();
1613
-
1614
- //
1615
- // Assemble rendered state
1616
- //
1617
- this._rendered = {
1618
- scene,
1619
- camera,
1620
- controls,
1621
- nestedGroup,
1622
- gridHelper,
1623
- axesHelper,
1624
- clipping,
1625
- treeview,
1626
- orientationMarker,
1627
- ambientLight,
1628
- directLight,
1629
- };
1630
-
1631
- // Now that rendered state exists, configure camera position
1632
- if (viewerOptions.position == null && viewerOptions.quaternion == null) {
1633
- this.presetCamera("iso", this.state.get("zoom"));
1634
- this.state.set("highlightedButton", "iso");
1635
- } else if (viewerOptions.position != null) {
1636
- this.setCamera(
1637
- false,
1638
- new THREE.Vector3(...viewerOptions.position),
1639
- viewerOptions.quaternion
1640
- ? new THREE.Quaternion(...viewerOptions.quaternion)
1641
- : null,
1642
- this.state.get("zoom"),
1643
- );
1644
- if (viewerOptions.quaternion == null) {
1645
- camera.lookAtTarget();
1646
- }
1647
- } else {
1648
- this.display.addInfoHtml(
1649
- "<b>quaternion needs position to be provided, falling back to ISO view</b>",
1650
- );
1651
- this.presetCamera("iso", this.state.get("zoom"));
1652
- }
1653
- controls.update();
1654
-
1655
- // Save the new state again
1656
- controls.saveState();
1657
-
1658
- this.setAmbientLight(this.state.get("ambientIntensity"));
1659
- this.setDirectLight(this.state.get("directIntensity"));
1660
-
1661
- this.display.setSliderLimits(this.gridSize / 2);
1662
- this.display.syncClipSlidersFromState();
1663
-
1664
- // Compute clip slider values (used later after ready=true)
1665
- const clipSlider0 =
1666
- viewerOptions.clipSlider0 != null
1667
- ? viewerOptions.clipSlider0
1668
- : this.gridSize / 2;
1669
- const clipSlider1 =
1670
- viewerOptions.clipSlider1 != null
1671
- ? viewerOptions.clipSlider1
1672
- : this.gridSize / 2;
1673
- const clipSlider2 =
1674
- viewerOptions.clipSlider2 != null
1675
- ? viewerOptions.clipSlider2
1676
- : this.gridSize / 2;
1677
-
1678
- nestedGroup.setClipPlanes(clipping.clipPlanes);
1679
-
1680
- this.setLocalClipping(false); // only allow clipping when Clipping tab is selected
1681
-
1682
- clipping.setVisible(false);
1683
-
1684
- this.toggleTab(false);
1685
-
1686
- //
1687
- // update UI elements
1688
- //
1689
-
1690
- this.display.updateUI();
1691
- timer.split("ui updated");
1692
- this.display.autoCollapse();
1693
-
1694
- timer.split("stencil done");
1695
- //
1696
- // show the rendering
1697
- //
1698
-
1699
- this.toggleAnimationLoop(this.hasAnimationLoop);
1700
-
1701
- this.ready = true;
1702
-
1703
- if (!this.state.get("tools")) {
1704
- this.display.showToolsPanel(false);
1705
- this.rendered.orientationMarker.setVisible(false);
1706
- }
1707
-
1708
- // Apply clip settings AFTER ready=true (clip setters check this.ready)
1709
- // Set normals first (if provided), passing slider values to avoid reset to gridSize/2
1710
- this.setClipNormal(0, viewerOptions.clipNormal0 ?? null, clipSlider0, true);
1711
- this.setClipNormal(1, viewerOptions.clipNormal1 ?? null, clipSlider1, true);
1712
- this.setClipNormal(2, viewerOptions.clipNormal2 ?? null, clipSlider2, true);
1713
- // Set sliders for any planes without custom normals (setClipNormal returns early if normal is null)
1714
- this.setClipSlider(0, clipSlider0, true);
1715
- this.setClipSlider(1, clipSlider1, true);
1716
- this.setClipSlider(2, clipSlider2, true);
1717
- this.setClipIntersection(viewerOptions.clipIntersection ?? false, true);
1718
- this.setClipObjectColorCaps(viewerOptions.clipObjectColors ?? false, true);
1719
- this.setClipPlaneHelpers(viewerOptions.clipPlaneHelpers ?? false, true);
1720
-
1721
- this.display.showReadyMessage(version, this.state.get("control"));
1722
- timer.split("show done");
1723
-
1724
- // Notify computed values and all config defaults
1725
- if (this.notifyCallback) {
1726
- this.notifyCallback({
1727
- // Computed values from controls/camera
1728
- target: { old: null, new: toVector3Tuple(controls.target.toArray()) },
1729
- target0: { old: null, new: toVector3Tuple(controls.target0.toArray()) },
1730
- position: { old: null, new: this.rendered.camera.getPosition().toArray() },
1731
- quaternion: { old: null, new: this.rendered.camera.getQuaternion().toArray() },
1732
- zoom: { old: null, new: this.rendered.camera.getZoom() },
1733
- // All config values from state
1734
- ...this.state.getAllNotifiable(),
1735
- });
1736
- }
1737
- timer.split("notification done");
1738
-
1739
- this.update(true, false);
1740
- treeview.update();
1741
- this.display.setTheme(this.state.get("theme"));
1742
-
1743
- this.setZebraCount(this.state.get("zebraCount"));
1744
- this.setZebraDirection(this.state.get("zebraDirection"));
1745
- this.setZebraOpacity(this.state.get("zebraOpacity"));
1746
- this.setZebraColorScheme(this.state.get("zebraColorScheme"));
1747
- this.setZebraMappingMode(this.state.get("zebraMappingMode"));
1748
-
1749
- timer.split("update done");
1750
- timer.stop();
1751
- }
1752
-
1753
- // ---------------------------------------------------------------------------
1754
- // Camera Controls
1755
- // ---------------------------------------------------------------------------
1756
-
1757
- /**
1758
- * Move the camera to a given location.
1759
- * @param relative - flag whether the position is a relative (e.g. [1,1,1] for iso) or absolute point.
1760
- * @param position - the camera position as THREE.Vector3
1761
- * @param quaternion - the camera rotation expressed by a quaternion.
1762
- * @param zoom - zoom value.
1763
- * @param notify - whether to send notification or not.
1764
- * @public
1765
- */
1766
- setCamera = (
1767
- relative: boolean,
1768
- position: THREE.Vector3,
1769
- quaternion: THREE.Quaternion | null = null,
1770
- zoom: number | null = null,
1771
- notify: boolean = true,
1772
- ): void => {
1773
- this.rendered.camera.setupCamera(relative, position, quaternion, zoom);
1774
- this.update(true, notify);
1775
- };
1776
-
1777
- /**
1778
- * Move the camera to one of the preset locations.
1779
- * @param dir - can be "iso", "top", "bottom", "front", "rear", "left", "right"
1780
- * @param zoom - zoom value
1781
- * @param notify - whether to send notification or not.
1782
- * @public
1783
- */
1784
- presetCamera = (
1785
- dir: CameraDirection,
1786
- zoom: number | null = null,
1787
- notify: boolean = true,
1788
- ): void => {
1789
- this.rendered.camera.target = new THREE.Vector3(...this.bbox!.center());
1790
- this.rendered.camera.presetCamera(dir, zoom);
1791
- this.rendered.controls.setTarget(this.rendered.camera.target);
1792
- this.update(true, notify);
1793
- };
1794
-
1795
- /**
1796
- * Get reset location value.
1797
- * @returns target, position, quaternion, zoom as object.
1798
- */
1799
- getResetLocation = (): ResetLocation => {
1800
- return this.rendered.controls.getResetLocation();
1801
- };
1802
-
1803
- /**
1804
- * Set reset location value.
1805
- * @param target - camera target as 3 dim Array [x,y,z].
1806
- * @param position - camera position as 3 dim Array [x,y,z].
1807
- * @param quaternion - camera rotation as 4 dim quaternion array [x,y,z,w].
1808
- * @param zoom - camera zoom value.
1809
- * @param notify - whether to send notification or not.
1810
- */
1811
- setResetLocation = (
1812
- target: Vector3Tuple,
1813
- position: Vector3Tuple,
1814
- quaternion: QuaternionTuple,
1815
- zoom: number,
1816
- notify: boolean = true,
1817
- ): void => {
1818
- const location = this.getResetLocation();
1819
- this.rendered.controls.setResetLocation(
1820
- new THREE.Vector3(...target),
1821
- new THREE.Vector3(...position),
1822
- new THREE.Quaternion(...quaternion),
1823
- zoom,
1824
- );
1825
- if (notify && this.notifyCallback) {
1826
- this.notifyCallback({
1827
- target0: {
1828
- old: toVector3Tuple(location.target0.toArray()),
1829
- new: target,
1830
- },
1831
- position0: {
1832
- old: toVector3Tuple(location.position0.toArray()),
1833
- new: position,
1834
- },
1835
- quaternion0: {
1836
- old: toQuaternionTuple(location.quaternion0.toArray()),
1837
- new: quaternion,
1838
- },
1839
- zoom0: { old: location.zoom0, new: zoom },
1840
- });
1841
- }
1842
- };
1843
-
1844
- // ---------------------------------------------------------------------------
1845
- // Camera Type & Projection
1846
- // ---------------------------------------------------------------------------
1847
-
1848
- /**
1849
- * Get camera type.
1850
- * @returns "ortho" or "perspective".
1851
- */
1852
- getCameraType(): string {
1853
- return this.rendered.camera.ortho ? "ortho" : "perspective";
1854
- }
1855
-
1856
- /**
1857
- * Set camera mode to OrthographicCamera or PerspectiveCamera.
1858
- * @param flag - true for orthographic, false for perspective
1859
- * @param notify - whether to send notification or not.
1860
- * @public
1861
- */
1862
- switchCamera(flag: boolean, notify: boolean = true): void {
1863
- this.state.set("ortho", flag, notify);
1864
- this.rendered.camera.switchCamera(flag);
1865
- this.rendered.controls.setCamera(this.rendered.camera.getCamera());
1866
-
1867
- // Update composer camera after the actual swap (not in the ortho
1868
- // subscriber, which fires before the camera switches)
1869
- this._studioManager.setCamera(this.rendered.camera.getCamera());
1870
-
1871
- this.rendered.gridHelper.scaleLabels();
1872
- this.rendered.gridHelper.update(this.rendered.camera.getZoom(), true);
1873
-
1874
- this.update(true);
1875
- }
1876
-
1877
- /**
1878
- * Recenter camera on the bounding box center of all objects.
1879
- * @param notify - whether to send notification or not.
1880
- */
1881
- recenterCamera(notify: boolean = true): void {
1882
- const target = new THREE.Vector3(...this.bbox!.center());
1883
- this.setCameraTarget(target);
1884
- this.update(true, notify);
1885
- }
1886
-
1887
- /**
1888
- * Centers the camera view on all visible objects in the scene.
1889
- * Calculates a bounding box that encompasses all visible ObjectGroup instances
1890
- * and sets the camera target to the center of that bounding box.
1891
- *
1892
- * @param notify - Whether to notify listeners of the camera update
1893
- */
1894
- centerVisibleObjects(notify: boolean = true): void {
1895
- const groups = this.rendered.nestedGroup.groups;
1896
-
1897
- const bbox = new BoundingBox();
1898
- for (const path in groups) {
1899
- const obj = groups[path];
1900
- if (obj instanceof ObjectGroup) {
1901
- if (obj.getVisibility()) {
1902
- bbox.expandByObject(obj);
1903
- }
1904
- }
1905
- }
1906
- const target = new THREE.Vector3(...bbox.center());
1907
- this.setCameraTarget(target);
1908
- this.update(true, notify);
1909
- }
1910
-
1911
- /**
1912
- * Reset zoom to 1.0.
1913
- * @public
1914
- */
1915
- resize = (): void => {
1916
- this.rendered.camera.changeDimensions(
1917
- this.bb_radius,
1918
- this.state.get("cadWidth"),
1919
- this.state.get("height"),
1920
- );
1921
- this.rendered.camera.setZoom(1.0);
1922
- this.rendered.camera.updateProjectionMatrix();
1923
- this.update(true);
1924
- };
1925
-
1926
- /**
1927
- * Reset the view to the initial camera and controls settings.
1928
- * @public
1929
- */
1930
- reset = (): void => {
1931
- this.rendered.camera.changeDimensions(
1932
- this.bb_radius,
1933
- this.state.get("cadWidth"),
1934
- this.state.get("height"),
1935
- );
1936
- this.rendered.controls.reset();
1937
- this.update(true);
1938
- };
1939
-
1940
- /**
1941
- * Enable/disable local clipping
1942
- * @param flag - whether to enable local clipping
1943
- */
1944
- setLocalClipping(flag: boolean): void {
1945
- this.renderer.localClippingEnabled = flag;
1946
- this.update(this.updateMarker);
1947
- }
1948
-
1949
- // ---------------------------------------------------------------------------
1950
- // Object Visibility & Bounding Box
1951
- // ---------------------------------------------------------------------------
1952
-
1953
- /**
1954
- * Sets the visibility state of an object in the viewer.
1955
- *
1956
- * @param path - The path of the object.
1957
- * @param state - The visibility state (0 or 1).
1958
- * @param iconNumber - The icon number.
1959
- * @param notify - Whether to notify the changes.
1960
- * @param update - Whether to update the view.
1961
- */
1962
- setObject = (
1963
- path: string,
1964
- state: number,
1965
- iconNumber: number,
1966
- notify: boolean = true,
1967
- update: boolean = true,
1968
- ): void => {
1969
- const objectGroup = this.rendered.nestedGroup.groups[path];
1970
- if (objectGroup != null && objectGroup instanceof ObjectGroup) {
1971
- if (iconNumber === 0) {
1972
- objectGroup.setShapeVisible(state === 1);
1973
- } else {
1974
- objectGroup.setEdgesVisible(state === 1);
1975
- }
1976
- if (notify) {
1977
- const stateObj: Record<string, VisibilityState> = {};
1978
- const state_ = this.getState(path);
1979
- if (state_) stateObj[path] = state_;
1980
- }
1981
- if (update) {
1982
- this.update(this.updateMarker);
1983
- }
1984
- }
1985
- };
1986
-
1987
- /**
1988
- * Sets the bounding box for a given ID.
1989
- * @param id - The ID of the group.
1990
- */
1991
- setBoundingBox = (id: string): void => {
1992
- const group = this.rendered.nestedGroup.groups[id];
1993
- if (group != null) {
1994
- if (this.lastBbox != null) {
1995
- this.rendered.scene.remove(this.lastBbox.bbox);
1996
- this.lastBbox.bbox.geometry.dispose();
1997
- const mat = this.lastBbox.bbox.material;
1998
- if (Array.isArray(mat)) {
1999
- mat.forEach((m) => m.dispose());
2000
- } else {
2001
- mat.dispose();
2002
- }
2003
- }
2004
- if (
2005
- this.lastBbox == null ||
2006
- (this.lastBbox != null && id !== this.lastBbox.id)
2007
- ) {
2008
- this.lastBbox = {
2009
- id: id,
2010
- bbox: new BoxHelper(group, 0xff00ff),
2011
- needsUpdate: false,
2012
- };
2013
- this.rendered.scene.add(this.lastBbox.bbox);
2014
- } else {
2015
- this.lastBbox = null;
2016
- }
2017
-
2018
- this.update(false, false);
2019
- }
2020
- };
2021
-
2022
- /**
2023
- * Refresh clipping plane
2024
- * @param index - index of the plane: 0,1,2
2025
- * @param value - distance on the clipping normal from the center
2026
- */
2027
- refreshPlane = (index: ClipIndex, value: number): void => {
2028
- if (!this.ready) return;
2029
- const sliderKeys = ["clipSlider0", "clipSlider1", "clipSlider2"] as const;
2030
- this.state.set(sliderKeys[index], value);
2031
- this.rendered.clipping.setConstant(index, value);
2032
- this.update(this.updateMarker);
2033
- };
2034
-
2035
- /**
2036
- * Backup animation (for switch to explode animation)
2037
- */
2038
- backupAnimation(): void {
2039
- if (this.animation.hasTracks()) {
2040
- this.animation.backup();
2041
- }
2042
- }
2043
-
2044
- /**
2045
- * Restore animation (for switch back from explode animation)
2046
- */
2047
- restoreAnimation(): void {
2048
- if (this.animation.hasBackup()) {
2049
- const params = this.animation.restore();
2050
- this.initAnimation(params.duration!, params.speed!, "A", params.repeat!);
2051
- }
2052
- }
2053
-
2054
- /**
2055
- * Handler for the animation control
2056
- * @param btn - the pressed button as string: "play", "pause", "stop"
2057
- */
2058
- controlAnimation = (btn: string): void => {
2059
- if (!this.clipAction) return;
2060
- switch (btn) {
2061
- case "play":
2062
- if (this.clipAction.paused) {
2063
- this.clipAction.paused = false;
2064
- }
2065
- this.clipAction.play();
2066
- break;
2067
- case "pause":
2068
- this.clipAction.paused = !this.clipAction.paused;
2069
- break;
2070
- case "stop":
2071
- this.clipAction.stop();
2072
- break;
2073
- }
2074
- };
2075
-
2076
- /**
2077
- * Set state of one entry of a treeview leaf given by an id
2078
- * @param id - object id
2079
- * @param state - 2 dim array [mesh, edges] = [0/1, 0/1]
2080
- * @param _nodeType - node type (unused)
2081
- * @param notify - whether to send notification or not.
2082
- */
2083
- setState = (
2084
- id: string,
2085
- state: VisibilityState,
2086
- _nodeType: string = "leaf",
2087
- notify: boolean = true,
2088
- ): void => {
2089
- this.rendered.treeview.setState(id, state);
2090
- this.update(this.updateMarker, notify);
2091
- };
2092
-
2093
- removeLastBbox(): void {
2094
- if (this.lastBbox != null) {
2095
- this.rendered.scene.remove(this.lastBbox.bbox);
2096
- this.lastBbox.bbox.dispose();
2097
- this.lastBbox = null;
2098
- }
2099
- }
2100
-
2101
- /**
2102
- * Handle bounding box and notifications for picked elements
2103
- * @param path - path of object
2104
- * @param name - name of object (id = path/name)
2105
- * @param meta - meta key pressed
2106
- * @param shift - shift key pressed
2107
- * @param alt - alt key pressed
2108
- * @param point - picked point
2109
- * @param nodeType - node type
2110
- * @param tree - whether from tree
2111
- */
2112
- handlePick = (
2113
- path: string,
2114
- name: string,
2115
- meta: boolean,
2116
- shift: boolean,
2117
- alt: boolean,
2118
- point: THREE.Vector3 | null,
2119
- nodeType: string | null = "leaf",
2120
- tree: boolean = false,
2121
- ): void => {
2122
- const id = `${path}/${name}`;
2123
- const object = this.rendered.nestedGroup.groups[id];
2124
- if (object == null) {
2125
- return;
2126
- }
2127
- let boundingBox: BoundingBox;
2128
- if (object.parent != null) {
2129
- boundingBox = new BoundingBox().setFromObject(object, true);
2130
- } else {
2131
- // ignore PlaneMesh group
2132
- boundingBox = new BoundingBox();
2133
- for (let i = 0; i < object.children.length - 1; i++) {
2134
- boundingBox = boundingBox.expandByObject(object.children[i]);
2135
- }
2136
- }
2137
-
2138
- if (this.lastBbox != null && this.lastBbox.id === id && !meta && !shift) {
2139
- this.removeLastBbox();
2140
- this.rendered.treeview.toggleLabelColor(null, id);
2141
- } else {
2142
- this.checkChanges({
2143
- lastPick: {
2144
- path: path,
2145
- name: name,
2146
- boundingBox: boundingBox,
2147
- boundingSphere: boundingBox.boundingSphere(),
2148
- },
2149
- });
2150
-
2151
- if (this.animation.clipAction?.isRunning()) {
2152
- this.bboxNeedsUpdate = true;
2153
- }
2154
-
2155
- if (shift && meta) {
2156
- this.removeLastBbox();
2157
- if (tree) {
2158
- this.rendered.treeview.hideAll();
2159
- const showEdges = this._studioManager.isActive ? 0 : 1;
2160
- this.setState(id, [1, showEdges], nodeType ?? "leaf");
2161
- } else {
2162
- const center = boundingBox.center();
2163
- this.setCameraTarget(point ?? new THREE.Vector3(...center));
2164
- this.display.showCenterInfo(center);
2165
- }
2166
- } else if (shift) {
2167
- this.removeLastBbox();
2168
- this.rendered.treeview.hideAll();
2169
- const showEdges = this._studioManager.isActive ? 0 : 1;
2170
- this.setState(id, [1, showEdges], nodeType ?? "leaf");
2171
- const center = boundingBox.center();
2172
- this.setCameraTarget(new THREE.Vector3(...center));
2173
- this.display.showCenterInfo(center);
2174
- } else if (meta) {
2175
- this.setState(id, [0, 0], nodeType ?? "leaf");
2176
- } else if (alt) {
2177
- // same as else branch to make typscript happy
2178
- this.display.showBoundingBoxInfo(path, name, boundingBox);
2179
- this.setBoundingBox(id);
2180
- this.rendered.treeview.openPath(id);
2181
- } else {
2182
- this.display.showBoundingBoxInfo(path, name, boundingBox);
2183
- this.setBoundingBox(id);
2184
- this.rendered.treeview.openPath(id);
2185
- }
2186
- }
2187
- if (this._studioManager.isActive) {
2188
- this.display.onSelectionChanged(this.lastBbox?.id ?? null);
2189
- }
2190
- this.update(true);
2191
- };
2192
-
2193
- // ---------------------------------------------------------------------------
2194
- // Object Picking & Selection
2195
- // ---------------------------------------------------------------------------
2196
-
2197
- setPickHandler(flag: boolean): void {
2198
- if (flag) {
2199
- this.renderer.domElement.addEventListener("dblclick", this.pick, false);
2200
- } else {
2201
- this.renderer.domElement.removeEventListener(
2202
- "dblclick",
2203
- this.pick,
2204
- false,
2205
- );
2206
- }
2207
- }
2208
-
2209
- /**
2210
- * Find the shape that was double clicked and send notification
2211
- * @param e - a DOM PointerEvent or MouseEvent
2212
- */
2213
- pick = (e: PointerEvent | MouseEvent): void => {
2214
- const raycaster = new Raycaster(
2215
- this.rendered.camera,
2216
- this.renderer.domElement,
2217
- this.state.get("cadWidth"),
2218
- this.state.get("height"),
2219
- this.bb_max / 30,
2220
- this.rendered.scene.children[0],
2221
- () => { },
2222
- );
2223
- raycaster.init();
2224
- raycaster.onPointerMove(e);
2225
-
2226
- const validObjs = raycaster.getIntersectedObjs();
2227
- if (validObjs.length === 0) {
2228
- return;
2229
- }
2230
-
2231
- // Find first mesh intersection
2232
- let nearestMesh: THREE.Mesh | null = null;
2233
- let nearestIntersection: THREE.Intersection | null = null;
2234
- for (const obj of validObjs) {
2235
- if (obj.object instanceof THREE.Mesh) {
2236
- nearestMesh = obj.object;
2237
- nearestIntersection = obj;
2238
- break;
2239
- }
2240
- }
2241
- if (nearestMesh == null || nearestIntersection == null) {
2242
- return;
2243
- }
2244
-
2245
- const point = nearestIntersection.point;
2246
- const shapesFormat = this.shapes?.format;
2247
- const grandparent = nearestMesh.parent?.parent;
2248
- const nearest = {
2249
- path: grandparent ? grandparent.name.replaceAll("|", "/") : "",
2250
- name: nearestMesh.name,
2251
- boundingBox:
2252
- shapesFormat === "GDS"
2253
- ? new THREE.Box3(
2254
- point.clone().subScalar(10),
2255
- point.clone().addScalar(10),
2256
- )
2257
- : nearestMesh.geometry.boundingBox,
2258
- boundingSphere:
2259
- shapesFormat === "GDS"
2260
- ? new THREE.Sphere(point, 1)
2261
- : nearestMesh.geometry.boundingSphere,
2262
- objectGroup: nearestMesh.parent,
2263
- };
2264
- this.handlePick(
2265
- nearest.path,
2266
- nearest.name,
2267
- KeyMapper.get(e, "meta"),
2268
- KeyMapper.get(e, "shift"),
2269
- KeyMapper.get(e, "alt"),
2270
- nearestIntersection.point,
2271
- null,
2272
- false,
2273
- );
2274
- raycaster.dispose();
2275
- };
2276
-
2277
- // ---------------------------------------------------------------------------
2278
- // CAD Tools & Raycasting
2279
- // ---------------------------------------------------------------------------
2280
-
2281
- clearSelection = (): void => {
2282
- this.rendered.nestedGroup.clearSelection();
2283
- this.cadTools.handleResetSelection();
2284
- };
2285
-
2286
- _releaseLastSelected = (): void => {
2287
- if (this.lastObject != null) {
2288
- const objs = this.lastObject.objs();
2289
- for (const obj of objs) {
2290
- obj.unhighlight(true);
2291
- }
2292
- }
2293
- };
2294
-
2295
- _removeLastSelected = (): void => {
2296
- if (this.lastSelection != null) {
2297
- const objs = this.lastSelection.objs();
2298
- for (const obj of objs) {
2299
- obj.unhighlight(false);
2300
- this.rendered.treeview.toggleLabelColor(
2301
- null,
2302
- obj.name.replaceAll(this.rendered.nestedGroup.delim, "/"),
2303
- );
2304
- }
2305
- this.lastSelection = null;
2306
- this.lastObject = null;
2307
- }
2308
- this.cadTools.handleRemoveLastSelection(true);
2309
- };
2310
-
2311
- /**
2312
- * Set raycast mode
2313
- * @param flag - turn raycast mode on or off
2314
- */
2315
- setRaycastMode(flag: boolean): void {
2316
- if (flag) {
2317
- // initiate raycasting
2318
- this.raycaster = new Raycaster(
2319
- this.rendered.camera,
2320
- this.renderer.domElement,
2321
- this.state.get("cadWidth"),
2322
- this.state.get("height"),
2323
- this.bb_max / 30,
2324
- this.rendered.scene.children[0],
2325
- this.handleRaycastEvent,
2326
- );
2327
- this.raycaster.init();
2328
- } else {
2329
- if (this.raycaster) {
2330
- this.raycaster.dispose();
2331
- }
2332
- this.raycaster = null;
2333
- }
2334
- }
2335
-
2336
- handleRaycast = (): void => {
2337
- const objects = this.raycaster!.getValidIntersectedObjs();
2338
- if (objects.length > 0) {
2339
- // highlight hovered object(s)
2340
- for (const object of objects) {
2341
- {
2342
- const objectGroup = object.object.parent;
2343
- if (!isObjectGroup(objectGroup)) break;
2344
- const name = objectGroup.name;
2345
- const last_name = this.lastObject ? this.lastObject.obj.name : null;
2346
- if (name !== last_name) {
2347
- this._releaseLastSelected();
2348
- const fromSolid = this.raycaster!.filters.topoFilter.includes(
2349
- TopoFilter.solid,
2350
- );
2351
-
2352
- // one object for a selected vertex, edge and face and multiple faces for a solid
2353
- const pickedObj = new PickedObject(objectGroup, fromSolid);
2354
- for (const obj of pickedObj.objs()) {
2355
- obj.highlight(true);
2356
- }
2357
- // this object will be handled in handleRaycastEvent after a mouse event
2358
- this.lastObject = pickedObj;
2359
- }
2360
- break;
2361
- }
2362
- }
2363
- } else {
2364
- // unhighlight hovered object(s)
2365
- if (this.lastObject != null) {
2366
- this._releaseLastSelected();
2367
- this.lastObject = null;
2368
- }
2369
- }
2370
- };
2371
-
2372
- handleRaycastEvent = (event: RaycastEvent): void => {
2373
- if (event.key) {
2374
- switch (event.key) {
2375
- case "Escape":
2376
- this.clearSelection();
2377
- break;
2378
- case "Backspace":
2379
- this._removeLastSelected();
2380
- break;
2381
- default:
2382
- break;
2383
- }
2384
- } else {
2385
- switch (event.mouse) {
2386
- case "left":
2387
- if (this.lastObject != null) {
2388
- const objs = this.lastObject.objs();
2389
- // one object for a selected vertex, edge and face and multiple faces for a solid
2390
- for (const obj of objs) {
2391
- obj.toggleSelection();
2392
- }
2393
- this.cadTools.handleSelectedObj(
2394
- this.lastObject,
2395
- this.lastSelection?.obj.name !== this.lastObject.obj.name,
2396
- event.shift ?? false,
2397
- );
2398
- this.lastSelection = this.lastObject;
2399
- }
2400
- break;
2401
- case "right":
2402
- this._removeLastSelected();
2403
- break;
2404
- default:
2405
- break;
2406
- }
2407
- }
2408
- };
2409
-
2410
- /**
2411
- * Handle a backend response sent by the backend
2412
- * The response is a JSON object sent by the Python backend through VSCode
2413
- * @param response
2414
- */
2415
- handleBackendResponse = (response: BackendResponse): void => {
2416
- if (isToolResponse(response)) {
2417
- this.cadTools.handleResponse(response);
2418
- }
2419
- };
2420
-
2421
- // ---------------------------------------------------------------------------
2422
- // Appearance (Axes, Grid, Visual Settings)
2423
- // ---------------------------------------------------------------------------
2424
-
2425
- /**
2426
- * Get whether axes helpers are visible.
2427
- * @returns true if axes are shown
2428
- * @public
2429
- */
2430
- getAxes(): boolean {
2431
- return this.state.get("axes");
2432
- }
2433
-
2434
- /**
2435
- * Show or hide the axes helper (X/Y/Z indicators).
2436
- * @param flag - true to show axes, false to hide
2437
- * @param notify - whether to send notification to callback
2438
- * @public
2439
- */
2440
- setAxes = (flag: boolean, notify: boolean = true): void => {
2441
- if (!this.ready) return;
2442
- this.state.set("axes", flag, notify);
2443
- this.rendered.axesHelper.setVisible(flag);
2444
- this.update(this.updateMarker);
2445
- };
2446
-
2447
- /**
2448
- * Show/hide grids
2449
- * @param action - one of "grid" (all grids), "grid-xy","grid-xz", "grid-yz"
2450
- * @param flag - visibility flag
2451
- * @param notify - whether to send notification or not.
2452
- */
2453
- setGrid = (action: string, flag: boolean, notify: boolean = true): void => {
2454
- this.rendered.gridHelper.setGrid(action, flag);
2455
- // Copy array to avoid reference comparison issues in state.set
2456
- const [a, b, c] = this.rendered.gridHelper.grid;
2457
- this.state.set("grid", [a, b, c], notify);
2458
- this.update(this.updateMarker);
2459
- };
2460
-
2461
- /**
2462
- * Get visibility of grids.
2463
- * @returns grids value.
2464
- */
2465
- getGrids(): [boolean, boolean, boolean] {
2466
- return this.state.get("grid");
2467
- }
2468
-
2469
- /**
2470
- * Toggle grid visibility
2471
- * @param grids - 3 dim grid visibility (xy, xz, yz)
2472
- * @param notify - whether to send notification or not.
2473
- */
2474
- setGrids = (
2475
- grids: [boolean, boolean, boolean],
2476
- notify: boolean = true,
2477
- ): void => {
2478
- this.rendered.gridHelper.setGrids(...grids);
2479
- // Copy array to avoid reference comparison issues in state.set
2480
- const [a, b, c] = this.rendered.gridHelper.grid;
2481
- this.state.set("grid", [a, b, c], notify);
2482
- this.update(this.updateMarker);
2483
- };
2484
-
2485
- /**
2486
- * Set grid center
2487
- * @param center - true for centering grid at (0,0,0)
2488
- * @param notify - whether to send notification or not.
2489
- */
2490
- setGridCenter = (center: boolean, notify: boolean = true): void => {
2491
- this.state.set("centerGrid", center, notify);
2492
- this.rendered.gridHelper.centerGrid = center;
2493
- this.rendered.gridHelper.setCenter(
2494
- this.state.get("axes0"),
2495
- this.state.get("up") === "Z",
2496
- );
2497
- this.update(this.updateMarker);
2498
- };
2499
-
2500
- /**
2501
- * Get location of axes.
2502
- * @returns axes0 value, true means at origin (0,0,0)
2503
- */
2504
- getAxes0(): boolean {
2505
- return this.state.get("axes0");
2506
- }
2507
-
2508
- /**
2509
- * Set whether grids and axes center at the origin or the object's boundary box center
2510
- * @param flag - whether grids and axes center at the origin (0,0,0)
2511
- * @param notify - whether to send notification or not.
2512
- */
2513
- setAxes0 = (flag: boolean, notify: boolean = true): void => {
2514
- if (!this.ready) return;
2515
- this.state.set("axes0", flag, notify);
2516
- this.rendered.gridHelper.setCenter(flag, this.state.get("up") === "Z");
2517
- this.rendered.axesHelper.setCenter(flag);
2518
- this.update(this.updateMarker);
2519
- };
2520
-
2521
- /**
2522
- * Get transparency state of CAD objects.
2523
- * @returns transparent value.
2524
- */
2525
- getTransparent(): boolean {
2526
- return this.state.get("transparent");
2527
- }
2528
-
2529
- /**
2530
- * Set CAD objects transparency.
2531
- * @param flag - whether to show the CAD object in transparent mode
2532
- * @param notify - whether to send notification or not.
2533
- * @public
2534
- */
2535
- setTransparent = (flag: boolean, notify: boolean = true): void => {
2536
- this.state.set("transparent", flag, notify);
2537
- this.rendered.nestedGroup.setTransparent(flag);
2538
- this.update(this.updateMarker);
2539
- };
2540
-
2541
- /**
2542
- * Get blackEdges value.
2543
- * @returns blackEdges value.
2544
- */
2545
- getBlackEdges(): boolean {
2546
- return this.state.get("blackEdges");
2547
- }
2548
-
2549
- /**
2550
- * Show edges in black or the default edge color.
2551
- * @param flag - whether to show edges in black
2552
- * @param notify - whether to send notification or not.
2553
- * @public
2554
- */
2555
- setBlackEdges = (flag: boolean, notify: boolean = true): void => {
2556
- this.state.set("blackEdges", flag, notify);
2557
- this.rendered.nestedGroup.setBlackEdges(flag);
2558
- this.update(this.updateMarker);
2559
- };
2560
-
2561
- /**
2562
- * Show or hide the CAD tools panel
2563
- * @param flag - whether to show tools
2564
- * @param notify - whether to send notification or not.
2565
- */
2566
- setTools = (flag: boolean, notify: boolean = true): void => {
2567
- this.state.set("tools", flag, notify);
2568
- };
2569
-
2570
- /**
2571
- * Enable or disable glass mode (overlay navigation)
2572
- * @param flag - whether to enable glass mode
2573
- * @param notify - whether to send notification or not.
2574
- */
2575
- setGlass = (flag: boolean, notify: boolean = true): void => {
2576
- this.state.set("glass", flag, notify);
2577
- };
2578
-
2579
- /**
2580
- * Get default color of the edges.
2581
- * @returns edgeColor value.
2582
- */
2583
- getEdgeColor(): number {
2584
- return this.state.get("edgeColor");
2585
- }
2586
-
2587
- /**
2588
- * Set the default edge color
2589
- * @param color - edge color (0xrrggbb)
2590
- * @param notify - whether to send notification or not.
2591
- */
2592
- setEdgeColor = (color: number, notify: boolean = true): void => {
2593
- this.state.set("edgeColor", color, notify);
2594
- this.rendered.nestedGroup.setEdgeColor(color);
2595
- this.update(this.updateMarker);
2596
- };
2597
-
2598
- /**
2599
- * Get default opacity.
2600
- * @returns opacity value.
2601
- */
2602
- getOpacity(): number {
2603
- return this.state.get("defaultOpacity");
2604
- }
2605
-
2606
- /**
2607
- * Set the default opacity
2608
- * @param opacity - opacity (between 0.0 and 1.0)
2609
- * @param notify - whether to send notification or not.
2610
- */
2611
- setOpacity = (opacity: number, notify: boolean = true): void => {
2612
- this.state.set("defaultOpacity", opacity, notify);
2613
- this.rendered.nestedGroup.setOpacity(opacity);
2614
- this.update(this.updateMarker);
2615
- };
2616
-
2617
- /**
2618
- * Get whether tools are shown/hidden.
2619
- * @returns tools value.
2620
- */
2621
- getTools(): boolean {
2622
- return this.state.get("tools");
2623
- }
2624
-
2625
- /**
2626
- * Show/hide the CAD tools
2627
- * @param flag - visibility flag
2628
- * @param notify - whether to send notification or not.
2629
- */
2630
- showTools = (flag: boolean, notify: boolean = true): void => {
2631
- this.state.set("tools", flag, notify);
2632
- this.update(this.updateMarker);
2633
- };
2634
-
2635
- // ---------------------------------------------------------------------------
2636
- // Getters & Setters: Lighting & Materials
2637
- // ---------------------------------------------------------------------------
2638
-
2639
- /**
2640
- * Get intensity of ambient light.
2641
- * @returns ambientLight value.
2642
- */
2643
- getAmbientLight(): number {
2644
- return this.state.get("ambientIntensity");
2645
- }
2646
-
2647
- /**
2648
- * Set the intensity of ambient light.
2649
- * @param val - the new ambient light intensity (0-4)
2650
- * @param notify - whether to send notification or not.
2651
- * @public
2652
- */
2653
- setAmbientLight = (val: number, notify: boolean = true): void => {
2654
- if (!this.ready) return;
2655
- val = Math.max(0, Math.min(4, val));
2656
- this.state.set("ambientIntensity", val, notify);
2657
- this.rendered.ambientLight.intensity = scaleLight(val);
2658
- this.update(this.updateMarker);
2659
- };
2660
-
2661
- /**
2662
- * Get intensity of direct light.
2663
- * @returns directLight value.
2664
- */
2665
- getDirectLight(): number {
2666
- return this.state.get("directIntensity");
2667
- }
2668
-
2669
- /**
2670
- * Set the intensity of directional light.
2671
- * @param val - the new direct light intensity (0-4)
2672
- * @param notify - whether to send notification or not.
2673
- * @public
2674
- */
2675
- setDirectLight = (val: number, notify: boolean = true): void => {
2676
- if (!this.ready) return;
2677
- val = Math.max(0, Math.min(4, val));
2678
- this.state.set("directIntensity", val, notify);
2679
- this.rendered.directLight.intensity = scaleLight(val);
2680
- this.update(this.updateMarker);
2681
- };
2682
-
2683
- /**
2684
- * Retrieves the metalness value.
2685
- *
2686
- * @returns The current metalness value.
2687
- */
2688
- getMetalness = (): number => {
2689
- return this.state.get("metalness");
2690
- };
2691
-
2692
- /**
2693
- * Sets the metalness value for the viewer and updates related properties.
2694
- *
2695
- * @param value - The metalness value to set (0-1).
2696
- * @param notify - Whether to notify about the changes.
2697
- * @public
2698
- */
2699
- setMetalness = (value: number, notify: boolean = true): void => {
2700
- value = Math.max(0, Math.min(1, value));
2701
- this.state.set("metalness", value, notify);
2702
- this.rendered.nestedGroup.setMetalness(value);
2703
- this.update(this.updateMarker);
2704
- };
2705
-
2706
- /**
2707
- * Retrieves the roughness value.
2708
- *
2709
- * @returns The current roughness value.
2710
- */
2711
- getRoughness = (): number => {
2712
- return this.state.get("roughness");
2713
- };
2714
-
2715
- /**
2716
- * Sets the roughness value for the viewer and updates related components.
2717
- *
2718
- * @param value - The roughness value to set (0-1).
2719
- * @param notify - Whether to notify about the changes.
2720
- * @public
2721
- */
2722
- setRoughness = (value: number, notify: boolean = true): void => {
2723
- value = Math.max(0, Math.min(1, value));
2724
- this.state.set("roughness", value, notify);
2725
- this.rendered.nestedGroup.setRoughness(value);
2726
- this.update(this.updateMarker);
2727
- };
2728
-
2729
- /**
2730
- * Resets the material settings of the viewer to their default values.
2731
- * Updates the metalness, roughness, ambient light intensity, and direct light intensity
2732
- * based on the current material settings.
2733
- */
2734
- resetMaterial = (): void => {
2735
- if (!this.materialSettings) return;
2736
- this.setMetalness(this.materialSettings.metalness, true);
2737
- this.setRoughness(this.materialSettings.roughness, true);
2738
- this.setAmbientLight(this.materialSettings.ambientIntensity, true);
2739
- this.setDirectLight(this.materialSettings.directIntensity, true);
2740
- };
2741
-
2742
- // ---------------------------------------------------------------------------
2743
- // Getters & Setters: Zebra Tool
2744
- // ---------------------------------------------------------------------------
2745
-
2746
- enableZebraTool = (flag: boolean): void => {
2747
- this.rendered.nestedGroup.setZebra(flag);
2748
- this.update(true, true);
2749
- this.rendered.treeview.update();
2750
- };
2751
-
2752
- /**
2753
- * Sets the stripe count value for the viewer and updates related components.
2754
- * @param value - The stripe count value to set.
2755
- */
2756
- setZebraCount = (value: number): void => {
2757
- value = Math.max(2, Math.min(50, value));
2758
- this.state.set("zebraCount", value);
2759
- this.rendered.nestedGroup.setZebraCount(value);
2760
- this.update(this.updateMarker);
2761
- };
2762
-
2763
- /**
2764
- * Sets the stripe opacity value for the viewer and updates related components.
2765
- * @param value - The stripe opacity value to set.
2766
- */
2767
- setZebraOpacity = (value: number): void => {
2768
- value = Math.max(0, Math.min(1, value));
2769
- this.state.set("zebraOpacity", value);
2770
- this.rendered.nestedGroup.setZebraOpacity(value);
2771
- this.update(this.updateMarker);
2772
- };
2773
-
2774
- /**
2775
- * Sets the stripe direction value for the viewer and updates related components.
2776
- * @param value - The stripe direction value to set.
2777
- */
2778
- setZebraDirection = (value: number): void => {
2779
- value = Math.max(0, Math.min(90, value));
2780
- this.state.set("zebraDirection", value);
2781
- this.rendered.nestedGroup.setZebraDirection(value);
2782
- this.update(this.updateMarker);
2783
- };
2784
-
2785
- /**
2786
- * Sets the stripe color scheme for the viewer and updates related components.
2787
- * @param value - The color scheme ("blackwhite", "colorful", "grayscale").
2788
- */
2789
- setZebraColorScheme = (value: ZebraColorScheme): void => {
2790
- this.state.set("zebraColorScheme", value);
2791
- this.rendered.nestedGroup.setZebraColorScheme(value);
2792
- this.update(this.updateMarker);
2793
- };
2794
-
2795
- /**
2796
- * Sets the stripe mapping mode for the viewer and updates related components.
2797
- * @param value - The mapping mode ("reflection", "normal").
2798
- */
2799
- setZebraMappingMode = (value: ZebraMappingMode): void => {
2800
- this.state.set("zebraMappingMode", value);
2801
- this.rendered.nestedGroup.setZebraMappingMode(value);
2802
- this.update(this.updateMarker);
2803
- };
2804
-
2805
- /**
2806
- * Resets zebra tool settings to defaults: count=9, opacity=1, direction=0,
2807
- * colorScheme=blackwhite, mappingMode=reflection.
2808
- */
2809
- resetZebra = (): void => {
2810
- this.setZebraCount(9);
2811
- this.setZebraOpacity(1.0);
2812
- this.setZebraDirection(0);
2813
- this.setZebraColorScheme("blackwhite");
2814
- this.setZebraMappingMode("reflection");
2815
- };
2816
-
2817
- /**
2818
- * Gets the current stripe count value.
2819
- * @returns The stripe count (2-50).
2820
- */
2821
- getZebraCount = (): number => {
2822
- return this.state.get("zebraCount");
2823
- };
2824
-
2825
- /**
2826
- * Gets the current stripe opacity value.
2827
- * @returns The stripe opacity (0-1).
2828
- */
2829
- getZebraOpacity = (): number => {
2830
- return this.state.get("zebraOpacity");
2831
- };
2832
-
2833
- /**
2834
- * Gets the current stripe direction value.
2835
- * @returns The stripe direction in degrees (0-90).
2836
- */
2837
- getZebraDirection = (): number => {
2838
- return this.state.get("zebraDirection");
2839
- };
2840
-
2841
- /**
2842
- * Gets the current stripe color scheme.
2843
- * @returns The color scheme ("blackwhite", "colorful", "grayscale").
2844
- */
2845
- getZebraColorScheme = (): ZebraColorScheme => {
2846
- return this.state.get("zebraColorScheme");
2847
- };
2848
-
2849
- /**
2850
- * Gets the current stripe mapping mode.
2851
- * @returns The mapping mode ("reflection", "normal").
2852
- */
2853
- getZebraMappingMode = (): ZebraMappingMode => {
2854
- return this.state.get("zebraMappingMode");
2855
- };
2856
-
2857
- // ---------------------------------------------------------------------------
2858
- // Getters & Setters: Studio Mode
2859
- // ---------------------------------------------------------------------------
2860
-
2861
- /**
2862
- * Sets the studio environment preset.
2863
- * @param value - The environment name ("studio", "neutral", "outdoor", "none", or custom HDR URL).
2864
- * @param notify - Whether to notify about the changes.
2865
- * @public
2866
- */
2867
- setStudioEnvironment = (value: string, notify: boolean = true): void => {
2868
- this.state.set("studioEnvironment", value, notify);
2869
- };
2870
-
2871
- /**
2872
- * Sets the studio environment intensity.
2873
- * @param value - The environment intensity (0-3).
2874
- * @param notify - Whether to notify about the changes.
2875
- * @public
2876
- */
2877
- setStudioEnvIntensity = (value: number, notify: boolean = true): void => {
2878
- value = Math.max(0, Math.min(3, value));
2879
- this.state.set("studioEnvIntensity", value, notify);
2880
- };
2881
-
2882
- /**
2883
- * Sets the background mode for Studio mode.
2884
- * @param value - The background mode ("grey", "white", "gradient", "environment", or "transparent").
2885
- * @param notify - Whether to notify about the changes.
2886
- * @public
2887
- */
2888
- setStudioBackground = (value: StudioBackground, notify: boolean = true): void => {
2889
- this.state.set("studioBackground", value, notify);
2890
- };
2891
-
2892
- /**
2893
- * Sets the tone mapping mode for Studio mode.
2894
- * @param value - The tone mapping mode ("neutral", "ACES", or "none").
2895
- * @param notify - Whether to notify about the changes.
2896
- * @public
2897
- */
2898
- setStudioToneMapping = (value: StudioToneMapping, notify: boolean = true): void => {
2899
- this.state.set("studioToneMapping", value, notify);
2900
- };
2901
-
2902
- /**
2903
- * Sets the exposure value for Studio mode.
2904
- * @param value - The exposure value (0-2).
2905
- * @param notify - Whether to notify about the changes.
2906
- * @public
2907
- */
2908
- setStudioExposure = (value: number, notify: boolean = true): void => {
2909
- value = Math.max(0, Math.min(2, value));
2910
- this.state.set("studioExposure", value, notify);
2911
- };
2912
-
2913
- /**
2914
- * Sets whether 4K environment maps are used (default: 2K).
2915
- * @param value - True for 4K, false for 2K.
2916
- * @param notify - Whether to notify about the changes.
2917
- * @public
2918
- */
2919
- setStudio4kEnvMaps = (value: boolean, notify: boolean = true): void => {
2920
- this.state.set("studio4kEnvMaps", value, notify);
2921
- };
2922
-
2923
- /**
2924
- * Gets whether 4K environment maps are enabled.
2925
- * @returns True for 4K, false for 2K.
2926
- * @public
2927
- */
2928
- getStudio4kEnvMaps = (): boolean => {
2929
- return this.state.get("studio4kEnvMaps");
2930
- };
2931
-
2932
- /**
2933
- * Sets the environment rotation for Studio mode.
2934
- * @param value - The rotation in degrees (0-360).
2935
- * @param notify - Whether to notify about the changes.
2936
- * @public
2937
- */
2938
- setStudioEnvRotation = (value: number, notify: boolean = true): void => {
2939
- this.state.set("studioEnvRotation", value, notify);
2940
- };
2941
-
2942
- /**
2943
- * Gets the current environment rotation for Studio mode.
2944
- * @returns The rotation in degrees (0-360).
2945
- * @public
2946
- */
2947
- getStudioEnvRotation = (): number => {
2948
- return this.state.get("studioEnvRotation");
2949
- };
2950
-
2951
- /**
2952
- * Sets the texture mapping mode for Studio mode.
2953
- * @param value - The texture mapping mode ("triplanar" or "parametric").
2954
- * @param notify - Whether to notify about the changes.
2955
- * @public
2956
- */
2957
- setStudioTextureMapping = (value: StudioTextureMapping, notify: boolean = true): void => {
2958
- this.state.set("studioTextureMapping", value, notify);
2959
- };
2960
-
2961
- /**
2962
- * Gets the current texture mapping mode for Studio mode.
2963
- * @returns The texture mapping mode ("triplanar" or "parametric").
2964
- * @public
2965
- */
2966
- getStudioTextureMapping = (): StudioTextureMapping => {
2967
- return this.state.get("studioTextureMapping");
2968
- };
2969
-
2970
- /**
2971
- * Gets the current studio environment preset.
2972
- * @returns The environment name ("studio", "neutral", "outdoor", "none", or custom HDR URL).
2973
- * @public
2974
- */
2975
- getStudioEnvironment = (): string => {
2976
- return this.state.get("studioEnvironment");
2977
- };
2978
-
2979
- /**
2980
- * Gets the current studio environment intensity.
2981
- * @returns The environment intensity (0-3).
2982
- * @public
2983
- */
2984
- getStudioEnvIntensity = (): number => {
2985
- return this.state.get("studioEnvIntensity");
2986
- };
2987
-
2988
- /**
2989
- * Gets the current background mode for Studio mode.
2990
- * @returns The background mode ("grey", "white", "gradient", "environment", or "transparent").
2991
- * @public
2992
- */
2993
- getStudioBackground = (): StudioBackground => {
2994
- return this.state.get("studioBackground");
2995
- };
2996
-
2997
- /**
2998
- * Gets the current tone mapping mode for Studio mode.
2999
- * @returns The tone mapping mode ("neutral", "ACES", or "none").
3000
- * @public
3001
- */
3002
- getStudioToneMapping = (): StudioToneMapping => {
3003
- return this.state.get("studioToneMapping");
3004
- };
3005
-
3006
- /**
3007
- * Gets the current exposure value for Studio mode.
3008
- * @returns The exposure value (0-3).
3009
- * @public
3010
- */
3011
- getStudioExposure = (): number => {
3012
- return this.state.get("studioExposure");
3013
- };
3014
-
3015
- /**
3016
- * Sets the shadow intensity in Studio mode.
3017
- * A value of 0 disables shadows; values > 0 enable them at that darkness.
3018
- * @param value - The shadow intensity (0-1).
3019
- * @param notify - Whether to notify about the changes.
3020
- * @public
3021
- */
3022
- setStudioShadowIntensity = (value: number, notify: boolean = true): void => {
3023
- value = Math.max(0, Math.min(1, value));
3024
- this.state.set("studioShadowIntensity", value, notify);
3025
- };
3026
-
3027
- /**
3028
- * Gets the current shadow intensity in Studio mode.
3029
- * @returns The shadow intensity (0-1). 0 means shadows are off.
3030
- * @public
3031
- */
3032
- getStudioShadowIntensity = (): number => {
3033
- return this.state.get("studioShadowIntensity");
3034
- };
3035
-
3036
- /**
3037
- * Sets the shadow softness in Studio mode.
3038
- * Controls PCSS penumbra width (virtual light source size).
3039
- * @param value - The shadow softness (0-1).
3040
- * @param notify - Whether to notify about the changes.
3041
- * @public
3042
- */
3043
- setStudioShadowSoftness = (value: number, notify: boolean = true): void => {
3044
- value = Math.max(0, Math.min(1, value));
3045
- this.state.set("studioShadowSoftness", value, notify);
3046
- };
3047
-
3048
- /**
3049
- * Gets the current shadow softness in Studio mode.
3050
- * @returns The shadow softness (0-1).
3051
- * @public
3052
- */
3053
- getStudioShadowSoftness = (): number => {
3054
- return this.state.get("studioShadowSoftness");
3055
- };
3056
-
3057
- /**
3058
- * Sets the ambient occlusion intensity in Studio mode.
3059
- * A value of 0 disables AO; values > 0 enable it at that intensity.
3060
- * @param value - The AO intensity (0-3.0).
3061
- * @param notify - Whether to notify about the changes.
3062
- * @public
3063
- */
3064
- setStudioAOIntensity = (value: number, notify: boolean = true): void => {
3065
- this.state.set("studioAOIntensity", value, notify);
3066
- };
3067
-
3068
- /**
3069
- * Gets the current ambient occlusion intensity in Studio mode.
3070
- * @returns The AO intensity value (0.5-3.0).
3071
- * @public
3072
- */
3073
- getStudioAOIntensity = (): number => {
3074
- return this.state.get("studioAOIntensity");
3075
- };
3076
-
3077
- /**
3078
- * Returns whether Studio mode is currently active.
3079
- * @returns True if Studio mode is active and the viewer has rendered content.
3080
- * @public
3081
- */
3082
- get isStudioActive(): boolean {
3083
- return this._studioManager.isActive;
3084
- }
3085
-
3086
- /**
3087
- * Get the ObjectGroup and path for the currently selected object in Studio mode.
3088
- * Returns null if nothing is selected, Studio mode is inactive, or the
3089
- * selection is a CompoundGroup (assembly node) rather than a leaf object.
3090
- */
3091
- getSelectedObjectGroup(): { object: ObjectGroup; path: string } | null {
3092
- return this._studioManager.getSelectedObjectGroup();
3093
- }
3094
-
3095
- /** Enter Studio mode. Called by display.ts switchToTab(). @internal */
3096
- enterStudioMode = () => this._studioManager.enterStudioMode();
3097
-
3098
- /** Leave Studio mode. Called by display.ts switchToTab(). @internal */
3099
- leaveStudioMode = () => this._studioManager.leaveStudioMode();
3100
-
3101
- /** Reset Studio settings to defaults. @public */
3102
- resetStudio = () => this._studioManager.resetStudio();
3103
-
3104
- // ---------------------------------------------------------------------------
3105
- // Camera State Getters & Setters
3106
- // ---------------------------------------------------------------------------
3107
-
3108
- /**
3109
- * Get ortho value as property (for ViewerLike interface compatibility).
3110
- */
3111
- get ortho(): boolean {
3112
- return this._rendered?.camera.ortho ?? true;
3113
- }
3114
-
3115
- /**
3116
- * Get camera property. Throws if not rendered.
3117
- */
3118
- get camera(): Camera {
3119
- return this.rendered.camera;
3120
- }
3121
-
3122
- /**
3123
- * Get nestedGroup property. Throws if not rendered.
3124
- */
3125
- get nestedGroup(): NestedGroup {
3126
- return this.rendered.nestedGroup;
3127
- }
3128
-
3129
- /**
3130
- * Get clipping property. Throws if not rendered.
3131
- */
3132
- get clipping(): Clipping {
3133
- return this.rendered.clipping;
3134
- }
3135
-
3136
- /**
3137
- * Get treeview property. Returns null if not rendered.
3138
- */
3139
- get treeview(): TreeView | null {
3140
- return this._rendered?.treeview ?? null;
3141
- }
3142
-
3143
- /**
3144
- * Get orientationMarker property. Throws if not rendered.
3145
- */
3146
- get orientationMarker(): OrientationMarker {
3147
- return this.rendered.orientationMarker;
3148
- }
3149
-
3150
- /**
3151
- * Get gridHelper property. Throws if not rendered.
3152
- */
3153
- get gridHelper(): Grid {
3154
- return this.rendered.gridHelper;
3155
- }
3156
-
3157
- /**
3158
- * Get axesHelper property. Throws if not rendered.
3159
- */
3160
- get axesHelper(): AxesHelper {
3161
- return this.rendered.axesHelper;
3162
- }
3163
-
3164
- /**
3165
- * Get scene property. Throws if not rendered.
3166
- */
3167
- get scene(): THREE.Scene {
3168
- return this.rendered.scene;
3169
- }
3170
-
3171
- /**
3172
- * Get controls property. Throws if not rendered.
3173
- */
3174
- get controls(): Controls {
3175
- return this.rendered.controls;
3176
- }
3177
-
3178
- /**
3179
- * Get ambientLight property. Throws if not rendered.
3180
- */
3181
- get ambientLight(): THREE.AmbientLight {
3182
- return this.rendered.ambientLight;
3183
- }
3184
-
3185
- /**
3186
- * Get directLight property. Throws if not rendered.
3187
- */
3188
- get directLight(): THREE.DirectionalLight {
3189
- return this.rendered.directLight;
3190
- }
3191
-
3192
- /**
3193
- * Get ortho value.
3194
- * @returns ortho value.
3195
- */
3196
- getOrtho(): boolean {
3197
- return this.rendered.camera.ortho;
3198
- }
3199
-
3200
- /**
3201
- * Set/unset camera's orthographic mode.
3202
- * @param flag - whether to set orthographic mode or not.
3203
- * @param notify - whether to send notification or not.
3204
- */
3205
- setOrtho(flag: boolean, notify: boolean = true): void {
3206
- this.switchCamera(flag, notify);
3207
- }
3208
-
3209
- /**
3210
- * Set zscaling value.
3211
- * @param value - scale factor.
3212
- */
3213
- setZscaleValue(value: number): void {
3214
- this.rendered.nestedGroup.setZScale(value);
3215
- this.zScale = value;
3216
- this.update(true);
3217
- }
3218
-
3219
- /**
3220
- * Get zoom value.
3221
- * @returns zoom value.
3222
- * @public
3223
- */
3224
- getCameraZoom(): number {
3225
- return this.rendered.camera.getZoom();
3226
- }
3227
-
3228
- /**
3229
- * Set zoom value.
3230
- * @param val - float zoom value.
3231
- * @param notify - whether to send notification or not.
3232
- * @public
3233
- */
3234
- setCameraZoom(val: number, notify: boolean = true): void {
3235
- this.rendered.camera.setZoom(val);
3236
- this.rendered.controls.update();
3237
- this.update(true, notify);
3238
- }
3239
-
3240
- /**
3241
- * Get the current camera position.
3242
- * @returns camera position as 3 dim array [x,y,z].
3243
- * @public
3244
- */
3245
- getCameraPosition(): number[] {
3246
- return this.rendered.camera.getPosition().toArray();
3247
- }
3248
-
3249
- /**
3250
- * Set camera position.
3251
- * @param position - camera position as 3 dim Array [x,y,z].
3252
- * @param relative - flag whether the position is a relative (e.g. [1,1,1] for iso) or absolute point.
3253
- * @param notify - whether to send notification or not.
3254
- * @public
3255
- */
3256
- setCameraPosition(
3257
- position: Vector3Tuple,
3258
- relative: boolean = false,
3259
- notify: boolean = true,
3260
- ): void {
3261
- this.rendered.camera.setPosition(position, relative);
3262
- this.rendered.controls.update();
3263
- this.update(true, notify);
3264
- }
3265
-
3266
- /**
3267
- * Get the current camera rotation as quaternion.
3268
- * @returns camera rotation as 4 dim quaternion array [x,y,z,w].
3269
- * @public
3270
- */
3271
- getCameraQuaternion(): QuaternionTuple {
3272
- return toQuaternionTuple(this.rendered.camera.getQuaternion().toArray());
3273
- }
3274
-
3275
- /**
3276
- * Set camera rotation via quaternion.
3277
- * @param quaternion - camera rotation as 4 dim quaternion array [x,y,z,w].
3278
- * @param notify - whether to send notification or not.
3279
- * @public
3280
- */
3281
- setCameraQuaternion(
3282
- quaternion: QuaternionTuple,
3283
- notify: boolean = true,
3284
- ): void {
3285
- this.rendered.camera.setQuaternion(quaternion);
3286
- this.rendered.controls.update();
3287
- this.update(true, notify);
3288
- }
3289
-
3290
- /**
3291
- * Get the current camera target.
3292
- * @returns camera target as 3 dim array array [x,y,z].
3293
- * @public
3294
- */
3295
- getCameraTarget(): Vector3Tuple {
3296
- return toVector3Tuple(this.rendered.controls.getTarget().toArray());
3297
- }
3298
-
3299
- /**
3300
- * Set camera target.
3301
- * @param target - camera target as THREE.Vector3 or [x, y, z] tuple.
3302
- * @param notify - whether to send notification or not.
3303
- * @public
3304
- */
3305
- setCameraTarget(target: THREE.Vector3 | Vector3Tuple, notify: boolean = true): void {
3306
- // Convert tuple to Vector3 if needed
3307
- const targetVec = Array.isArray(target)
3308
- ? new THREE.Vector3(...target)
3309
- : target;
3310
-
3311
- // Store current state
3312
- const camera = this.rendered.camera.getCamera();
3313
- const zoom = camera.zoom; // For orthographic cameras
3314
-
3315
- const offset = camera.position.clone().sub(this.rendered.controls.getTarget());
3316
-
3317
- // Update position and target
3318
- camera.position.copy(targetVec.clone().add(offset));
3319
- camera.updateWorldMatrix(true, false);
3320
- this.rendered.controls.getTarget().copy(targetVec);
3321
-
3322
- // Preserve zoom for orthographic cameras
3323
- if (isOrthographicCamera(camera)) {
3324
- camera.zoom = zoom;
3325
- camera.updateProjectionMatrix();
3326
- }
3327
-
3328
- // Update controls
3329
- this.rendered.controls.update();
3330
- this.update(true, notify);
3331
- }
3332
-
3333
- getCameraLocationSettings(): CameraLocationSettings {
3334
- return {
3335
- position: this.getCameraPosition(),
3336
- quaternion: this.getCameraQuaternion(),
3337
- target: this.getCameraTarget(),
3338
- zoom: this.getCameraZoom(),
3339
- };
3340
- }
3341
-
3342
- setCameraLocationSettings(
3343
- position: Vector3Tuple | null = null,
3344
- quaternion: QuaternionTuple | null = null,
3345
- target: Vector3Tuple | null = null,
3346
- zoom: number | null = null,
3347
- notify: boolean = true,
3348
- ): void {
3349
- if (position != null) {
3350
- this.rendered.camera.setPosition(position, false);
3351
- }
3352
- if (quaternion != null && this.state.get("control") === "trackball") {
3353
- this.rendered.camera.setQuaternion(quaternion);
3354
- }
3355
- if (target != null) {
3356
- this.rendered.controls.setTarget(new THREE.Vector3(...target));
3357
- }
3358
- if (zoom != null) {
3359
- this.rendered.camera.setZoom(zoom);
3360
- }
3361
- this.rendered.controls.update();
3362
- this.update(true, notify);
3363
- }
3364
-
3365
- // ---------------------------------------------------------------------------
3366
- // Tree State Management
3367
- // ---------------------------------------------------------------------------
3368
-
3369
- /**
3370
- * Get states of all treeview leaves.
3371
- * @returns object mapping paths to visibility states.
3372
- * @public
3373
- */
3374
- getStates(): Record<string, VisibilityState> {
3375
- if (!this._rendered) return {};
3376
- return this._rendered.treeview.getStates();
3377
- }
3378
-
3379
- /**
3380
- * Get state of a treeview leaf for a path.
3381
- * separator can be / or |
3382
- * @param path - path of the object
3383
- * @returns state value in the form of [mesh, edges] = [0/1, 0/1]
3384
- * @public
3385
- */
3386
- getState(path: string): VisibilityState | null {
3387
- if (!this._rendered) return null;
3388
- const p = path.replaceAll("|", "/");
3389
- return this._rendered.treeview.getState(p);
3390
- }
3391
-
3392
- /**
3393
- * Set states of treeview leaves.
3394
- * @param states - states object mapping paths to visibility states.
3395
- * @public
3396
- */
3397
- setStates = (states: Record<string, VisibilityState>): void => {
3398
- if (!this._rendered) return;
3399
- this._rendered.treeview.setStates(states);
3400
- };
3401
-
3402
- // ---------------------------------------------------------------------------
3403
- // Dynamic Part Management
3404
- // ---------------------------------------------------------------------------
3405
-
3406
- /**
3407
- * Build tree data from a Shapes object.
3408
- * Mirrors ShapeRenderer._getTree() logic.
3409
- */
3410
- private _buildTreeData(shapes: Shapes): ShapeTreeData {
3411
- const build = (parts: Shapes[]): ShapeTreeData => {
3412
- const result: ShapeTreeData = {};
3413
- for (const part of parts) {
3414
- if (part.parts != null) {
3415
- result[part.name] = build(part.parts);
3416
- } else {
3417
- result[part.name] = part.state as VisibilityState;
3418
- }
3419
- }
3420
- return result;
3421
- };
3422
- const tree: ShapeTreeData = {};
3423
- tree[shapes.name] = build(shapes.parts ?? []);
3424
- return tree;
3425
- }
3426
-
3427
- /**
3428
- * Find the parent Shapes node and the parent's parts array for a given path.
3429
- * @param path - Absolute path like "/root/group/part"
3430
- * @returns The parent Shapes node, or null if not found.
3431
- */
3432
- private _findShapesParent(path: string): Shapes | null {
3433
- if (!this.shapes) return null;
3434
- const parts = path.split("/").filter(Boolean);
3435
- // parts[0] is the root name, parent is everything except the last segment
3436
- if (parts.length < 2) return null;
3437
- const parentParts = parts.slice(0, -1);
3438
-
3439
- let current: Shapes = this.shapes;
3440
- // The first segment should match the root
3441
- if (current.name !== parentParts[0]) return null;
3442
-
3443
- for (let i = 1; i < parentParts.length; i++) {
3444
- if (!current.parts) return null;
3445
- const child = current.parts.find((p) => p.name === parentParts[i]);
3446
- if (!child) return null;
3447
- current = child;
3448
- }
3449
- return current;
3450
- }
3451
-
3452
- /**
3453
- * Rebuild the treeview from the current shapes data.
3454
- * Preserves visibility states across the rebuild.
3455
- */
3456
- private _rebuildTreeView(): void {
3457
- // Save visibility states before disposing the old tree
3458
- const savedStates = this.rendered.treeview.getStates();
3459
-
3460
- // Rebuild tree data from this.shapes
3461
- this.compactTree = this._buildTreeData(this.shapes!);
3462
- this.tree = this.compactTree;
3463
-
3464
- // Dispose old treeview and create new one
3465
- deepDispose(this.rendered.treeview);
3466
-
3467
- const treeview = new TreeView(
3468
- this.tree,
3469
- this.display.cadTreeScrollContainer,
3470
- this.setObject,
3471
- this.handlePick,
3472
- this.update,
3473
- this.notifyStates,
3474
- this.getNodeColor,
3475
- this.state.get("theme"),
3476
- this.state.get("newTreeBehavior"),
3477
- false,
3478
- );
3479
- this.rendered.treeview = treeview;
3480
-
3481
- this.display.clearCadTree();
3482
- const t = treeview.create();
3483
- this.display.addCadTree(t);
3484
- treeview.render();
3485
-
3486
- // Restore visibility states (updates both tree model and 3D objects)
3487
- this.rendered.treeview.setStates(savedStates);
3488
-
3489
- // Re-apply the current collapse state to the new tree
3490
- const collapse = this.state.get("collapse") as CollapseState;
3491
- if (collapse != null) {
3492
- this.collapseNodes(collapse, false);
3493
- }
3494
- }
3495
-
3496
- /**
3497
- * Apply current material/rendering settings to new objects in the group.
3498
- * @param paths - The paths of the newly added objects.
3499
- */
3500
- private _applyCurrentSettings(paths: string[]): void {
3501
- const nestedGroup = this.rendered.nestedGroup;
3502
- for (const path of paths) {
3503
- const obj = nestedGroup.groups[path];
3504
- if (obj instanceof ObjectGroup) {
3505
- obj.setTransparent(this.state.get("transparent"));
3506
- obj.setBlackEdges(this.state.get("blackEdges"));
3507
- obj.setMetalness(this.state.get("metalness"));
3508
- obj.setRoughness(this.state.get("roughness"));
3509
- obj.setPolygonOffset(2);
3510
- if (nestedGroup.clipPlanes) {
3511
- obj.setClipPlanes(nestedGroup.clipPlanes);
3512
- }
3513
- }
3514
- }
3515
- }
3516
-
3517
- /**
3518
- * Add a part (leaf or subtree) to the scene under an existing parent.
3519
- *
3520
- * For a **leaf**, pass a Shapes object with `shape` set and `name`
3521
- * as a plain name (no leading slash). The absolute path is built as
3522
- * `parentPath + "/" + partData.name`.
3523
- *
3524
- * For a **subtree**, pass a Shapes object with `parts` set and `id`
3525
- * as a slash-prefixed relative tree (e.g. `"/shelf"`). All `id`
3526
- * fields in the tree are prefixed with `parentPath` before rendering.
3527
- *
3528
- * When adding many parts in a batch, pass `{ skipBounds: true }` to
3529
- * defer the expensive bounds/clipping/treeview recomputation, then call
3530
- * `updateBounds()` once after the loop.
3531
- *
3532
- * @param parentPath - Absolute path of the parent group
3533
- * (e.g. "/assembly"). Must already exist as a CompoundGroup.
3534
- * @param partData - A Shapes object describing the part to add.
3535
- * @param options - Optional settings.
3536
- * @param options.skipBounds - When true, skip bounds/clipping/treeview
3537
- * update and re-render. Caller must call `updateBounds()` afterwards.
3538
- * @returns The absolute path of the added root element.
3539
- * @throws If the viewer is not rendered, the parent doesn't exist,
3540
- * or the name/id already exists at that level.
3541
- * @public
3542
- */
3543
- addPart(
3544
- parentPath: string,
3545
- partData: Shapes,
3546
- options: { skipBounds?: boolean } = {},
3547
- ): string {
3548
- if (!this._rendered) {
3549
- throw new Error("Viewer.render() must be called before addPart()");
3550
- }
3551
-
3552
- const nestedGroup = this.rendered.nestedGroup;
3553
-
3554
- // Validate parent exists and is a CompoundGroup
3555
- const parentGroup = nestedGroup.groups[parentPath];
3556
- if (!parentGroup || !isCompoundGroup(parentGroup)) {
3557
- throw new Error(
3558
- `Parent group not found or not a CompoundGroup: ${parentPath}`,
3559
- );
3560
- }
3561
-
3562
- const isTree = partData.parts != null && Array.isArray(partData.parts);
3563
-
3564
- // Build the absolute root path
3565
- const path = isTree
3566
- ? parentPath + partData.id // "/group1/group2" + "/shelf" → "/group1/group2/shelf"
3567
- : parentPath + "/" + partData.name; // "/group1/group2" + "/" + "obj1"
3568
-
3569
- // Validate root doesn't already exist at this level
3570
- if (nestedGroup.groups[path] != null) {
3571
- throw new Error(`Part already exists: ${path}`);
3572
- }
3573
-
3574
- // Rewrite ids to absolute paths
3575
- if (isTree) {
3576
- this._prefixIds(partData, parentPath);
3577
- } else {
3578
- partData.id = path;
3579
- }
3580
-
3581
- // Update this.shapes tree
3582
- const parentShapes = this._findShapesParent(path);
3583
- if (!parentShapes) {
3584
- throw new Error(`Parent not found in shapes data: ${parentPath}`);
3585
- }
3586
- if (!parentShapes.parts) {
3587
- parentShapes.parts = [];
3588
- }
3589
- parentShapes.parts.push(partData);
3590
-
3591
- // Render the new part using existing NestedGroup methods
3592
- if (isTree) {
3593
- // Subtree with children - renderLoop handles it directly
3594
- const newGroup = nestedGroup.renderLoop(partData);
3595
- parentGroup.add(newGroup);
3596
- } else {
3597
- // Single leaf shape - wrap in temporary tree for renderLoop
3598
- const wrapperId = `${path}/__addPart_tmp__`;
3599
- const wrapper: Shapes = {
3600
- version: partData.version,
3601
- id: wrapperId,
3602
- name: "__addPart_tmp__",
3603
- loc: [[0, 0, 0], [0, 0, 0, 1]],
3604
- parts: [partData],
3605
- };
3606
- const wrapperGroup = nestedGroup.renderLoop(wrapper);
3607
- // Move the rendered leaf from wrapper to actual parent
3608
- const leafGroup = nestedGroup.groups[path]!;
3609
- wrapperGroup.remove(leafGroup);
3610
- parentGroup.add(leafGroup);
3611
- // Clean up temporary wrapper
3612
- delete nestedGroup.groups[wrapperId];
3613
- }
3614
-
3615
- // Collect all new paths for settings application
3616
- const newPaths = Object.keys(nestedGroup.groups).filter(
3617
- (p) => p === path || p.startsWith(path + "/"),
3618
- );
3619
- this._applyCurrentSettings(newPaths);
3620
-
3621
- // Invalidate explode cache
3622
- if (this.expandedNestedGroup != null) {
3623
- deepDispose(this.expandedNestedGroup);
3624
- this.expandedNestedGroup = null;
3625
- this.expandedTree = null;
3626
- }
3627
-
3628
- if (options.skipBounds) {
3629
- this._treeNeedsRebuild = true;
3630
- return path;
3631
- }
3632
-
3633
- this._treeNeedsRebuild = true;
3634
- this.updateBounds();
3635
-
3636
- return path;
3637
- }
3638
-
3639
- /**
3640
- * Recursively prefix all `id` fields in a Shapes tree.
3641
- */
3642
- private _prefixIds(shapes: Shapes, prefix: string): void {
3643
- shapes.id = prefix + shapes.id;
3644
- if (shapes.parts) {
3645
- for (const part of shapes.parts) {
3646
- this._prefixIds(part, prefix);
3647
- }
3648
- }
3649
- }
3650
-
3651
- /**
3652
- * Remove a part (leaf or subtree) from the scene by path.
3653
- *
3654
- * When removing many parts in a batch, pass `{ skipBounds: true }` to
3655
- * defer the expensive bounds/clipping/treeview recomputation, then call
3656
- * `updateBounds()` once after the loop.
3657
- *
3658
- * @param path - The absolute path of the part to remove
3659
- * (e.g., "/assembly/shelf_5").
3660
- * @param options - Optional settings.
3661
- * @param options.skipBounds - When true, skip bounds/clipping/treeview
3662
- * update and re-render. Caller must call `updateBounds()` afterwards.
3663
- * @throws If the viewer is not rendered or the path doesn't exist.
3664
- * @public
3665
- */
3666
- removePart(path: string, options: { skipBounds?: boolean } = {}): void {
3667
- if (!this._rendered) {
3668
- throw new Error("Viewer.render() must be called before removePart()");
3669
- }
3670
-
3671
- const nestedGroup = this.rendered.nestedGroup;
3672
- const group = nestedGroup.groups[path];
3673
- if (!group) {
3674
- throw new Error(`Part not found: ${path}`);
3675
- }
3676
-
3677
- // Remove from Three.js scene graph
3678
- if (group.parent) {
3679
- group.parent.remove(group);
3680
- }
3681
-
3682
- // Collect all paths in this subtree and remove from groups map
3683
- const pathsToRemove = Object.keys(nestedGroup.groups).filter(
3684
- (p) => p === path || p.startsWith(path + "/"),
3685
- );
3686
- for (const p of pathsToRemove) {
3687
- delete nestedGroup.groups[p];
3688
- }
3689
-
3690
- // Remove from this.shapes tree
3691
- const parentShapes = this._findShapesParent(path);
3692
- if (parentShapes && parentShapes.parts) {
3693
- const name = path.substring(path.lastIndexOf("/") + 1);
3694
- parentShapes.parts = parentShapes.parts.filter((p) => p.name !== name);
3695
- }
3696
-
3697
- // Invalidate explode cache
3698
- if (this.expandedNestedGroup != null) {
3699
- deepDispose(this.expandedNestedGroup);
3700
- this.expandedNestedGroup = null;
3701
- this.expandedTree = null;
3702
- }
3703
-
3704
- if (options.skipBounds) {
3705
- // Defer disposal: keep materials alive so WebGL shader programs stay
3706
- // cached. Programs are reference-counted; disposing all materials of a
3707
- // type deletes the compiled program, causing expensive recompilation
3708
- // when addPart creates new materials. Deferred groups are disposed in
3709
- // updateBounds() after the render pass, when new materials already
3710
- // share the programs.
3711
- this._pendingDisposal.push(group);
3712
- this._treeNeedsRebuild = true;
3713
- return;
3714
- }
3715
-
3716
- // Dispose the removed Three.js objects
3717
- deepDispose(group);
3718
-
3719
- this._treeNeedsRebuild = true;
3720
- this.updateBounds();
3721
- }
3722
-
3723
- /**
3724
- * Update an existing part's geometry.
3725
- *
3726
- * When the mesh topology is unchanged (same number of vertices, triangles,
3727
- * and edge segments), buffers are updated in-place — no Three.js objects
3728
- * are disposed or recreated. When topology differs the method
3729
- * automatically falls back to a batched `removePart` + `addPart`.
3730
- *
3731
- * Only leaf parts (ObjectGroups with `shapeGeometry`) are supported.
3732
- * The part must already exist in the scene.
3733
- *
3734
- * When updating many parts in a batch, pass `{ skipBounds: true }` to
3735
- * defer the expensive bounds/clipping recomputation, then call
3736
- * `updateBounds()` once after the loop:
3737
- *
3738
- * ```ts
3739
- * for (const p of parts) {
3740
- * viewer.updatePart(path, data, { skipBounds: true });
3741
- * }
3742
- * viewer.updateBounds();
3743
- * ```
3744
- *
3745
- * @param path - The absolute path of the part to update
3746
- * (e.g., "/assembly/part").
3747
- * @param partData - A Shapes object with the new `shape` data.
3748
- * The `shape.vertices`, `shape.normals`, `shape.triangles`, and
3749
- * `shape.edges` fields are used to update the geometry.
3750
- * Optionally `color`, `alpha`, and `loc` are synced into `this.shapes`.
3751
- * @param options - Optional settings.
3752
- * @param options.skipBounds - When true, skip bounds/clipping/explode-cache
3753
- * update and re-render. Caller must call `updateBounds()` afterwards.
3754
- * @throws If the viewer is not rendered, the path doesn't exist,
3755
- * or the target is not a leaf ObjectGroup with shape geometry.
3756
- * @public
3757
- */
3758
- updatePart(
3759
- path: string,
3760
- partData: Shapes,
3761
- options: { skipBounds?: boolean } = {},
3762
- ): void {
3763
- if (!this._rendered) {
3764
- throw new Error("Viewer.render() must be called before updatePart()");
3765
- }
3766
-
3767
- const nestedGroup = this.rendered.nestedGroup;
3768
- const group = nestedGroup.groups[path];
3769
- if (!group) {
3770
- throw new Error(`Part not found: ${path}`);
3771
- }
3772
- if (!isObjectGroup(group)) {
3773
- throw new Error(`Part is not a leaf ObjectGroup: ${path}`);
3774
- }
3775
- if (!group.shapeGeometry) {
3776
- throw new Error(
3777
- `Part has no shape geometry (may be edges/vertices only): ${path}`,
3778
- );
3779
- }
3780
- if (!partData.shape) {
3781
- throw new Error("partData.shape is required for updatePart");
3782
- }
3783
-
3784
- const shape = partData.shape;
3785
- const geom = group.shapeGeometry;
3786
-
3787
- // --- Check whether topology is unchanged ---
3788
- const flatLen = (
3789
- data: number[] | number[][] | Float32Array | Uint32Array | undefined,
3790
- ): number => {
3791
- if (!data) return 0;
3792
- if (data instanceof Float32Array || data instanceof Uint32Array)
3793
- return data.length;
3794
- if (Array.isArray(data) && data.length > 0 && Array.isArray(data[0]))
3795
- return (data as number[][]).reduce((s, a) => s + a.length, 0);
3796
- return (data as number[]).length;
3797
- };
3798
-
3799
- const posAttr = geom.getAttribute("position") as THREE.BufferAttribute;
3800
- const oldIndex = geom.getIndex();
3801
-
3802
- const sameVertices = posAttr.count === flatLen(shape.vertices) / 3;
3803
- const sameTriangles =
3804
- oldIndex != null && oldIndex.count === flatLen(shape.triangles);
3805
-
3806
- let sameEdges = true;
3807
- if (group.edges && shape.edges) {
3808
- if (isLineSegments2(group.edges)) {
3809
- const edgeGeom = group.edges.geometry;
3810
- const instanceCount =
3811
- edgeGeom.getAttribute("instanceStart")?.count ?? 0;
3812
- // LineSegmentsGeometry stores 1 instance per segment (2 points)
3813
- sameEdges = instanceCount === flatLen(shape.edges) / 6;
3814
- } else {
3815
- const edgePosAttr = group.edges.geometry.getAttribute(
3816
- "position",
3817
- ) as THREE.BufferAttribute | null;
3818
- sameEdges =
3819
- edgePosAttr != null &&
3820
- edgePosAttr.count === flatLen(shape.edges) / 3;
3821
- }
3822
- }
3823
-
3824
- if (!sameVertices || !sameTriangles || !sameEdges) {
3825
- // Topology changed — fall back to remove + add.
3826
- // Visibility states are preserved by _rebuildTreeView() which is
3827
- // triggered via updateBounds() (or the caller's updateBounds call
3828
- // when skipBounds is true).
3829
- const parentPath = path.substring(0, path.lastIndexOf("/"));
3830
- this.removePart(path, { skipBounds: true });
3831
- this.addPart(parentPath, partData, { skipBounds: true });
3832
- if (!options.skipBounds) {
3833
- this.updateBounds();
3834
- }
3835
- return;
3836
- }
3837
-
3838
- // --- Topology matches — fast in-place buffer update ---
3839
-
3840
- // Helper: convert to typed arrays
3841
- const toF32 = (
3842
- data: number[] | number[][] | Float32Array,
3843
- ): Float32Array => {
3844
- if (data instanceof Float32Array) return data;
3845
- if (Array.isArray(data) && data.length > 0 && Array.isArray(data[0])) {
3846
- return new Float32Array((data as number[][]).flat());
3847
- }
3848
- return new Float32Array(data as number[]);
3849
- };
3850
- const toU32 = (
3851
- data: number[] | number[][] | Uint32Array,
3852
- ): Uint32Array => {
3853
- if (data instanceof Uint32Array) return data;
3854
- if (Array.isArray(data) && data.length > 0 && Array.isArray(data[0])) {
3855
- return new Uint32Array((data as number[][]).flat());
3856
- }
3857
- return new Uint32Array(data as number[]);
3858
- };
3859
-
3860
- // Step 1: Update face geometry buffers (in-place, counts match)
3861
- const newPositions = toF32(shape.vertices);
3862
- const newNormals = toF32(shape.normals);
3863
- const newTriangles = toU32(shape.triangles);
3864
-
3865
- (posAttr.array as Float32Array).set(newPositions);
3866
- posAttr.needsUpdate = true;
3867
- const normAttr = geom.getAttribute("normal") as THREE.BufferAttribute;
3868
- (normAttr.array as Float32Array).set(newNormals);
3869
- normAttr.needsUpdate = true;
3870
- (oldIndex!.array as Uint32Array).set(newTriangles);
3871
- oldIndex!.needsUpdate = true;
3872
-
3873
- geom.computeBoundingBox();
3874
- geom.computeBoundingSphere();
3875
-
3876
- // Step 2: Update edge geometry (in-place, counts match)
3877
- if (group.edges && shape.edges && shape.edges.length > 0) {
3878
- const newEdgePositions = toF32(shape.edges);
3879
- if (isLineSegments2(group.edges)) {
3880
- const startAttr = group.edges.geometry.getAttribute("instanceStart");
3881
- if (startAttr && "data" in startAttr) {
3882
- const buffer = (startAttr as THREE.InterleavedBufferAttribute).data;
3883
- (buffer.array as Float32Array).set(newEdgePositions);
3884
- buffer.needsUpdate = true;
3885
- } else {
3886
- group.edges.geometry.setPositions(newEdgePositions);
3887
- }
3888
- group.edges.geometry.computeBoundingBox();
3889
- group.edges.geometry.computeBoundingSphere();
3890
- } else {
3891
- const edgeGeom = group.edges.geometry;
3892
- const edgePosAttr = edgeGeom.getAttribute(
3893
- "position",
3894
- ) as THREE.BufferAttribute;
3895
- (edgePosAttr.array as Float32Array).set(newEdgePositions);
3896
- edgePosAttr.needsUpdate = true;
3897
- edgeGeom.computeBoundingBox();
3898
- edgeGeom.computeBoundingSphere();
3899
- }
3900
- }
3901
-
3902
- // Step 3: Sync this.shapes data
3903
- const parentShapes = this._findShapesParent(path);
3904
- if (parentShapes && parentShapes.parts) {
3905
- const name = path.substring(path.lastIndexOf("/") + 1);
3906
- const entry = parentShapes.parts.find((p) => p.name === name);
3907
- if (entry) {
3908
- entry.shape = shape;
3909
- if (partData.color !== undefined) entry.color = partData.color;
3910
- if (partData.alpha !== undefined) entry.alpha = partData.alpha;
3911
- if (partData.loc !== undefined) entry.loc = partData.loc;
3912
- }
3913
- }
3914
-
3915
- // Step 4: Update bounds or defer
3916
- if (options.skipBounds) {
3917
- return;
3918
- }
3919
-
3920
- this.updateBounds();
3921
- }
3922
-
3923
- /**
3924
- * Recompute scene bounds, camera far plane, clipping stencils, and
3925
- * re-render. Call this once after a batch of
3926
- * `addPart`, `removePart`, or `updatePart` calls that used
3927
- * `{ skipBounds: true }`.
3928
- *
3929
- * If parts were added or removed in the batch, the navigation treeview
3930
- * is also rebuilt automatically.
3931
- *
3932
- * @public
3933
- */
3934
- updateBounds(): void {
3935
- if (!this._rendered) {
3936
- throw new Error("Viewer.render() must be called before updateBounds()");
3937
- }
3938
-
3939
- const nestedGroup = this.rendered.nestedGroup;
3940
-
3941
- // Recompute bounding box from current geometry
3942
- nestedGroup.bbox = null;
3943
- this.bbox = nestedGroup.boundingBox();
3944
-
3945
- const center = new THREE.Vector3();
3946
- this.bbox.getCenter(center);
3947
- this.bb_max = this.bbox.max_dist_from_center();
3948
- this.bb_radius = Math.max(
3949
- this.bbox.boundingSphere().radius,
3950
- center.length(),
3951
- );
3952
-
3953
- // Always update camera far plane and distance (cheap)
3954
- this.rendered.camera.updateFarPlane(this.bb_radius);
3955
- this.rendered.camera.updateCameraDistance(this.bb_radius);
3956
-
3957
- // Update controls reset location to current bbox center so that
3958
- // reset() frames the updated geometry, not the original.
3959
- // Shift both target and position by the same offset to preserve
3960
- // the viewing direction and distance.
3961
- const loc = this.rendered.controls.getResetLocation();
3962
- const offset = loc.position0.clone().sub(loc.target0);
3963
- loc.target0.set(...this.bbox.center());
3964
- loc.position0.copy(loc.target0).add(offset);
3965
- this.rendered.controls.setResetLocation(
3966
- loc.target0,
3967
- loc.position0,
3968
- loc.quaternion0,
3969
- loc.zoom0,
3970
- );
3971
-
3972
- // Only rebuild stencils if geometry grew beyond the region that stencils
3973
- // were last built for. Shrinking geometry still fits within existing
3974
- // stencils, so skip the expensive rebuild in that case.
3975
- const newCSize = 1.1 * Math.max(
3976
- Math.abs(this.bbox.min.length()),
3977
- Math.abs(this.bbox.max.length()),
3978
- );
3979
- if (newCSize > this._stencilCSize + 1e-6) {
3980
- this._stencilCSize = newCSize;
3981
- const clipping = this.rendered.clipping;
3982
- clipping.rebuildStencils(this.bbox.center(), 2 * newCSize);
3983
- nestedGroup.setClipPlanes(clipping.clipPlanes);
3984
- this.display.setSliderLimits(newCSize);
3985
- }
3986
-
3987
- // Invalidate explode cache
3988
- if (this.expandedNestedGroup != null) {
3989
- deepDispose(this.expandedNestedGroup);
3990
- this.expandedNestedGroup = null;
3991
- this.expandedTree = null;
3992
- }
3993
-
3994
- // Rebuild treeview if parts were added or removed in this batch
3995
- if (this._treeNeedsRebuild) {
3996
- this._treeNeedsRebuild = false;
3997
- this._rebuildTreeView();
3998
- }
3999
-
4000
- // Re-render
4001
- this.update(this.updateMarker);
4002
-
4003
- // Flush deferred disposal: now that new materials have been rendered
4004
- // (and share compiled shader programs), dispose the old objects safely.
4005
- if (this._pendingDisposal.length > 0) {
4006
- for (const obj of this._pendingDisposal) {
4007
- deepDispose(obj);
4008
- }
4009
- this._pendingDisposal = [];
4010
- }
4011
- }
4012
-
4013
- /**
4014
- * Pre-size the clipping stencil region so that all future `updatePart` /
4015
- * `updateBounds` calls whose geometry stays within `bb` will never trigger
4016
- * an expensive `rebuildStencils`.
4017
- *
4018
- * Call this once before a series of updates when the maximum extent of the
4019
- * geometry is known upfront (e.g. the parameter range of a slider).
4020
- *
4021
- * @param bb - The maximum bounding box that geometry will ever occupy.
4022
- */
4023
- ensureStencilSize(bb: BoundingBoxFlat): void {
4024
- if (!this._rendered) {
4025
- throw new Error(
4026
- "Viewer.render() must be called before ensureStencilSize()",
4027
- );
4028
- }
4029
-
4030
- const min = new THREE.Vector3(bb.xmin, bb.ymin, bb.zmin);
4031
- const max = new THREE.Vector3(bb.xmax, bb.ymax, bb.zmax);
4032
- const center = new THREE.Vector3()
4033
- .addVectors(min, max)
4034
- .multiplyScalar(0.5);
4035
-
4036
- const requiredCSize =
4037
- 1.1 * Math.max(Math.abs(min.length()), Math.abs(max.length()));
4038
-
4039
- if (requiredCSize > this._stencilCSize + 1e-6) {
4040
- this._stencilCSize = requiredCSize;
4041
- const clipping = this.rendered.clipping;
4042
- const nestedGroup = this.rendered.nestedGroup;
4043
- clipping.rebuildStencils(
4044
- [center.x, center.y, center.z] as [number, number, number],
4045
- 2 * requiredCSize,
4046
- );
4047
- nestedGroup.setClipPlanes(clipping.clipPlanes);
4048
- this.display.setSliderLimits(requiredCSize);
4049
- }
4050
- }
4051
-
4052
- // ---------------------------------------------------------------------------
4053
- // UI sensitivity
4054
- // ---------------------------------------------------------------------------
4055
-
4056
- /**
4057
- * Get zoom speed.
4058
- * @returns zoomSpeed value.
4059
- */
4060
- getZoomSpeed(): number {
4061
- return this.state.get("zoomSpeed");
4062
- }
4063
-
4064
- /**
4065
- * Set zoom speed.
4066
- * @param val - the new zoom speed
4067
- * @param notify - whether to send notification or not.
4068
- */
4069
- setZoomSpeed = (val: number, notify: boolean = true): void => {
4070
- this.state.set("zoomSpeed", val, notify);
4071
- this.rendered.controls.setZoomSpeed(val);
4072
- };
4073
-
4074
- /**
4075
- * Get panning speed.
4076
- * @returns pan speed value.
4077
- */
4078
- getPanSpeed(): number {
4079
- return this.state.get("panSpeed");
4080
- }
4081
-
4082
- /**
4083
- * Set pan speed.
4084
- * @param val - the new pan speed
4085
- * @param notify - whether to send notification or not.
4086
- */
4087
- setPanSpeed = (val: number, notify: boolean = true): void => {
4088
- this.state.set("panSpeed", val, notify);
4089
- this.rendered.controls.setPanSpeed(val);
4090
- };
4091
-
4092
- /**
4093
- * Get rotation speed.
4094
- * @returns rotation speed value.
4095
- */
4096
- getRotateSpeed(): number {
4097
- return this.state.get("rotateSpeed");
4098
- }
4099
-
4100
- /**
4101
- * Set rotation speed.
4102
- * @param val - the new rotation speed.
4103
- * @param notify - whether to send notification or not.
4104
- */
4105
- setRotateSpeed = (val: number, notify: boolean = true): void => {
4106
- this.state.set("rotateSpeed", val, notify);
4107
- this.rendered.controls.setRotateSpeed(val);
4108
- };
4109
-
4110
- /**
4111
- * Get holroyd (non-tumbling) trackball mode.
4112
- * @returns holroyd flag.
4113
- */
4114
- getHolroyd(): boolean {
4115
- return this.state.get("holroyd");
4116
- }
4117
-
4118
- /**
4119
- * Set holroyd (non-tumbling) trackball mode.
4120
- * When false, uses standard Three.js TrackballControls behavior.
4121
- * @param flag - whether to enable holroyd mode.
4122
- * @param notify - whether to send notification or not.
4123
- */
4124
- setHolroyd = (flag: boolean, notify: boolean = true): void => {
4125
- this.state.set("holroyd", flag, notify);
4126
- this.rendered.controls.setHolroydTrackball(flag);
4127
- };
4128
-
4129
- // ---------------------------------------------------------------------------
4130
- // Clipping Planes
4131
- // ---------------------------------------------------------------------------
4132
-
4133
- /**
4134
- * Get intersection mode.
4135
- * @returns clip intersection value.
4136
- */
4137
- getClipIntersection(): boolean {
4138
- return this.state.get("clipIntersection");
4139
- }
4140
-
4141
- /**
4142
- * Set the clipping mode to intersection mode
4143
- * @param flag - whether to use intersection mode
4144
- * @param notify - whether to send notification or not.
4145
- */
4146
- setClipIntersection = (flag: boolean, notify: boolean = true): void => {
4147
- if (flag == null || !this.ready) return;
4148
-
4149
- this.state.set("clipIntersection", flag, notify);
4150
- this.rendered.nestedGroup.setClipIntersection(flag);
4151
-
4152
- const clipPlanes = flag
4153
- ? this.rendered.clipping.reverseClipPlanes
4154
- : this.rendered.clipping.clipPlanes;
4155
-
4156
- for (const child of this.rendered.nestedGroup.rootGroup!.children) {
4157
- if (child.name === "PlaneMeshes") {
4158
- for (const capPlane of child.children) {
4159
- if (!isIndexedMesh(capPlane)) continue;
4160
- if (!isClippableMaterial(capPlane.material)) continue;
4161
- capPlane.material.clippingPlanes = clipPlanes!.filter(
4162
- (_: THREE.Plane, j: number) => j !== capPlane.index,
4163
- );
4164
- }
4165
- }
4166
- }
4167
-
4168
- for (const child of this.rendered.scene.children) {
4169
- if (child.name === "PlaneHelpers") {
4170
- for (const helper of child.children[0].children) {
4171
- if (!isIndexedMesh(helper)) continue;
4172
- if (!isClippableMaterial(helper.material)) continue;
4173
- helper.material.clippingPlanes = clipPlanes!.filter(
4174
- (_: THREE.Plane, j: number) => j !== helper.index,
4175
- );
4176
- }
4177
- }
4178
- }
4179
-
4180
- this.update(this.updateMarker);
4181
- };
4182
-
4183
- /**
4184
- * Get whether the clipping caps color status
4185
- * @returns color caps value (object color (true) or RGB (false)).
4186
- */
4187
- getObjectColorCaps = (): boolean => {
4188
- return this._rendered?.clipping.getObjectColorCaps() ?? false;
4189
- };
4190
-
4191
- /**
4192
- * Toggle the clipping caps color between object color and RGB
4193
- * @param flag - whether to use intersection mode
4194
- * @param notify - whether to send notification or not.
4195
- */
4196
- setClipObjectColorCaps = (flag: boolean, notify: boolean = true): void => {
4197
- if (flag == null || !this.ready) return;
4198
- this.state.set("clipObjectColors", flag, notify);
4199
- this.rendered.clipping.setObjectColorCaps(flag);
4200
- this.update(this.updateMarker);
4201
- };
4202
-
4203
- /**
4204
- * Get clipping plane state.
4205
- * @returns clip plane visibility value.
4206
- */
4207
- getClipPlaneHelpers(): boolean {
4208
- return this.state.get("clipPlaneHelpers");
4209
- }
4210
-
4211
- /**
4212
- * Show/hide clip plane helpers
4213
- * @param flag - whether to show clip plane helpers
4214
- * @param notify - whether to send notification or not.
4215
- */
4216
- setClipPlaneHelpers = (flag: boolean, notify: boolean = true): void => {
4217
- if (flag == null || !this.ready) return;
4218
-
4219
- this.state.set("clipPlaneHelpers", flag, notify);
4220
- // Only show plane helpers if flag is true AND clip tab is active
4221
- const isClipTabActive = this.state.get("activeTab") === "clip";
4222
- this.rendered.clipping.planeHelpers!.visible = flag && isClipTabActive;
4223
-
4224
- this.update(this.updateMarker);
4225
- };
4226
-
4227
- /**
4228
- * Get clipping plane state.
4229
- * @param index - index of the normal: 0, 1 ,2
4230
- * @returns clip plane visibility value.
4231
- */
4232
- getClipNormal(index: ClipIndex): Vector3Tuple {
4233
- return toVector3Tuple(this.clipNormals[index].toArray());
4234
- }
4235
-
4236
- /**
4237
- * Set the normal at index to a given normal
4238
- * @param index - index of the normal: 0, 1 ,2
4239
- * @param normal - 3 dim array representing the normal
4240
- * @param value - value of the slider, if given
4241
- * @param notify - whether to send notification or not.
4242
- */
4243
- setClipNormal(
4244
- index: ClipIndex,
4245
- normal: Vector3Tuple | null,
4246
- value: number | null = null,
4247
- notify: boolean = true,
4248
- ): void {
4249
- if (normal == null || !this.ready) return;
4250
- const normal1 = new THREE.Vector3(...normal).normalize();
4251
- this.clipNormals[index] = normal1;
4252
-
4253
- // Update state (triggers auto-notification for clipNormal)
4254
- const normalKeys = ["clipNormal0", "clipNormal1", "clipNormal2"] as const;
4255
- this.state.set(normalKeys[index], normal1, notify);
4256
-
4257
- this.rendered.clipping.setNormal(index, normal1);
4258
- this.rendered.clipping.setConstant(index, this.gridSize / 2);
4259
- if (value == null) value = this.gridSize / 2;
4260
- // setClipSlider will handle its own state update and notification
4261
- this.setClipSlider(index, value, notify);
4262
-
4263
- this.rendered.nestedGroup.setClipPlanes(this.rendered.clipping.clipPlanes);
4264
-
4265
- this.update(this.updateMarker);
4266
- }
4267
-
4268
- /**
4269
- * Set the normal at index to the current viewing direction
4270
- * @param index - index of the normal: 0, 1 ,2
4271
- * @param notify - whether to send notification or not.
4272
- */
4273
- setClipNormalFromPosition = (index: ClipIndex, notify: boolean = true): void => {
4274
- if (!this.ready) return;
4275
- const cameraPosition = this.rendered.camera.getPosition().clone();
4276
- const normal = toVector3Tuple(
4277
- cameraPosition
4278
- .sub(this.rendered.controls.getTarget())
4279
- .normalize()
4280
- .negate()
4281
- .toArray()
4282
- );
4283
- this.setClipNormal(index, normal, null, notify);
4284
- };
4285
-
4286
- /**
4287
- * Get clipping slider value.
4288
- * @param index - index of the normal: 0, 1 ,2
4289
- * @returns clip slider value.
4290
- */
4291
- getClipSlider = (index: 0 | 1 | 2): number => {
4292
- const keys = ["clipSlider0", "clipSlider1", "clipSlider2"] as const;
4293
- return this.state.get(keys[index]);
4294
- };
4295
-
4296
- /**
4297
- * Set clipping slider value and update the clipping plane.
4298
- * @param index - index of the normal: 0, 1 ,2
4299
- * @param value - value for the clipping slider
4300
- * @param notify - whether to send notification or not.
4301
- */
4302
- setClipSlider = (
4303
- index: 0 | 1 | 2,
4304
- value: number,
4305
- notify: boolean = true,
4306
- ): void => {
4307
- if (value === -1 || value == null) return;
4308
-
4309
- const keys = ["clipSlider0", "clipSlider1", "clipSlider2"] as const;
4310
- this.state.set(keys[index], value, notify);
4311
-
4312
- // Also update the 3D clipping plane (consistent with other setters)
4313
- if (this.ready) {
4314
- this.rendered.clipping.setConstant(index, value);
4315
- this.update(this.updateMarker);
4316
- }
4317
- };
4318
-
4319
- /**
4320
- * Resets clip planes to default normals and slider positions.
4321
- * Normals reset to -X, -Y, -Z; sliders to gridSize/2; checkboxes unchecked.
4322
- */
4323
- resetClip = (): void => {
4324
- if (!this.ready) return;
4325
- const mid = this.gridSize / 2;
4326
- this.setClipNormal(0, [-1, 0, 0], mid, true);
4327
- this.setClipNormal(1, [0, -1, 0], mid, true);
4328
- this.setClipNormal(2, [0, 0, -1], mid, true);
4329
- this.setClipIntersection(false, true);
4330
- this.setClipObjectColorCaps(false, true);
4331
- this.setClipPlaneHelpers(false, true);
4332
- };
4333
-
4334
- // ---------------------------------------------------------------------------
4335
- // Image Export
4336
- // ---------------------------------------------------------------------------
4337
-
4338
- /**
4339
- * Replace CadView with an inline png image of the canvas.
4340
- *
4341
- * Note: Only the canvas will be shown, no tools and orientation marker
4342
- */
4343
- pinAsPng = (): void => {
4344
- const screenshot = this.getImage("screenshot");
4345
- screenshot.then((data: ImageResult) => {
4346
- if (typeof data.dataUrl !== "string") {
4347
- logger.error("Screenshot dataUrl is not a string");
4348
- return;
4349
- }
4350
- const image = document.createElement("img");
4351
- image.width = this.state.get("cadWidth");
4352
- image.height = this.state.get("height");
4353
- image.src = data.dataUrl;
4354
- if (this.pinAsPngCallback == null) {
4355
- // default, replace the viewer with the image
4356
- this.display.replaceWithImage(image);
4357
- }
4358
- });
4359
- };
4360
-
4361
- /**
4362
- * Get the current canvas as png data.
4363
- * @param taskId - an id to identify the screenshot
4364
- * @returns Promise resolving to task ID and data URL
4365
- * Note: Only the canvas will be shown, no tools and orientation marker
4366
- * @public
4367
- */
4368
- getImage = (taskId: string): Promise<ImageResult> => {
4369
- if (!this.ready) {
4370
- return Promise.resolve({ task: taskId, dataUrl: null });
4371
- }
4372
- // canvas.toBlob can be very slow when animation loop is off!
4373
- const animationLoop = this.hasAnimationLoop;
4374
- if (!animationLoop) {
4375
- this.toggleAnimationLoop(true);
4376
- }
4377
- this.rendered.orientationMarker.setVisible(false);
4378
- this.update(true);
4379
-
4380
- return this.display.captureCanvas({
4381
- taskId,
4382
- render: () => {
4383
- this.renderer.setViewport(
4384
- 0,
4385
- 0,
4386
- this.state.get("cadWidth"),
4387
- this.state.get("height"),
4388
- );
4389
- if (this._studioManager.isEnvBackgroundActive) {
4390
- this._studioManager.updateEnvBackground(this.renderer, this.rendered.camera.getCamera());
4391
- }
4392
- if (this._studioManager.hasComposer) {
4393
- this._studioManager.render();
4394
- } else {
4395
- this.renderer.render(this.rendered.scene, this.rendered.camera.getCamera());
4396
- }
4397
- },
4398
- onComplete: () => {
4399
- // Restore animation loop to original state
4400
- if (!animationLoop) {
4401
- this.toggleAnimationLoop(false);
4402
- }
4403
- this.rendered.orientationMarker.setVisible(true);
4404
- this.update(true);
4405
- },
4406
- });
4407
- };
4408
-
4409
- // ---------------------------------------------------------------------------
4410
- // Explode Animation
4411
- // ---------------------------------------------------------------------------
4412
-
4413
- /**
4414
- * Calculate explode trajectories and initiate the animation.
4415
- *
4416
- * @param duration - duration of animation.
4417
- * @param speed - speed of animation.
4418
- * @param multiplier - multiplier for length of trajectories.
4419
- * @public
4420
- */
4421
- explode(
4422
- duration: number = 2,
4423
- speed: number = 1,
4424
- multiplier: number = 2.5,
4425
- ): void {
4426
- this.clearAnimation();
4427
-
4428
- const use_origin = this.getAxes0();
4429
-
4430
- const worldCenterOrOrigin = new THREE.Vector3();
4431
- const worldObjectCenter = new THREE.Vector3();
4432
-
4433
- let worldDirection: THREE.Vector3 | null = null;
4434
- let localDirection: THREE.Vector3 | null = null;
4435
- let scaledLocalDirection: THREE.Vector3 | null = null;
4436
-
4437
- if (!use_origin) {
4438
- const bb = new THREE.Box3().setFromObject(this.rendered.nestedGroup.rootGroup!);
4439
- bb.getCenter(worldCenterOrOrigin);
4440
- }
4441
- for (const id in this.rendered.nestedGroup.groups) {
4442
- // Loop over all Group elements
4443
- const group = this.rendered.nestedGroup.groups[id];
4444
-
4445
- const b = new THREE.Box3();
4446
- if (group instanceof ObjectGroup) {
4447
- b.expandByObject(group);
4448
- }
4449
- if (b.isEmpty()) {
4450
- continue;
4451
- }
4452
- b.getCenter(worldObjectCenter);
4453
- // Explode around global center or origin
4454
- worldDirection = worldObjectCenter.sub(worldCenterOrOrigin);
4455
- localDirection = group.parent!.worldToLocal(worldDirection.clone());
4456
-
4457
- // Use the parent to calculate the local directions
4458
- scaledLocalDirection = group.parent!.worldToLocal(
4459
- worldDirection.clone().multiplyScalar(multiplier),
4460
- );
4461
- // and ensure to shift objects at its center and not at its position
4462
- scaledLocalDirection.sub(localDirection);
4463
-
4464
- // build an animation track for the group with this direction
4465
- this.addPositionTrack(
4466
- id,
4467
- [0, duration],
4468
- [[0, 0, 0], scaledLocalDirection.toArray()],
4469
- );
4470
- }
4471
- this.initAnimation(duration, speed, "E", false);
4472
- }
4473
-
4474
- /**
4475
- * Toggle explode mode on/off.
4476
- * @param flag - whether to enable or disable explode mode
4477
- * @param notify - whether to send notification or not.
4478
- * @public
4479
- */
4480
- setExplode(flag: boolean, notify: boolean = true): void {
4481
- const isExplodeActive = this.state.get("animationMode") === "explode";
4482
- if (flag === isExplodeActive) return;
4483
-
4484
- if (flag) {
4485
- if (this.hasAnimation()) {
4486
- this.backupAnimation();
4487
- }
4488
- this.explode(); // This sets animationMode to "explode" via initAnimation
4489
- } else {
4490
- if (this.hasAnimation()) {
4491
- this.controlAnimation("stop");
4492
- this.clearAnimation(); // This sets animationMode to "none"
4493
- this.restoreAnimation();
4494
- } else {
4495
- this.state.set("animationMode", "none");
4496
- }
4497
- }
4498
-
4499
- // Send explode notification (client expects boolean, not animationMode)
4500
- this.checkChanges({ explode: flag }, notify);
4501
- }
4502
-
4503
- /**
4504
- * Activate or deactivate a measurement/selection tool.
4505
- * This is the single entry point for tool state changes - Display should call this
4506
- * rather than mutating state directly.
4507
- * @param name - Tool name ("distance", "properties", "select")
4508
- * @param flag - Whether to activate (true) or deactivate (false) the tool
4509
- */
4510
- activateTool(name: string, flag: boolean): void {
4511
- const currentTool = this.state.get("activeTool");
4512
-
4513
- if (flag) {
4514
- // Activating a tool
4515
- this.state.set("animationMode", "none");
4516
- if (this.hasAnimation()) {
4517
- this.backupAnimation();
4518
- }
4519
- this.state.set("activeTool", name);
4520
- } else {
4521
- // Deactivating a tool
4522
- if (currentTool === name || name === "explode") {
4523
- this.state.set("activeTool", null);
4524
- }
4525
- if (this.hasAnimation()) {
4526
- this.controlAnimation("stop");
4527
- this.clearAnimation();
4528
- this.restoreAnimation();
4529
- }
4530
- }
4531
- }
4532
-
4533
- // ---------------------------------------------------------------------------
4534
- // Keyboard Configuration
4535
- // ---------------------------------------------------------------------------
4536
-
4537
- /**
4538
- * Set modifiers and action shortcuts for keymap
4539
- *
4540
- * @param config - keymap e.g. {"shift": "shiftKey", "ctrl": "ctrlKey", "meta": "altKey", "axes": "a", ...}
4541
- */
4542
- setKeyMap(config: Keymap): void {
4543
- const modifierKeys = new Set(["shift", "ctrl", "meta", "alt"]);
4544
- const modifiers: Partial<KeyMappingConfig> = {};
4545
- const actions: Record<string, string> = {};
4546
-
4547
- for (const [key, value] of Object.entries(config)) {
4548
- if (value === undefined) continue;
4549
- if (modifierKeys.has(key)) {
4550
- modifiers[key as keyof KeyMappingConfig] = value as KeyMappingConfig[keyof KeyMappingConfig];
4551
- } else {
4552
- actions[key] = value;
4553
- }
4554
- }
4555
-
4556
- if (Object.keys(modifiers).length > 0) {
4557
- const before = KeyMapper.get_config();
4558
- KeyMapper.set(modifiers);
4559
- this.display.updateHelp(before, modifiers);
4560
- }
4561
-
4562
- KeyMapper.setActionShortcuts(actions);
4563
- this.display.updateTooltips();
4564
- }
4565
-
4566
- // ---------------------------------------------------------------------------
4567
- // View Layout
4568
- // ---------------------------------------------------------------------------
4569
-
4570
- /**
4571
- * Get the current CAD view width.
4572
- * @public
4573
- */
4574
- get cadWidth(): number {
4575
- return this.state.get("cadWidth");
4576
- }
4577
-
4578
- /**
4579
- * Get the current tree width.
4580
- * @public
4581
- */
4582
- get treeWidth(): number {
4583
- return this.state.get("treeWidth");
4584
- }
4585
-
4586
- /**
4587
- * Get the current view height.
4588
- * @public
4589
- */
4590
- get height(): number {
4591
- return this.state.get("height");
4592
- }
4593
-
4594
- /**
4595
- * Get the current glass mode state.
4596
- * @public
4597
- */
4598
- get glass(): boolean {
4599
- return this.state.get("glass");
4600
- }
4601
-
4602
- /**
4603
- * Resize UI and renderer.
4604
- *
4605
- * @param cadWidth - new width of CAD View
4606
- * @param treeWidth - new width of navigation tree
4607
- * @param height - new height of CAD View
4608
- * @param glass - Whether to use glass mode or not
4609
- * @public
4610
- */
4611
- resizeCadView(
4612
- cadWidth: number,
4613
- treeWidth: number,
4614
- height: number,
4615
- glass: boolean = false,
4616
- ): void {
4617
- this.state.set("cadWidth", cadWidth);
4618
- this.state.set("height", height);
4619
-
4620
- // Adapt renderer dimensions
4621
- this.renderer.setSize(cadWidth, height);
4622
-
4623
- // Adapt display dimensions
4624
- this.display.setSizes({
4625
- treeWidth: treeWidth,
4626
- treeHeight: this.state.get("treeHeight"),
4627
- cadWidth: cadWidth,
4628
- height: height,
4629
- });
4630
- // Set glass state - subscription will update UI
4631
- this.state.set("glass", glass);
4632
-
4633
- const fullWidth = cadWidth + (glass ? 0 : treeWidth);
4634
- this.display.updateToolbarCollapse(fullWidth);
4635
-
4636
- // Adapt camera to new dimensions
4637
- this.rendered.camera.changeDimensions(this.bb_radius, cadWidth, height);
4638
- this.controls.handleResize();
4639
-
4640
- // Resize the post-processing composer (render targets must match viewport)
4641
- this._studioManager.setSize(cadWidth, height);
4642
-
4643
- // update the this
4644
- this.update(true);
4645
-
4646
- // update the raycaster
4647
- if (this.raycaster) {
4648
- this.raycaster.width = cadWidth;
4649
- this.raycaster.height = height;
4650
- }
4651
- }
4652
-
4653
- // ---------------------------------------------------------------------------
4654
- // UI Control Wrappers (delegate to display)
4655
- // ---------------------------------------------------------------------------
4656
-
4657
- /**
4658
- * Set camera to a predefined view direction.
4659
- * @param direction - "iso", "front", "rear", "left", "right", "top", or "bottom"
4660
- * @param focus - whether to focus/center on visible objects
4661
- * @public
4662
- */
4663
- setView = (direction: string, focus: boolean = false): void => {
4664
- this.display.setView(direction, focus);
4665
- };
4666
-
4667
- /**
4668
- * Enable/disable glass mode (transparent overlay UI).
4669
- * @param flag - whether to enable glass mode
4670
- * @param notify - whether to send notification or not.
4671
- * @public
4672
- */
4673
- glassMode = (flag: boolean, notify: boolean = true): void => {
4674
- this.state.set("glass", flag, notify);
4675
- this.display.glassMode(flag);
4676
- };
4677
-
4678
- /**
4679
- * Collapse or expand tree nodes.
4680
- * @param value - CollapseState enum value
4681
- * @param notify - whether to send notification or not.
4682
- * @public
4683
- */
4684
- collapseNodes = (value: CollapseState, notify: boolean = true): void => {
4685
- this.state.set("collapse", value, notify);
4686
- if (!this.treeview) return;
4687
- // Translate CollapseState to treeview operations
4688
- switch (value) {
4689
- case CollapseState.COLLAPSED:
4690
- this.treeview.collapseAll();
4691
- break;
4692
- case CollapseState.ROOT:
4693
- this.treeview.openLevel(1);
4694
- break;
4695
- case CollapseState.LEAVES:
4696
- this.treeview.openLevel(-1);
4697
- break;
4698
- case CollapseState.EXPANDED:
4699
- this.treeview.expandAll();
4700
- break;
4701
- }
4702
- };
4703
-
4704
- /**
4705
- * Set the UI theme.
4706
- * @param theme - "light", "dark", or "browser" for auto-detection
4707
- * @returns The resolved theme ("light" or "dark")
4708
- * @public
4709
- */
4710
- setTheme = (theme: ThemeInput): string => {
4711
- return this.display.setTheme(theme);
4712
- };
4713
-
4714
- /**
4715
- * Show/hide the help dialog.
4716
- * @param flag - whether to show the help dialog
4717
- * @public
4718
- */
4719
- showHelp = (flag: boolean): void => {
4720
- this.display.showHelp(flag);
4721
- };
4722
-
4723
- /**
4724
- * Collapse or expand the info panel in glass mode.
4725
- * @param flag - true to show, false to collapse
4726
- * @public
4727
- */
4728
- showInfoPanel = (flag: boolean): void => {
4729
- this.display.showInfo(flag);
4730
- };
4731
-
4732
- /**
4733
- * @deprecated Use showInfoPanel() instead.
4734
- */
4735
- showInfo = (flag: boolean): void => {
4736
- console.warn("showInfo() is deprecated, use showInfoPanel() instead.");
4737
- this.showInfoPanel(flag);
4738
- };
4739
-
4740
- /**
4741
- * Collapse or expand the tools panel (tabs + content) in glass mode.
4742
- * @param flag - true to show, false to collapse
4743
- * @public
4744
- */
4745
- showToolsPanel = (flag: boolean): void => {
4746
- this.display.showToolsPanel(flag);
4747
- };
4748
-
4749
- /**
4750
- * Show/hide the pinning button.
4751
- * @param flag - whether to show the pinning button
4752
- * @public
4753
- */
4754
- showPinning = (flag: boolean): void => {
4755
- this.display.showPinning(flag);
4756
- };
4757
-
4758
- /**
4759
- * Show/hide the measure tools.
4760
- * @param flag - whether to show the measure tools
4761
- * @public
4762
- */
4763
- showMeasureTools = (flag: boolean): void => {
4764
- this.display.showMeasureTools(flag);
4765
- };
4766
-
4767
- /**
4768
- * Show/hide the select tool.
4769
- * @param flag - whether to show the select tool
4770
- * @public
4771
- */
4772
- showSelectTool = (flag: boolean): void => {
4773
- this.display.showSelectTool(flag);
4774
- };
4775
-
4776
- /**
4777
- * Show/hide the explode tool.
4778
- * @param flag - whether to show the explode tool
4779
- * @public
4780
- */
4781
- showExplodeTool = (flag: boolean): void => {
4782
- this.display.showExplodeTool(flag);
4783
- };
4784
-
4785
- /**
4786
- * Show/hide the z-scale tool.
4787
- * @param flag - whether to show the z-scale tool
4788
- * @public
4789
- */
4790
- showZScaleTool = (flag: boolean): void => {
4791
- this.display.showZScaleTool(flag);
4792
- };
4793
-
4794
- /**
4795
- * Get the canvas DOM element.
4796
- * @returns The canvas element
4797
- * @public
4798
- */
4799
- getCanvas = (): Element => {
4800
- return this.display.getCanvas();
4801
- };
4802
-
4803
- // ---------------------------------------------------------------------------
4804
- // THREE.js Helper Factories
4805
- // ---------------------------------------------------------------------------
4806
-
4807
- vector3(x: number = 0, y: number = 0, z: number = 0): THREE.Vector3 {
4808
- return new THREE.Vector3(x, y, z);
4809
- }
4810
-
4811
- quaternion(
4812
- x: number = 0,
4813
- y: number = 0,
4814
- z: number = 0,
4815
- w: number = 1,
4816
- ): THREE.Quaternion {
4817
- return new THREE.Quaternion(x, y, z, w);
4818
- }
4819
- }
4820
-
4821
- export { Viewer };