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/scene/nestedgroup.ts
DELETED
|
@@ -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 };
|