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.
- package/dist/scene/clipping.d.ts +6 -0
- package/dist/three-cad-viewer.esm.js +20 -5
- package/dist/three-cad-viewer.esm.js.map +1 -1
- package/dist/three-cad-viewer.esm.min.js +1 -1
- package/dist/three-cad-viewer.js +20 -5
- package/dist/three-cad-viewer.min.js +1 -1
- package/package.json +2 -3
- package/src/_version.ts +0 -1
- package/src/camera/camera.ts +0 -445
- package/src/camera/controls/CADOrbitControls.ts +0 -241
- package/src/camera/controls/CADTrackballControls.ts +0 -598
- package/src/camera/controls.ts +0 -380
- package/src/core/patches.ts +0 -16
- package/src/core/studio-manager.ts +0 -652
- package/src/core/types.ts +0 -892
- package/src/core/viewer-state.ts +0 -784
- package/src/core/viewer.ts +0 -4821
- package/src/index.ts +0 -151
- package/src/rendering/environment.ts +0 -840
- package/src/rendering/light-detection.ts +0 -327
- package/src/rendering/material-factory.ts +0 -735
- package/src/rendering/material-presets.ts +0 -289
- package/src/rendering/raycast.ts +0 -291
- package/src/rendering/room-environment.ts +0 -192
- package/src/rendering/studio-composer.ts +0 -577
- package/src/rendering/studio-floor.ts +0 -108
- package/src/rendering/texture-cache.ts +0 -324
- package/src/rendering/tree-model.ts +0 -542
- package/src/rendering/triplanar.ts +0 -329
- package/src/scene/animation.ts +0 -343
- package/src/scene/axes.ts +0 -108
- package/src/scene/bbox.ts +0 -223
- package/src/scene/clipping.ts +0 -640
- package/src/scene/grid.ts +0 -864
- package/src/scene/nestedgroup.ts +0 -1444
- package/src/scene/objectgroup.ts +0 -866
- package/src/scene/orientation.ts +0 -259
- package/src/scene/render-shape.ts +0 -634
- package/src/tools/cad_tools/measure.ts +0 -811
- package/src/tools/cad_tools/select.ts +0 -100
- package/src/tools/cad_tools/tools.ts +0 -231
- package/src/tools/cad_tools/ui.ts +0 -454
- package/src/tools/cad_tools/zebra.ts +0 -369
- package/src/types/html.d.ts +0 -5
- package/src/types/n8ao.d.ts +0 -28
- package/src/types/three-augmentation.d.ts +0 -60
- package/src/ui/display.ts +0 -3295
- package/src/ui/index.html +0 -505
- package/src/ui/info.ts +0 -177
- package/src/ui/slider.ts +0 -206
- package/src/ui/toolbar.ts +0 -347
- package/src/ui/treeview.ts +0 -945
- package/src/utils/decode-instances.ts +0 -233
- package/src/utils/font.ts +0 -60
- package/src/utils/gpu-tracker.ts +0 -265
- package/src/utils/logger.ts +0 -92
- package/src/utils/sizeof.ts +0 -116
- package/src/utils/timer.ts +0 -69
- package/src/utils/utils.ts +0 -446
package/src/core/viewer.ts
DELETED
|
@@ -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 };
|