three-cad-viewer 4.3.4 → 4.3.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/dist/scene/clipping.d.ts +6 -0
  2. package/dist/three-cad-viewer.esm.js +20 -5
  3. package/dist/three-cad-viewer.esm.js.map +1 -1
  4. package/dist/three-cad-viewer.esm.min.js +1 -1
  5. package/dist/three-cad-viewer.js +20 -5
  6. package/dist/three-cad-viewer.min.js +1 -1
  7. package/package.json +2 -3
  8. package/src/_version.ts +0 -1
  9. package/src/camera/camera.ts +0 -445
  10. package/src/camera/controls/CADOrbitControls.ts +0 -241
  11. package/src/camera/controls/CADTrackballControls.ts +0 -598
  12. package/src/camera/controls.ts +0 -380
  13. package/src/core/patches.ts +0 -16
  14. package/src/core/studio-manager.ts +0 -652
  15. package/src/core/types.ts +0 -892
  16. package/src/core/viewer-state.ts +0 -784
  17. package/src/core/viewer.ts +0 -4821
  18. package/src/index.ts +0 -151
  19. package/src/rendering/environment.ts +0 -840
  20. package/src/rendering/light-detection.ts +0 -327
  21. package/src/rendering/material-factory.ts +0 -735
  22. package/src/rendering/material-presets.ts +0 -289
  23. package/src/rendering/raycast.ts +0 -291
  24. package/src/rendering/room-environment.ts +0 -192
  25. package/src/rendering/studio-composer.ts +0 -577
  26. package/src/rendering/studio-floor.ts +0 -108
  27. package/src/rendering/texture-cache.ts +0 -324
  28. package/src/rendering/tree-model.ts +0 -542
  29. package/src/rendering/triplanar.ts +0 -329
  30. package/src/scene/animation.ts +0 -343
  31. package/src/scene/axes.ts +0 -108
  32. package/src/scene/bbox.ts +0 -223
  33. package/src/scene/clipping.ts +0 -640
  34. package/src/scene/grid.ts +0 -864
  35. package/src/scene/nestedgroup.ts +0 -1444
  36. package/src/scene/objectgroup.ts +0 -866
  37. package/src/scene/orientation.ts +0 -259
  38. package/src/scene/render-shape.ts +0 -634
  39. package/src/tools/cad_tools/measure.ts +0 -811
  40. package/src/tools/cad_tools/select.ts +0 -100
  41. package/src/tools/cad_tools/tools.ts +0 -231
  42. package/src/tools/cad_tools/ui.ts +0 -454
  43. package/src/tools/cad_tools/zebra.ts +0 -369
  44. package/src/types/html.d.ts +0 -5
  45. package/src/types/n8ao.d.ts +0 -28
  46. package/src/types/three-augmentation.d.ts +0 -60
  47. package/src/ui/display.ts +0 -3295
  48. package/src/ui/index.html +0 -505
  49. package/src/ui/info.ts +0 -177
  50. package/src/ui/slider.ts +0 -206
  51. package/src/ui/toolbar.ts +0 -347
  52. package/src/ui/treeview.ts +0 -945
  53. package/src/utils/decode-instances.ts +0 -233
  54. package/src/utils/font.ts +0 -60
  55. package/src/utils/gpu-tracker.ts +0 -265
  56. package/src/utils/logger.ts +0 -92
  57. package/src/utils/sizeof.ts +0 -116
  58. package/src/utils/timer.ts +0 -69
  59. package/src/utils/utils.ts +0 -446
@@ -1,1444 +0,0 @@
1
- import * as THREE from "three";
2
- import { LineSegments2 } from "three/examples/jsm/lines/LineSegments2.js";
3
- import { LineSegmentsGeometry } from "../core/patches.js";
4
- import { VertexNormalsHelper } from "three/examples/jsm/helpers/VertexNormalsHelper.js";
5
- import { BoundingBox } from "./bbox.js";
6
- import { ObjectGroup, isObjectGroup } from "./objectgroup.js";
7
- import { MaterialFactory } from "../rendering/material-factory.js";
8
- import { deepDispose, flatten } from "../utils/utils.js";
9
- import { gpuTracker } from "../utils/gpu-tracker.js";
10
- import type {
11
- ZebraColorScheme,
12
- ZebraMappingMode,
13
- StudioTextureMapping,
14
- Shapes,
15
- ColorValue,
16
- ColoredMaterial,
17
- MaterialAppearance,
18
- MaterialXMaterial,
19
- } from "../core/types";
20
- import { isMaterialXMaterial } from "../core/types";
21
- import { MATERIAL_PRESETS } from "../rendering/material-presets.js";
22
- import { logger } from "../utils/logger.js";
23
- import { TextureCache } from "../rendering/texture-cache.js";
24
- import type { TextureCacheInterface } from "../rendering/material-factory.js";
25
- import { applyTriplanarMapping } from "../rendering/triplanar.js";
26
-
27
- interface ShapeData {
28
- vertices: Float32Array | number[][];
29
- normals: Float32Array | number[][];
30
- triangles: Uint32Array | number[][];
31
- edges?: Float32Array | number[][];
32
- uvs?: Float32Array | number[];
33
- }
34
-
35
- interface EdgeData {
36
- edges: Float32Array | number[][];
37
- }
38
-
39
- interface VertexData {
40
- obj_vertices: Float32Array | number[];
41
- }
42
-
43
- interface PolygonShape {
44
- refs: string[];
45
- matrices?: number[];
46
- height: number;
47
- }
48
-
49
- interface TextureData {
50
- format: string;
51
- data: string;
52
- }
53
-
54
- interface ShapeEntry {
55
- type: string;
56
- shape: ShapeData | EdgeData | VertexData | PolygonShape;
57
- id: string;
58
- name: string;
59
- color?: string | string[];
60
- alpha?: number;
61
- width?: number;
62
- size?: number;
63
- state: number[];
64
- loc?: [[number, number, number], [number, number, number, number]];
65
- renderback?: boolean;
66
- exploded?: boolean;
67
- geomtype?: number | null;
68
- subtype?: string | null;
69
- texture?: { image: TextureData; width: number; height: number };
70
- material?: string;
71
- }
72
-
73
- interface ShapeTree {
74
- id: string;
75
- loc?: [[number, number, number], [number, number, number, number]];
76
- parts: (ShapeEntry | ShapeTree)[];
77
- format?: string;
78
- instances?: Record<string, number[]>;
79
- }
80
-
81
- type GroupsMap = Record<string, ObjectGroup | CompoundGroup>;
82
-
83
- /** Type guard to check if a shape entry has nested parts (is a ShapeTree) */
84
- function isShapeTree(
85
- shape: ShapeEntry | ShapeTree | Shapes,
86
- ): shape is ShapeTree {
87
- return "parts" in shape;
88
- }
89
-
90
- /**
91
- * A THREE.Group for compound geometry that contains ObjectGroups.
92
- * Follows Three.js convention with type identifier and type guard property.
93
- */
94
- class CompoundGroup extends THREE.Group {
95
- /** Type identifier following Three.js convention */
96
- override readonly type = "CompoundGroup";
97
- /** Type guard property following Three.js convention */
98
- readonly isCompoundGroup = true;
99
- }
100
-
101
- /**
102
- * Manages hierarchical 3D geometry rendering from tessellated CAD data.
103
- *
104
- * NestedGroup is the central scene graph manager that:
105
- * - Parses Shapes data into Three.js geometry
106
- * - Creates ObjectGroup instances for individual shapes, edges, and vertices
107
- * - Maintains a flat `groups` map for path-based access
108
- * - Handles materials, transparency, and clipping planes
109
- *
110
- * ## Architecture
111
- * ```
112
- * NestedGroup (manager)
113
- * └── rootGroup (THREE.Group)
114
- * └── CompoundGroup (per assembly)
115
- * └── ObjectGroup (per shape/edge/vertex)
116
- * └── THREE.Mesh / LineSegments2
117
- * ```
118
- *
119
- * ## Key Methods
120
- * - `render()`: Build geometry from Shapes data
121
- * - `setTransparent()`: Toggle transparency mode
122
- * - `setClipPlanes()`: Apply clipping planes
123
- * - `groups[path]`: Access ObjectGroup by path
124
- *
125
- * @internal - This is an internal class used by Viewer
126
- */
127
-
128
- /** Texture field names on MaterialAppearance that require UV coordinates. */
129
- const TEXTURE_FIELDS = [
130
- "map", "normalMap", "aoMap",
131
- "metalnessMap", "roughnessMap", "emissiveMap", "transmissionMap",
132
- "clearcoatMap", "clearcoatRoughnessMap", "clearcoatNormalMap",
133
- "thicknessMap", "specularIntensityMap", "specularColorMap",
134
- "sheenColorMap", "sheenRoughnessMap", "anisotropyMap",
135
- ] as const;
136
-
137
- /** Check whether a resolved MaterialAppearance references any texture. */
138
- function materialHasTexture(def: MaterialAppearance): boolean {
139
- for (const f of TEXTURE_FIELDS) {
140
- if ((def as Record<string, unknown>)[f]) return true;
141
- }
142
- return false;
143
- }
144
-
145
- /** Check whether a threejs-materials entry has texture references. */
146
- function materialXHasTextures(entry: MaterialXMaterial): boolean {
147
- return Object.keys(entry.textures).length > 0;
148
- }
149
-
150
- class NestedGroup {
151
- shapes!: Shapes;
152
- width: number;
153
- height: number;
154
- edgeColor: number;
155
- transparent: boolean;
156
- metalness: number;
157
- roughness: number;
158
- defaultOpacity: number;
159
- normalLen: number;
160
- blackEdges: boolean;
161
- backVisible: boolean;
162
- bb_max: number;
163
- delim: string;
164
- rootGroup: THREE.Group | null;
165
- instances: Record<string, number[]> | null;
166
- bbox: BoundingBox | null;
167
- bsphere: THREE.Sphere | null;
168
- groups!: GroupsMap; // Initialized to {} in constructor
169
- clipPlanes: THREE.Plane[] | null;
170
- materialFactory: MaterialFactory;
171
- materialsTable: Record<string, string | MaterialXMaterial | MaterialAppearance> | null;
172
- resolvedMaterials: Map<string, MaterialAppearance>;
173
- /** Cache for threejs-materials entries resolved from the materials table */
174
- resolvedMaterialX: Map<string, MaterialXMaterial>;
175
- private _textureCache: TextureCache | null;
176
- private _studioMaterialCache: Map<string, THREE.MeshPhysicalMaterial | THREE.MeshBasicMaterial>;
177
- /** Sharing keys of materials that have textures (for UV generation on cache hits) */
178
- private _texturedMaterialKeys: Set<string>;
179
- private _isStudioMode: boolean;
180
-
181
- /**
182
- * Create a NestedGroup for rendering CAD geometry.
183
- * @param shapes - The tessellated shape data to render.
184
- * @param width - Canvas/viewport width for line material resolution.
185
- * @param height - Canvas/viewport height for line material resolution.
186
- * @param edgeColor - Default edge color as hex value (e.g., 0x000000).
187
- * @param transparent - Whether to render shapes with transparency.
188
- * @param opacity - Default opacity value (0.0 to 1.0).
189
- * @param metalness - Material metalness value (0.0 to 1.0).
190
- * @param roughness - Material roughness value (0.0 to 1.0).
191
- * @param normalLen - Length for vertex normal helpers (0 to disable).
192
- * @param bb_max - Maximum bounding box dimension.
193
- */
194
- constructor(
195
- shapes: Shapes,
196
- width: number,
197
- height: number,
198
- edgeColor: number,
199
- transparent: boolean,
200
- opacity: number,
201
- metalness: number,
202
- roughness: number,
203
- normalLen: number,
204
- bb_max: number = 0,
205
- ) {
206
- this.shapes = shapes;
207
- this.width = width;
208
- this.height = height;
209
- this.edgeColor = edgeColor;
210
- this.transparent = transparent;
211
- this.metalness = metalness;
212
- this.roughness = roughness;
213
- this.defaultOpacity = opacity;
214
- this.normalLen = normalLen;
215
- this.blackEdges = false;
216
- this.backVisible = false;
217
- this.bb_max = bb_max;
218
- this.delim = "|";
219
- this.rootGroup = null;
220
- this.instances = null;
221
- this.bbox = null;
222
- this.bsphere = null;
223
- this.groups = {};
224
-
225
- this.clipPlanes = null;
226
-
227
- this.materialsTable = null;
228
- this.resolvedMaterials = new Map();
229
- this.resolvedMaterialX = new Map();
230
- this._textureCache = null;
231
- this._studioMaterialCache = new Map();
232
- this._texturedMaterialKeys = new Set();
233
- this._isStudioMode = false;
234
-
235
- this.materialFactory = new MaterialFactory({
236
- defaultOpacity: opacity,
237
- metalness: metalness,
238
- roughness: roughness,
239
- edgeColor: edgeColor,
240
- transparent: transparent,
241
- });
242
- }
243
-
244
- /**
245
- * Dispose of all resources and clean up memory.
246
- */
247
- dispose(): void {
248
- if (Object.keys(this.groups).length > 0) {
249
- deepDispose(Object.values(this.groups));
250
- this.groups = {};
251
- }
252
- if (this.rootGroup) {
253
- deepDispose(this.rootGroup);
254
- this.rootGroup = null;
255
- }
256
- this._disposeStudioResources();
257
- this.resolvedMaterials.clear();
258
- this.resolvedMaterialX.clear();
259
- this.materialsTable = null;
260
- }
261
-
262
- /**
263
- * Resolve a material tag to its definition.
264
- *
265
- * Returns either a MaterialAppearance (for builtin presets) or a
266
- * MaterialXMaterial (for threejs-materials entries). The caller must check the
267
- * return type to determine which factory method to use.
268
- *
269
- * Resolution order:
270
- * 1. Check caches (resolvedMaterials / resolvedMaterialX)
271
- * 2. Look up in root-level `materials` table:
272
- * - string starting with "builtin:" → MATERIAL_PRESETS lookup
273
- * - object with `properties` key → threejs-materials entry
274
- * 3. Direct lookup in MATERIAL_PRESETS by tag name
275
- * 4. No match → warning, return null
276
- *
277
- * @param tag - The material tag from a leaf node
278
- * @param objectPath - The object path (for warning messages)
279
- * @returns Resolved material definition or null if not found
280
- */
281
- resolveMaterialTag(
282
- tag: string,
283
- objectPath: string,
284
- ): MaterialAppearance | MaterialXMaterial | null {
285
- // Empty string is equivalent to no tag -- skip silently
286
- if (tag === "") {
287
- return null;
288
- }
289
-
290
- // Check caches
291
- const cachedPreset = this.resolvedMaterials.get(tag);
292
- if (cachedPreset !== undefined) return cachedPreset;
293
-
294
- const cachedMX = this.resolvedMaterialX.get(tag);
295
- if (cachedMX !== undefined) return cachedMX;
296
-
297
- // 1. Look up in user-defined materials table
298
- if (this.materialsTable && tag in this.materialsTable) {
299
- const entry = this.materialsTable[tag];
300
-
301
- // String entry: "builtin:<preset-name>"
302
- if (typeof entry === "string") {
303
- if (entry.startsWith("builtin:")) {
304
- const presetName = entry.slice(8);
305
- const preset = MATERIAL_PRESETS[presetName];
306
- if (preset) {
307
- const resolved = { ...preset };
308
- this.resolvedMaterials.set(tag, resolved);
309
- return resolved;
310
- }
311
- logger.warn(
312
- `Unknown builtin preset '${presetName}' referenced by '${tag}' on '${objectPath}'`,
313
- );
314
- return null;
315
- }
316
- logger.warn(
317
- `Invalid material string '${entry}' for tag '${tag}' (expected "builtin:" prefix)`,
318
- );
319
- return null;
320
- }
321
-
322
- // MaterialXMaterial entry: object with `values` key
323
- if (isMaterialXMaterial(entry)) {
324
- this.resolvedMaterialX.set(tag, entry);
325
- return entry;
326
- }
327
-
328
- // MaterialAppearance entry: object with `builtin` key (preset + overrides)
329
- if (typeof entry === "object" && "builtin" in entry) {
330
- const appearance = entry as MaterialAppearance;
331
- const presetName = appearance.builtin!;
332
- const preset = MATERIAL_PRESETS[presetName];
333
- if (!preset) {
334
- logger.warn(
335
- `Unknown builtin preset '${presetName}' referenced by '${tag}' on '${objectPath}'`,
336
- );
337
- return null;
338
- }
339
- const resolved: MaterialAppearance = { ...preset, ...appearance };
340
- this.resolvedMaterials.set(tag, resolved);
341
- return resolved;
342
- }
343
-
344
- // Should not happen with current type, but guard anyway
345
- logger.warn(`Unrecognised material entry for tag '${tag}' on '${objectPath}'`);
346
- return null;
347
- }
348
-
349
- // 2. Direct lookup in built-in presets (leaf tag matches preset name)
350
- const preset = MATERIAL_PRESETS[tag];
351
- if (preset) {
352
- const resolved = { ...preset };
353
- this.resolvedMaterials.set(tag, resolved);
354
- return resolved;
355
- }
356
-
357
- // 3. No match
358
- logger.warn(`Unknown material tag '${tag}' on object '${objectPath}'`);
359
- return null;
360
- }
361
-
362
- /**
363
- * Check if array is nested (number[][]).
364
- */
365
- private _isNestedArray(data: number[] | number[][]): data is number[][] {
366
- return data.length > 0 && Array.isArray(data[0]);
367
- }
368
-
369
- /**
370
- * Convert array data to Float32Array, detecting nested arrays at runtime.
371
- */
372
- private _toFloat32Array(
373
- data: Float32Array | number[] | number[][],
374
- depth: number = 1,
375
- ): Float32Array {
376
- if (data instanceof Float32Array) {
377
- return data;
378
- }
379
- if (this._isNestedArray(data)) {
380
- return new Float32Array(flatten(data, depth));
381
- }
382
- return new Float32Array(data);
383
- }
384
-
385
- /**
386
- * Convert array data to Uint32Array, detecting nested arrays at runtime.
387
- */
388
- private _toUint32Array(
389
- data: Uint32Array | number[] | number[][],
390
- depth: number = 1,
391
- ): Uint32Array {
392
- if (data instanceof Uint32Array) {
393
- return data;
394
- }
395
- if (this._isNestedArray(data)) {
396
- return new Uint32Array(flatten(data, depth));
397
- }
398
- return new Uint32Array(data);
399
- }
400
-
401
- /**
402
- * Internal method to render edge geometry as fat lines.
403
- */
404
- private _renderEdges(
405
- edgeList: Float32Array | number[] | number[][],
406
- lineWidth: number,
407
- color: ColorValue | ColorValue[] | null,
408
- state: number,
409
- label?: string,
410
- ): LineSegments2 {
411
- const positions = this._toFloat32Array(edgeList, 3);
412
-
413
- const lineGeometry = gpuTracker.trackGeometry(
414
- new LineSegmentsGeometry(),
415
- label
416
- ? `LineSegmentsGeometry for ${label}`
417
- : "LineSegmentsGeometry (edges)",
418
- );
419
- lineGeometry.setPositions(positions);
420
-
421
- // Handle vertex colors for multi-colored edges (e.g., trihedron axes)
422
- if (Array.isArray(color)) {
423
- const colors = color
424
- .map((c) => {
425
- const col = new THREE.Color(c);
426
- return [col.r, col.g, col.b, col.r, col.g, col.b];
427
- })
428
- .flat();
429
- lineGeometry.setColors(new Float32Array(colors));
430
- }
431
-
432
- const lineMaterial = this.materialFactory.createEdgeMaterial(
433
- {
434
- lineWidth,
435
- color: Array.isArray(color) ? null : (color ?? this.edgeColor),
436
- vertexColors: Array.isArray(color),
437
- visible: state == 1,
438
- resolution: { width: this.width, height: this.height },
439
- },
440
- label ? `LineMaterial for ${label}` : "LineMaterial (edges)",
441
- );
442
-
443
- const edges = new LineSegments2(lineGeometry, lineMaterial);
444
- edges.renderOrder = 999;
445
-
446
- return edges;
447
- }
448
-
449
- /**
450
- * Render standalone edge geometry (not associated with a face).
451
- */
452
- renderEdges(
453
- edgeData: EdgeData,
454
- lineWidth: number,
455
- color: ColorValue | ColorValue[] | null,
456
- path: string,
457
- name: string,
458
- state: number,
459
- geomtype: { topo: string; geomtype: number | string | null } | null = null,
460
- ): ObjectGroup {
461
- // For vertex colors (array), use default edge color for the group
462
- const groupColor = Array.isArray(color)
463
- ? this.edgeColor
464
- : (color ?? this.edgeColor);
465
- const group = new ObjectGroup(
466
- this.defaultOpacity,
467
- 1.0,
468
- groupColor,
469
- geomtype,
470
- "edges",
471
- );
472
-
473
- const edges = this._renderEdges(
474
- edgeData.edges,
475
- lineWidth,
476
- color,
477
- state,
478
- path,
479
- );
480
- if (name) {
481
- edges.name = name;
482
- }
483
- group.setEdges(edges);
484
-
485
- this.groups[path] = group;
486
- group.name = path.replaceAll("/", this.delim);
487
-
488
- return group;
489
- }
490
-
491
- /**
492
- * Render vertex points as a point cloud.
493
- */
494
- renderVertices(
495
- vertexData: VertexData,
496
- size: number,
497
- color: ColorValue | null,
498
- path: string,
499
- name: string,
500
- state: number,
501
- geomtype: { topo: string; geomtype: number | string | null } | null = null,
502
- ): ObjectGroup {
503
- const group = new ObjectGroup(
504
- this.defaultOpacity,
505
- 1.0,
506
- color ?? this.edgeColor,
507
- geomtype,
508
- "vertices",
509
- );
510
-
511
- const positions = this._toFloat32Array(vertexData.obj_vertices);
512
-
513
- const geometry = gpuTracker.trackGeometry(
514
- new THREE.BufferGeometry(),
515
- `BufferGeometry (vertices) for ${path}`,
516
- );
517
- geometry.setAttribute(
518
- "position",
519
- new THREE.Float32BufferAttribute(positions, 3),
520
- );
521
-
522
- const material = this.materialFactory.createVertexMaterial(
523
- {
524
- size: size,
525
- color: color,
526
- visible: state == 1,
527
- },
528
- `PointsMaterial for ${path}`,
529
- );
530
-
531
- const points = new THREE.Points(geometry, material);
532
- if (name) {
533
- points.name = name;
534
- }
535
- group.setVertices(points);
536
-
537
- this.groups[path] = group;
538
- group.name = path.replaceAll("/", this.delim);
539
-
540
- return group;
541
- }
542
-
543
- /**
544
- * Render a tessellated 3D shape with front/back faces and optional edges.
545
- */
546
- renderShape(
547
- shape: ShapeData,
548
- color: ColorValue,
549
- alpha: number | null,
550
- renderback: boolean,
551
- exploded: boolean,
552
- path: string,
553
- name: string,
554
- states: number[],
555
- geomtype: { topo: string; geomtype: number | string | null } | null = null,
556
- subtype: string | null = null,
557
- texture_data: TextureData | null = null,
558
- texture_width: number | null = null,
559
- texture_height: number | null = null,
560
- ): ObjectGroup {
561
- const positions = this._toFloat32Array(shape.vertices);
562
- const normals = this._toFloat32Array(shape.normals);
563
- const triangles = this._toUint32Array(shape.triangles);
564
-
565
- const group = new ObjectGroup(
566
- this.defaultOpacity,
567
- alpha ?? 1.0,
568
- this.edgeColor,
569
- geomtype,
570
- subtype,
571
- renderback,
572
- );
573
-
574
- this.groups[path] = group;
575
- group.name = path.replaceAll("/", this.delim);
576
-
577
- if (alpha == null) {
578
- alpha = 1.0;
579
- } else if (alpha < 1.0) {
580
- this.transparent = true;
581
- }
582
-
583
- let shapeGeometry: THREE.BufferGeometry | THREE.PlaneGeometry;
584
- let texture: THREE.Texture | null = null;
585
- let frontMaterial: ColoredMaterial;
586
-
587
- if (texture_data != null) {
588
- const url = `data:image/${texture_data.format};base64,${texture_data.data}`;
589
- const img = new Image();
590
- shapeGeometry = gpuTracker.trackGeometry(
591
- new THREE.PlaneGeometry(texture_width!, texture_height!),
592
- `PlaneGeometry (textured) for ${path}`,
593
- );
594
-
595
- texture = gpuTracker.trackTexture(
596
- new THREE.Texture(img),
597
- `Texture for ${path}`,
598
- );
599
- texture.colorSpace = THREE.SRGBColorSpace;
600
-
601
- // Set src after texture is created, and mark needsUpdate in onload handler
602
- // to avoid "Texture marked for update but image is incomplete" warning
603
- img.onload = () => {
604
- texture!.needsUpdate = true;
605
- };
606
- img.src = url;
607
-
608
- frontMaterial = this.materialFactory.createTextureMaterial(
609
- { texture },
610
- `MeshBasicMaterial (textured) for ${path}`,
611
- );
612
- renderback = false;
613
- } else {
614
- shapeGeometry = gpuTracker.trackGeometry(
615
- new THREE.BufferGeometry(),
616
- `BufferGeometry (shape) for ${path}`,
617
- );
618
- shapeGeometry.setAttribute(
619
- "position",
620
- new THREE.BufferAttribute(positions, 3),
621
- );
622
- shapeGeometry.setAttribute(
623
- "normal",
624
- new THREE.BufferAttribute(normals, 3),
625
- );
626
- shapeGeometry.setIndex(new THREE.BufferAttribute(triangles, 1));
627
- if (shape.uvs && shape.uvs.length > 0) {
628
- const uvArray = shape.uvs instanceof Float32Array
629
- ? shape.uvs
630
- : new Float32Array(shape.uvs);
631
- shapeGeometry.setAttribute(
632
- "uv",
633
- new THREE.BufferAttribute(uvArray, 2),
634
- );
635
- }
636
- group.shapeGeometry = shapeGeometry;
637
-
638
- frontMaterial = this.materialFactory.createFrontFaceMaterial(
639
- {
640
- color: color,
641
- alpha: alpha,
642
- visible: states[0] == 1,
643
- },
644
- `MeshStandardMaterial (front) for ${path}`,
645
- );
646
- frontMaterial.name = "frontMaterial";
647
- }
648
-
649
- const backColor =
650
- group.subtype === "solid" && !exploded
651
- ? color
652
- : new THREE.Color(this.edgeColor)
653
- .lerp(new THREE.Color(1, 1, 1), 0.15)
654
- .getHex();
655
-
656
- const backMaterial = this.materialFactory.createBackFaceBasicMaterial(
657
- {
658
- color: backColor,
659
- alpha: alpha,
660
- visible: states[0] == 1 && (renderback || this.backVisible),
661
- },
662
- `MeshBasicMaterial (back) for ${path}`,
663
- );
664
- backMaterial.name = "backMaterial";
665
-
666
- const back = new THREE.Mesh(shapeGeometry, backMaterial);
667
- back.name = name;
668
-
669
- const front = new THREE.Mesh(shapeGeometry, frontMaterial);
670
- front.name = name;
671
-
672
- // ensure, transparent objects will be rendered at the end
673
- if (alpha < 1.0) {
674
- back.renderOrder = 999;
675
- front.renderOrder = 999;
676
- }
677
-
678
- if (front.geometry.boundingBox == null) {
679
- front.geometry.computeBoundingBox();
680
- }
681
-
682
- group.setBack(back);
683
- group.setFront(front);
684
-
685
- if (this.normalLen > 0) {
686
- const normalsHelper = new VertexNormalsHelper(
687
- front,
688
- this.normalLen,
689
- 0xff00ff,
690
- );
691
- group.add(normalsHelper);
692
- }
693
-
694
- const edgeList = shape.edges;
695
- if (edgeList && edgeList.length > 0) {
696
- const edges = this._renderEdges(edgeList, 1, null, states[1], path);
697
- edges.name = name;
698
- group.setEdges(edges);
699
- }
700
-
701
- return group;
702
- }
703
-
704
- /**
705
- * Create edge geometry from extruded polygons.
706
- */
707
- private _createEdgesFromPolygons(
708
- polygons: THREE.Shape[],
709
- depth: number,
710
- ): THREE.BufferGeometry {
711
- const vertices: THREE.Vector3[] = [];
712
- const indices: number[] = [];
713
- let vertexOffset = 0;
714
-
715
- for (let j = 0; j < polygons.length; j++) {
716
- const polygon = polygons[j];
717
- const points = polygon.getPoints(); // Get 2D polygon points
718
- const bottomPoints = points.map((p) => new THREE.Vector3(p.x, p.y, 0));
719
- const topPoints = points.map((p) => new THREE.Vector3(p.x, p.y, depth));
720
-
721
- // Add bottom and top perimeter edges
722
- const addPerimeter = (perimeterPoints: THREE.Vector3[]) => {
723
- for (let i = 0; i < perimeterPoints.length; i++) {
724
- const nextIndex = (i + 1) % perimeterPoints.length;
725
- indices.push(vertexOffset + i, vertexOffset + nextIndex);
726
- }
727
- vertices.push(...perimeterPoints);
728
- vertexOffset += perimeterPoints.length;
729
- };
730
-
731
- addPerimeter(bottomPoints);
732
- addPerimeter(topPoints);
733
-
734
- // Add vertical edges between corresponding points
735
- for (let i = 0; i < points.length; i++) {
736
- indices.push(
737
- vertexOffset - 2 * points.length + i, // Bottom point index
738
- vertexOffset - points.length + i, // Top point index
739
- );
740
- }
741
- }
742
-
743
- const geometry = new THREE.BufferGeometry();
744
- geometry.setFromPoints(vertices);
745
- geometry.setIndex(indices);
746
-
747
- return geometry;
748
- }
749
-
750
- /**
751
- * Render extruded 2D polygons (GDS format) as 3D geometry.
752
- */
753
- renderPolygons(
754
- shape: PolygonShape,
755
- minZ: number,
756
- color: ColorValue,
757
- alpha: number,
758
- renderback: boolean,
759
- _exploded: boolean,
760
- path: string,
761
- name: string,
762
- states: number[],
763
- geomtype: { topo: string; geomtype: number | string | null } | null = null,
764
- subtype: string | null = null,
765
- ): ObjectGroup {
766
- const group = new ObjectGroup(
767
- this.defaultOpacity,
768
- 1.0,
769
- this.edgeColor,
770
- geomtype,
771
- subtype,
772
- renderback,
773
- );
774
- group.name = path.replaceAll("/", this.delim);
775
- group.minZ = minZ;
776
- group.height = shape.height;
777
-
778
- this.groups[path] = group;
779
-
780
- const polygons: THREE.Shape[] = [];
781
- let matrices: number[];
782
- if (shape.matrices && shape.matrices.length > 0) {
783
- matrices = shape.matrices;
784
- } else {
785
- matrices = [1, 0, 0, 0, 1, 0];
786
- }
787
- for (const ref of shape.refs) {
788
- const vertices = this.instances![ref];
789
- const n = vertices.length / 2;
790
- const points = new Array<THREE.Vector2>(n);
791
- for (let i = 0; i < matrices.length / 6; i++) {
792
- const a = matrices[6 * i];
793
- const b = matrices[6 * i + 1];
794
- const x = matrices[6 * i + 2];
795
- const d = matrices[6 * i + 3];
796
- const e = matrices[6 * i + 4];
797
- const y = matrices[6 * i + 5];
798
- for (let j = 0; j < n; j++) {
799
- points[j] = new THREE.Vector2(
800
- a * vertices[2 * j] + b * vertices[2 * j + 1] + x,
801
- d * vertices[2 * j] + e * vertices[2 * j + 1] + y,
802
- );
803
- }
804
-
805
- const polygon = new THREE.Shape(points);
806
- polygons.push(polygon);
807
- }
808
- }
809
-
810
- const extrudeSettings = {
811
- depth: shape.height,
812
- bevelEnabled: false,
813
- };
814
- const polyGeometry = gpuTracker.trackGeometry(
815
- new THREE.ExtrudeGeometry(polygons, extrudeSettings),
816
- `ExtrudeGeometry (polygon) for ${path}`,
817
- );
818
-
819
- const frontMaterial = this.materialFactory.createFrontFaceMaterial(
820
- {
821
- color: color,
822
- alpha: alpha,
823
- visible: states[0] == 1,
824
- },
825
- `MeshStandardMaterial (front polygon) for ${path}`,
826
- );
827
- frontMaterial.name = "frontMaterial";
828
-
829
- const backMaterial = this.materialFactory.createBackFaceStandardMaterial(
830
- {
831
- color: color,
832
- alpha: alpha,
833
- visible: states[0] == 1 && (renderback || this.backVisible),
834
- },
835
- `MeshStandardMaterial (back polygon) for ${path}`,
836
- );
837
- backMaterial.name = "backMaterial";
838
-
839
- const back = new THREE.Mesh(polyGeometry, backMaterial);
840
- back.name = name;
841
- const front = new THREE.Mesh(polyGeometry, frontMaterial);
842
- front.name = name;
843
-
844
- // Edges
845
- const edgeGeom = gpuTracker.trackGeometry(
846
- this._createEdgesFromPolygons(polygons, shape.height),
847
- `BufferGeometry (polygon edges) for ${path}`,
848
- );
849
-
850
- const lineMat = this.materialFactory.createSimpleEdgeMaterial(
851
- {},
852
- `LineBasicMaterial (polygon edges) for ${path}`,
853
- );
854
-
855
- const polyEdges = new THREE.LineSegments(edgeGeom, lineMat);
856
-
857
- group.shapeGeometry = polyGeometry;
858
- group.setFront(front);
859
- group.setBack(back);
860
- group.setEdges(polyEdges);
861
-
862
- return group;
863
- }
864
-
865
- /**
866
- * Recursively render all shapes in the shape tree.
867
- * Note: The shapes parameter uses the public Shapes type but internally
868
- * contains ShapeEntry/ShapeTree data after decomposition by viewer._decompose()
869
- */
870
- renderLoop(shapes: Shapes): THREE.Group {
871
- const _render = (
872
- shape: ShapeEntry,
873
- texture: TextureData | null,
874
- width: number | null,
875
- height: number | null,
876
- ): ObjectGroup => {
877
- let mesh: ObjectGroup;
878
- switch (shape.type) {
879
- case "edges":
880
- mesh = this.renderEdges(
881
- shape.shape as EdgeData,
882
- shape.width!,
883
- shape.color as ColorValue | ColorValue[] | null,
884
- shape.id,
885
- shape.name,
886
- shape.state[1],
887
- { topo: "edge", geomtype: shape.geomtype || null },
888
- );
889
- break;
890
- case "vertices":
891
- mesh = this.renderVertices(
892
- shape.shape as VertexData,
893
- shape.size!,
894
- (shape.color as ColorValue) ?? null,
895
- shape.id,
896
- shape.name,
897
- shape.state[1],
898
- { topo: "vertex", geomtype: null },
899
- );
900
- break;
901
- case "polygon":
902
- mesh = this.renderPolygons(
903
- shape.shape as PolygonShape,
904
- shape.loc![0][2],
905
- (shape.color as ColorValue) ?? this.edgeColor,
906
- 1.0,
907
- shape.renderback == null ? false : shape.renderback,
908
- false, //exploded
909
- shape.id,
910
- shape.name,
911
- shape.state,
912
- { topo: "face", geomtype: shape.geomtype || null },
913
- shape.subtype || null,
914
- );
915
- break;
916
- default: {
917
- // Shape color must be a single value, not an array
918
- const shapeColor = Array.isArray(shape.color)
919
- ? shape.color[0]
920
- : shape.color;
921
- mesh = this.renderShape(
922
- shape.shape as ShapeData,
923
- shapeColor ?? this.edgeColor,
924
- shape.alpha ?? null,
925
- shape.renderback == null ? false : shape.renderback,
926
- shape.exploded ?? false,
927
- shape.id,
928
- shape.name,
929
- shape.state,
930
- { topo: "face", geomtype: shape.geomtype || null },
931
- shape.subtype || null,
932
- texture,
933
- width,
934
- height,
935
- );
936
- }
937
- }
938
- // support object locations
939
- if (shape.loc != null) {
940
- mesh.position.set(...shape.loc[0]);
941
- mesh.quaternion.set(...shape.loc[1]);
942
- }
943
- return mesh;
944
- };
945
-
946
- const group = new CompoundGroup();
947
- if (shapes.loc == null) {
948
- shapes.loc = [
949
- [0.0, 0.0, 0.0],
950
- [0.0, 0.0, 0.0, 1.0],
951
- ];
952
- }
953
- group.position.set(...shapes.loc[0]);
954
- group.quaternion.set(...shapes.loc[1]);
955
-
956
- this.groups[shapes.id] = group;
957
- group.name = shapes.id.replaceAll("/", "|");
958
-
959
- // shapes.parts contains ShapeEntry | ShapeTree after viewer._decompose()
960
- for (const shape of shapes.parts!) {
961
- if (isShapeTree(shape)) {
962
- group.add(this.renderLoop(shape));
963
- } else {
964
- const entry = shape as ShapeEntry;
965
- // Propagate material tag from shapes data to local ShapeEntry
966
- const materialTag = (shape as Shapes).material;
967
- if (materialTag != null) {
968
- entry.material = materialTag;
969
- }
970
- const has_texture = entry.texture != null;
971
- const texture = has_texture ? entry.texture!.image : null;
972
- const width = has_texture ? entry.texture!.width : null;
973
- const height = has_texture ? entry.texture!.height : null;
974
- const objectGroup = _render(entry, texture, width, height);
975
- this.groups[entry.id] = objectGroup;
976
- // Store material tag on ObjectGroup for Studio mode lookup
977
- if (entry.material !== undefined && entry.material !== null) {
978
- objectGroup.materialTag = entry.material;
979
- }
980
- group.add(objectGroup);
981
- }
982
- }
983
- return group;
984
- }
985
-
986
- /**
987
- * Main entry point to render all shapes.
988
- */
989
- render(): THREE.Group {
990
- if (this.shapes.format == "GDS") {
991
- this.instances = this.shapes.instances || null;
992
- }
993
- this.materialsTable = this.shapes.materials || null;
994
- this.resolvedMaterials.clear();
995
- this.resolvedMaterialX.clear();
996
- this.rootGroup = this.renderLoop(this.shapes);
997
- return this.rootGroup;
998
- }
999
-
1000
- /**
1001
- * Get the bounding box of all rendered geometry.
1002
- */
1003
- boundingBox(): BoundingBox {
1004
- if (this.bbox == null) {
1005
- this.bbox = new BoundingBox();
1006
- this.bbox.setFromObject(this.rootGroup!, false);
1007
- }
1008
- return this.bbox;
1009
- }
1010
-
1011
- /**
1012
- * Traverse all ObjectGroup instances and call a method on each.
1013
- * Note: Uses dynamic dispatch for methods that exist on ObjectGroup.
1014
- */
1015
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
1016
- private _traverse(func: string, flag?: any): void {
1017
- for (const path in this.groups) {
1018
- const obj = this.groups[path];
1019
- if (obj instanceof ObjectGroup) {
1020
- const method = obj[func];
1021
- if (typeof method === "function") {
1022
- method.call(obj, flag);
1023
- }
1024
- }
1025
- }
1026
- }
1027
-
1028
- /**
1029
- * Get all currently selected ObjectGroup instances.
1030
- */
1031
- selection(): ObjectGroup[] {
1032
- const result: ObjectGroup[] = [];
1033
- for (const path in this.groups) {
1034
- for (const obj of this.groups[path].children) {
1035
- if (obj instanceof ObjectGroup) {
1036
- if (obj.isSelected) {
1037
- result.push(obj);
1038
- }
1039
- }
1040
- }
1041
- }
1042
- return result;
1043
- }
1044
-
1045
- /**
1046
- * Clear selection and highlights from all selected objects.
1047
- */
1048
- clearSelection(): void {
1049
- for (const object of this.selection()) {
1050
- object.clearHighlights();
1051
- }
1052
- }
1053
-
1054
- /**
1055
- * Set metalness value for all materials.
1056
- */
1057
- setMetalness(value: number): void {
1058
- this.metalness = value;
1059
- this.materialFactory.update({ metalness: value });
1060
- this._traverse("setMetalness", value);
1061
- }
1062
-
1063
- /**
1064
- * Set roughness value for all materials.
1065
- */
1066
- setRoughness(value: number): void {
1067
- this.roughness = value;
1068
- this.materialFactory.update({ roughness: value });
1069
- this._traverse("setRoughness", value);
1070
- }
1071
-
1072
- /**
1073
- * Enable or disable transparency for all shapes.
1074
- */
1075
- setTransparent(flag: boolean): void {
1076
- this.transparent = flag;
1077
- this.materialFactory.update({ transparent: flag });
1078
- this._traverse("setTransparent", flag);
1079
- }
1080
-
1081
- /**
1082
- * Set whether edges should be rendered in black.
1083
- */
1084
- setBlackEdges(flag: boolean): void {
1085
- this.blackEdges = flag;
1086
- this._traverse("setBlackEdges", flag);
1087
- }
1088
-
1089
- /**
1090
- * Set visibility of back faces.
1091
- */
1092
- setBackVisible(flag: boolean): void {
1093
- this.backVisible = flag;
1094
- this._traverse("setBackVisible", flag);
1095
- }
1096
-
1097
- /**
1098
- * Set the edge color for all shapes.
1099
- */
1100
- setEdgeColor(color: number): void {
1101
- this.edgeColor = color;
1102
- this._traverse("setEdgeColor", color);
1103
- }
1104
-
1105
- /**
1106
- * Set the opacity for all shapes.
1107
- */
1108
- setOpacity(opacity: number): void {
1109
- this.defaultOpacity = opacity;
1110
- this._traverse("setOpacity", opacity);
1111
- }
1112
-
1113
- /**
1114
- * Set clip intersection mode for all materials.
1115
- */
1116
- setClipIntersection(flag: boolean): void {
1117
- this._traverse("setClipIntersection", flag);
1118
- }
1119
-
1120
- /**
1121
- * Set clipping planes for all materials.
1122
- */
1123
- setClipPlanes(planes: THREE.Plane[]): void {
1124
- this.clipPlanes = planes;
1125
- this._traverse("setClipPlanes", planes);
1126
- }
1127
-
1128
- /**
1129
- * Set polygon offset for depth sorting.
1130
- */
1131
- setPolygonOffset(offset: number): void {
1132
- this._traverse("setPolygonOffset", offset);
1133
- }
1134
-
1135
- /**
1136
- * Set Z-axis scale for all shapes (used for GDS extrusion visualization).
1137
- */
1138
- setZScale(value: number): void {
1139
- this._traverse("setZScale", value);
1140
- }
1141
-
1142
- /**
1143
- * Reset minimum Z position for all shapes.
1144
- */
1145
- setMinZ(): void {
1146
- this._traverse("setMinZ");
1147
- }
1148
-
1149
- /**
1150
- * Mark all materials as needing update.
1151
- */
1152
- updateMaterials(): void {
1153
- this._traverse("updateMaterials", true);
1154
- }
1155
-
1156
- /**
1157
- * Enable or disable zebra stripe visualization.
1158
- */
1159
- setZebra(flag: boolean): void {
1160
- this._traverse("setZebra", flag);
1161
- }
1162
-
1163
- /**
1164
- * Set the number of zebra stripes.
1165
- */
1166
- setZebraCount(value: number): void {
1167
- this._traverse("setZebraCount", value);
1168
- }
1169
-
1170
- /**
1171
- * Set the opacity of zebra stripes.
1172
- */
1173
- setZebraOpacity(value: number): void {
1174
- this._traverse("setZebraOpacity", value);
1175
- }
1176
-
1177
- /**
1178
- * Set the direction/angle of zebra stripes.
1179
- */
1180
- setZebraDirection(value: number): void {
1181
- this._traverse("setZebraDirection", value);
1182
- }
1183
-
1184
- /**
1185
- * Set the color scheme for zebra stripes.
1186
- */
1187
- setZebraColorScheme(flag: ZebraColorScheme): void {
1188
- this._traverse("setZebraColorScheme", flag);
1189
- }
1190
-
1191
- /**
1192
- * Set the mapping mode for zebra stripes.
1193
- */
1194
- setZebraMappingMode(flag: ZebraMappingMode): void {
1195
- this._traverse("setZebraMappingMode", flag);
1196
- }
1197
-
1198
- // ===========================================================================
1199
- // Studio Mode
1200
- // ===========================================================================
1201
-
1202
- /**
1203
- * Enter Studio mode: build and apply studio materials to all ObjectGroups.
1204
- *
1205
- * Material resolution per ObjectGroup:
1206
- * 1. Resolve the material tag via `resolveMaterialTag()`
1207
- * - MaterialXMaterial → `createStudioMaterialFromMaterialX`
1208
- * - MaterialAppearance → `createStudioMaterial` (builtin presets)
1209
- * - null (no tag) → fallback plastic-glossy preset tinted with CAD color
1210
- * 2. Cache by sharing key for reuse across objects with the same tag+color
1211
- * 3. Clone BackSide variant for renderback objects
1212
- * 4. Auto-generate box-projected UVs when textured but geometry has no UVs
1213
- */
1214
- async enterStudioMode(textureMapping: StudioTextureMapping = "triplanar"): Promise<string[]> {
1215
- // Create TextureCache lazily
1216
- if (!this._textureCache) {
1217
- this._textureCache = new TextureCache();
1218
- }
1219
- // Track material tags that failed to resolve
1220
- const unresolvedTags = new Set<string>();
1221
-
1222
- // Iterate all ObjectGroups with front meshes
1223
- for (const path in this.groups) {
1224
- const obj = this.groups[path];
1225
- if (!(obj instanceof ObjectGroup)) continue;
1226
- if (!obj.front) continue;
1227
-
1228
- // Determine material tag, leaf color, and leaf alpha
1229
- const tag = obj.materialTag || "";
1230
- const leafColor = obj.originalColor
1231
- ? "#" + obj.originalColor.getHexString()
1232
- : "#707070";
1233
- const leafAlpha = obj.alpha;
1234
-
1235
- // Compute sharing key
1236
- const sharingKey = `${tag}:${leafColor}:${leafAlpha}`;
1237
-
1238
- // Check cached material for this key
1239
- let studioMaterial = this._studioMaterialCache.get(sharingKey);
1240
-
1241
- if (!studioMaterial) {
1242
- // Resolve the tag
1243
- const resolved = tag ? this.resolveMaterialTag(tag, path) : null;
1244
- if (tag && !resolved) {
1245
- unresolvedTags.add(tag);
1246
- }
1247
-
1248
- // Per-object try/catch: a single failure should not abort the rest
1249
- try {
1250
- if (resolved && isMaterialXMaterial(resolved)) {
1251
- // --- threejs-materials path ---
1252
- studioMaterial = await this.materialFactory.createStudioMaterialFromMaterialX(
1253
- resolved.values,
1254
- resolved.textures,
1255
- resolved.textureRepeat,
1256
- this._textureCache as TextureCacheInterface,
1257
- );
1258
- if (materialXHasTextures(resolved)) {
1259
- this._texturedMaterialKeys.add(sharingKey);
1260
- }
1261
- } else {
1262
- // --- Builtin preset path (or fallback) ---
1263
- let materialDef: MaterialAppearance;
1264
- if (resolved) {
1265
- materialDef = resolved;
1266
- } else if (leafAlpha < 1) {
1267
- // Fallback for transparent objects: acrylic-clear with
1268
- // transmission matching the CAD alpha, tinted with CAD color
1269
- const { color: _, ...acrylicClear } = MATERIAL_PRESETS["acrylic-clear"];
1270
- materialDef = { ...acrylicClear, transmission: 1 - leafAlpha };
1271
- } else {
1272
- // Fallback: plastic-glossy tinted with CAD color
1273
- const { color: _, ...plasticGlossy } = MATERIAL_PRESETS["plastic-glossy"];
1274
- materialDef = plasticGlossy;
1275
- }
1276
- studioMaterial = await this.materialFactory.createStudioMaterial({
1277
- materialDef,
1278
- fallbackColor: leafColor,
1279
- fallbackAlpha: leafAlpha,
1280
- textureCache: this._textureCache as TextureCacheInterface,
1281
- });
1282
- if (materialHasTexture(materialDef)) {
1283
- this._texturedMaterialKeys.add(sharingKey);
1284
- }
1285
- }
1286
- } catch (err) {
1287
- logger.warn(
1288
- `Studio material creation failed for "${path}" (tag="${tag}"), skipping`,
1289
- err,
1290
- );
1291
- continue;
1292
- }
1293
-
1294
- this._studioMaterialCache.set(sharingKey, studioMaterial);
1295
- }
1296
-
1297
- // Triplanar mapping for textured materials.
1298
- // "triplanar" mode: always use triplanar for textured materials
1299
- // "parametric" mode: triplanar only when geometry has no UVs (fallback)
1300
- const textured = this._texturedMaterialKeys.has(sharingKey);
1301
- const hasUVs = obj.shapeGeometry?.getAttribute("uv") != null;
1302
- const needsTriplanar =
1303
- textured &&
1304
- obj.shapeGeometry != null &&
1305
- (textureMapping === "triplanar" || !hasUVs);
1306
-
1307
- if (textured) {
1308
- logger.debug(`Studio "${path}": ${needsTriplanar ? "using triplanar" : "using parametric UVs"}`);
1309
- }
1310
-
1311
- if (needsTriplanar && studioMaterial instanceof THREE.MeshPhysicalMaterial) {
1312
- const triKey = `${sharingKey}:tri:${path}`;
1313
- let triMat = this._studioMaterialCache.get(triKey);
1314
- if (!triMat) {
1315
- triMat = studioMaterial.clone();
1316
- applyTriplanarMapping(triMat as THREE.MeshPhysicalMaterial, obj.shapeGeometry!);
1317
- this._studioMaterialCache.set(triKey, triMat);
1318
- }
1319
- studioMaterial = triMat;
1320
- }
1321
-
1322
- // Build back-face variant if needed
1323
- let studioBack: THREE.MeshPhysicalMaterial | null = null;
1324
- if (obj.renderback && studioMaterial instanceof THREE.MeshPhysicalMaterial) {
1325
- const backKey = needsTriplanar
1326
- ? `${sharingKey}:tri:${path}:back`
1327
- : `${sharingKey}:back`;
1328
- let cachedBack = this._studioMaterialCache.get(backKey);
1329
- if (!cachedBack) {
1330
- cachedBack = studioMaterial.clone();
1331
- cachedBack.side = THREE.BackSide;
1332
- if (needsTriplanar && obj.shapeGeometry) {
1333
- applyTriplanarMapping(cachedBack as THREE.MeshPhysicalMaterial, obj.shapeGeometry);
1334
- }
1335
- this._studioMaterialCache.set(backKey, cachedBack);
1336
- }
1337
- studioBack = cachedBack as THREE.MeshPhysicalMaterial;
1338
- }
1339
-
1340
- // Compute tangents for anisotropic materials (required by Three.js)
1341
- if (
1342
- studioMaterial instanceof THREE.MeshPhysicalMaterial &&
1343
- studioMaterial.anisotropy > 0 &&
1344
- obj.shapeGeometry?.getAttribute("uv") != null &&
1345
- obj.shapeGeometry.getAttribute("tangent") == null
1346
- ) {
1347
- try {
1348
- obj.shapeGeometry.computeTangents();
1349
- } catch {
1350
- logger.debug(`Studio "${path}": tangent computation failed, anisotropy may have artifacts`);
1351
- }
1352
- }
1353
-
1354
- // Apply to ObjectGroup
1355
- obj.enterStudioMode(
1356
- studioMaterial instanceof THREE.MeshPhysicalMaterial ? studioMaterial : null,
1357
- studioBack,
1358
- );
1359
- }
1360
-
1361
- this._isStudioMode = true;
1362
- return [...unresolvedTags];
1363
- }
1364
-
1365
- /**
1366
- * Leave Studio mode: restore CAD materials on all ObjectGroups.
1367
- * Does NOT clear the material cache (allows fast re-entry).
1368
- */
1369
- leaveStudioMode(): void {
1370
- for (const path in this.groups) {
1371
- const obj = this.groups[path];
1372
- if (!(obj instanceof ObjectGroup)) continue;
1373
- obj.leaveStudioMode();
1374
- }
1375
- this._isStudioMode = false;
1376
- }
1377
-
1378
- /**
1379
- * Clear cached Studio materials so they are rebuilt on next enterStudioMode.
1380
- */
1381
- clearStudioMaterialCache(): void {
1382
- for (const [, material] of this._studioMaterialCache) {
1383
- material.dispose();
1384
- }
1385
- this._studioMaterialCache.clear();
1386
- this._texturedMaterialKeys.clear();
1387
- }
1388
-
1389
- /**
1390
- * Set edge visibility across all ObjectGroups while in Studio mode.
1391
- * @param visible - Whether edges should be visible
1392
- */
1393
- setStudioShowEdges(visible: boolean): void {
1394
- for (const path in this.groups) {
1395
- const obj = this.groups[path];
1396
- if (!(obj instanceof ObjectGroup)) continue;
1397
- obj.setStudioShowEdges(visible);
1398
- }
1399
- }
1400
-
1401
- /**
1402
- * Dispose all Studio mode resources (material cache + texture cache).
1403
- */
1404
- private _disposeStudioResources(): void {
1405
- // Leave studio mode if still active
1406
- if (this._isStudioMode) {
1407
- this.leaveStudioMode();
1408
- }
1409
-
1410
- // Dispose cached studio materials
1411
- for (const [, material] of this._studioMaterialCache) {
1412
- material.dispose();
1413
- }
1414
- this._studioMaterialCache.clear();
1415
- this._texturedMaterialKeys.clear();
1416
-
1417
- // Dispose texture cache
1418
- if (this._textureCache) {
1419
- this._textureCache.disposeFull();
1420
- this._textureCache = null;
1421
- }
1422
-
1423
- this._isStudioMode = false;
1424
- }
1425
- }
1426
-
1427
- /**
1428
- * Type guard to check if an object is a CompoundGroup instance.
1429
- * Uses the isCompoundGroup property following Three.js convention.
1430
- */
1431
- function isCompoundGroup(obj: THREE.Object3D | null): obj is CompoundGroup {
1432
- return (
1433
- obj != null && "isCompoundGroup" in obj && obj.isCompoundGroup === true
1434
- );
1435
- }
1436
-
1437
- export {
1438
- NestedGroup,
1439
- ObjectGroup,
1440
- CompoundGroup,
1441
- isObjectGroup,
1442
- isCompoundGroup,
1443
- };
1444
- export type { ShapeEntry };