three-cad-viewer 4.1.2 → 4.3.0
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/Readme.md +12 -5
- package/dist/camera/camera.d.ts +14 -2
- package/dist/core/studio-manager.d.ts +90 -0
- package/dist/core/types.d.ts +239 -9
- package/dist/core/viewer-state.d.ts +28 -2
- package/dist/core/viewer.d.ts +200 -6
- package/dist/index.d.ts +7 -2
- package/dist/rendering/environment.d.ts +239 -0
- package/dist/rendering/light-detection.d.ts +44 -0
- package/dist/rendering/material-factory.d.ts +77 -2
- package/dist/rendering/material-presets.d.ts +32 -0
- package/dist/rendering/room-environment.d.ts +13 -0
- package/dist/rendering/studio-composer.d.ts +130 -0
- package/dist/rendering/studio-floor.d.ts +53 -0
- package/dist/rendering/texture-cache.d.ts +100 -0
- package/dist/rendering/triplanar.d.ts +37 -0
- package/dist/scene/animation.d.ts +1 -1
- package/dist/scene/clipping.d.ts +31 -0
- package/dist/scene/nestedgroup.d.ts +63 -27
- package/dist/scene/objectgroup.d.ts +47 -0
- package/dist/three-cad-viewer.css +339 -29
- package/dist/three-cad-viewer.esm.js +26944 -11874
- package/dist/three-cad-viewer.esm.js.map +1 -1
- package/dist/three-cad-viewer.esm.min.js +10 -4
- package/dist/three-cad-viewer.js +26863 -11787
- package/dist/three-cad-viewer.min.js +10 -4
- package/dist/ui/display.d.ts +147 -0
- package/dist/utils/decode-instances.d.ts +60 -0
- package/dist/utils/utils.d.ts +10 -0
- package/package.json +4 -2
- package/src/_version.ts +1 -1
- package/src/camera/camera.ts +27 -10
- package/src/core/studio-manager.ts +652 -0
- package/src/core/types.ts +302 -9
- package/src/core/viewer-state.ts +84 -4
- package/src/core/viewer.ts +453 -22
- package/src/index.ts +24 -1
- package/src/rendering/environment.ts +840 -0
- package/src/rendering/light-detection.ts +327 -0
- package/src/rendering/material-factory.ts +458 -2
- package/src/rendering/material-presets.ts +289 -0
- package/src/rendering/raycast.ts +2 -2
- package/src/rendering/room-environment.ts +192 -0
- package/src/rendering/studio-composer.ts +577 -0
- package/src/rendering/studio-floor.ts +108 -0
- package/src/rendering/texture-cache.ts +319 -0
- package/src/rendering/triplanar.ts +329 -0
- package/src/scene/animation.ts +3 -2
- package/src/scene/clipping.ts +59 -0
- package/src/scene/nestedgroup.ts +392 -0
- package/src/scene/objectgroup.ts +186 -11
- package/src/scene/orientation.ts +12 -0
- package/src/scene/render-shape.ts +55 -21
- package/src/types/n8ao.d.ts +28 -0
- package/src/ui/display.ts +1032 -27
- package/src/ui/index.html +181 -44
- package/src/utils/decode-instances.ts +233 -0
- package/src/utils/utils.ts +33 -20
package/src/scene/clipping.ts
CHANGED
|
@@ -272,6 +272,20 @@ interface ClippingOptions {
|
|
|
272
272
|
onNormalChange?: (index: ClipIndex, normalArray: Vector3Tuple) => void;
|
|
273
273
|
}
|
|
274
274
|
|
|
275
|
+
/**
|
|
276
|
+
* Saved clipping state for mode transitions (e.g., entering/leaving Studio mode).
|
|
277
|
+
* Captures only Clipping-internal state; renderer flags and ViewerState keys
|
|
278
|
+
* are managed by the caller.
|
|
279
|
+
*/
|
|
280
|
+
interface ClippingState {
|
|
281
|
+
/** Centered constant (position) for each of the 3 clip planes */
|
|
282
|
+
planeConstants: [number, number, number];
|
|
283
|
+
/** Whether the plane helper meshes (translucent colored rectangles) are visible */
|
|
284
|
+
helperVisible: boolean;
|
|
285
|
+
/** Whether the stencil plane meshes (solid colored caps) are visible */
|
|
286
|
+
planesVisible: boolean;
|
|
287
|
+
}
|
|
288
|
+
|
|
275
289
|
/**
|
|
276
290
|
* Manages clipping planes, stencil rendering, and plane visualization.
|
|
277
291
|
*/
|
|
@@ -566,6 +580,50 @@ class Clipping extends THREE.Group {
|
|
|
566
580
|
}
|
|
567
581
|
};
|
|
568
582
|
|
|
583
|
+
/**
|
|
584
|
+
* Save the current clipping state for later restoration.
|
|
585
|
+
* Captures plane positions, helper visibility, and stencil plane visibility.
|
|
586
|
+
* Used by Studio mode to snapshot clipping state before disabling clipping.
|
|
587
|
+
*
|
|
588
|
+
* Note: `renderer.localClippingEnabled` and `clipPlaneHelpers` ViewerState
|
|
589
|
+
* are managed by the caller (Display/Viewer layer), not captured here.
|
|
590
|
+
*/
|
|
591
|
+
saveState(): ClippingState {
|
|
592
|
+
return {
|
|
593
|
+
planeConstants: [
|
|
594
|
+
this.clipPlanes[0].centeredConstant,
|
|
595
|
+
this.clipPlanes[1].centeredConstant,
|
|
596
|
+
this.clipPlanes[2].centeredConstant,
|
|
597
|
+
],
|
|
598
|
+
helperVisible: this.planeHelpers?.visible ?? false,
|
|
599
|
+
planesVisible: this._planeMeshGroup?.children.length
|
|
600
|
+
? this._planeMeshGroup.children[0].material.visible
|
|
601
|
+
: false,
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
/**
|
|
606
|
+
* Restore a previously saved clipping state.
|
|
607
|
+
* Re-applies plane positions, helper visibility, and stencil plane visibility.
|
|
608
|
+
* Used by Studio mode when leaving to restore the clipping configuration.
|
|
609
|
+
*
|
|
610
|
+
* @param state - The state previously captured by `saveState()`.
|
|
611
|
+
*/
|
|
612
|
+
restoreState(state: ClippingState): void {
|
|
613
|
+
// Restore plane positions
|
|
614
|
+
for (const i of CLIP_INDICES) {
|
|
615
|
+
this.setConstant(i, state.planeConstants[i]);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// Restore plane helper visibility
|
|
619
|
+
if (this.planeHelpers) {
|
|
620
|
+
this.planeHelpers.visible = state.helperVisible;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// Restore stencil plane mesh visibility
|
|
624
|
+
this.setVisible(state.planesVisible);
|
|
625
|
+
}
|
|
626
|
+
|
|
569
627
|
/**
|
|
570
628
|
* Clean up resources.
|
|
571
629
|
* Note: We don't null out arrays/references as GC handles cleanup when the Clipping object is collected.
|
|
@@ -579,3 +637,4 @@ class Clipping extends THREE.Group {
|
|
|
579
637
|
}
|
|
580
638
|
|
|
581
639
|
export { Clipping };
|
|
640
|
+
export type { ClippingState };
|
package/src/scene/nestedgroup.ts
CHANGED
|
@@ -10,16 +10,26 @@ import { gpuTracker } from "../utils/gpu-tracker.js";
|
|
|
10
10
|
import type {
|
|
11
11
|
ZebraColorScheme,
|
|
12
12
|
ZebraMappingMode,
|
|
13
|
+
StudioTextureMapping,
|
|
13
14
|
Shapes,
|
|
14
15
|
ColorValue,
|
|
15
16
|
ColoredMaterial,
|
|
17
|
+
MaterialAppearance,
|
|
18
|
+
MaterialXMaterial,
|
|
16
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";
|
|
17
26
|
|
|
18
27
|
interface ShapeData {
|
|
19
28
|
vertices: Float32Array | number[][];
|
|
20
29
|
normals: Float32Array | number[][];
|
|
21
30
|
triangles: Uint32Array | number[][];
|
|
22
31
|
edges?: Float32Array | number[][];
|
|
32
|
+
uvs?: Float32Array | number[];
|
|
23
33
|
}
|
|
24
34
|
|
|
25
35
|
interface EdgeData {
|
|
@@ -57,6 +67,7 @@ interface ShapeEntry {
|
|
|
57
67
|
geomtype?: number | null;
|
|
58
68
|
subtype?: string | null;
|
|
59
69
|
texture?: { image: TextureData; width: number; height: number };
|
|
70
|
+
material?: string;
|
|
60
71
|
}
|
|
61
72
|
|
|
62
73
|
interface ShapeTree {
|
|
@@ -113,6 +124,32 @@ class CompoundGroup extends THREE.Group {
|
|
|
113
124
|
*
|
|
114
125
|
* @internal - This is an internal class used by Viewer
|
|
115
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 in its properties. */
|
|
146
|
+
function materialXHasTextures(entry: MaterialXMaterial): boolean {
|
|
147
|
+
for (const [, prop] of Object.entries(entry.properties)) {
|
|
148
|
+
if (prop.texture) return true;
|
|
149
|
+
}
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
|
|
116
153
|
class NestedGroup {
|
|
117
154
|
shapes!: Shapes;
|
|
118
155
|
width: number;
|
|
@@ -134,6 +171,15 @@ class NestedGroup {
|
|
|
134
171
|
groups!: GroupsMap; // Initialized to {} in constructor
|
|
135
172
|
clipPlanes: THREE.Plane[] | null;
|
|
136
173
|
materialFactory: MaterialFactory;
|
|
174
|
+
materialsTable: Record<string, string | MaterialXMaterial | MaterialAppearance> | null;
|
|
175
|
+
resolvedMaterials: Map<string, MaterialAppearance>;
|
|
176
|
+
/** Cache for threejs-materials entries resolved from the materials table */
|
|
177
|
+
resolvedMaterialX: Map<string, MaterialXMaterial>;
|
|
178
|
+
private _textureCache: TextureCache | null;
|
|
179
|
+
private _studioMaterialCache: Map<string, THREE.MeshPhysicalMaterial | THREE.MeshBasicMaterial>;
|
|
180
|
+
/** Sharing keys of materials that have textures (for UV generation on cache hits) */
|
|
181
|
+
private _texturedMaterialKeys: Set<string>;
|
|
182
|
+
private _isStudioMode: boolean;
|
|
137
183
|
|
|
138
184
|
/**
|
|
139
185
|
* Create a NestedGroup for rendering CAD geometry.
|
|
@@ -181,6 +227,14 @@ class NestedGroup {
|
|
|
181
227
|
|
|
182
228
|
this.clipPlanes = null;
|
|
183
229
|
|
|
230
|
+
this.materialsTable = null;
|
|
231
|
+
this.resolvedMaterials = new Map();
|
|
232
|
+
this.resolvedMaterialX = new Map();
|
|
233
|
+
this._textureCache = null;
|
|
234
|
+
this._studioMaterialCache = new Map();
|
|
235
|
+
this._texturedMaterialKeys = new Set();
|
|
236
|
+
this._isStudioMode = false;
|
|
237
|
+
|
|
184
238
|
this.materialFactory = new MaterialFactory({
|
|
185
239
|
defaultOpacity: opacity,
|
|
186
240
|
metalness: metalness,
|
|
@@ -202,6 +256,110 @@ class NestedGroup {
|
|
|
202
256
|
deepDispose(this.rootGroup);
|
|
203
257
|
this.rootGroup = null;
|
|
204
258
|
}
|
|
259
|
+
this._disposeStudioResources();
|
|
260
|
+
this.resolvedMaterials.clear();
|
|
261
|
+
this.resolvedMaterialX.clear();
|
|
262
|
+
this.materialsTable = null;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Resolve a material tag to its definition.
|
|
267
|
+
*
|
|
268
|
+
* Returns either a MaterialAppearance (for builtin presets) or a
|
|
269
|
+
* MaterialXMaterial (for threejs-materials entries). The caller must check the
|
|
270
|
+
* return type to determine which factory method to use.
|
|
271
|
+
*
|
|
272
|
+
* Resolution order:
|
|
273
|
+
* 1. Check caches (resolvedMaterials / resolvedMaterialX)
|
|
274
|
+
* 2. Look up in root-level `materials` table:
|
|
275
|
+
* - string starting with "builtin:" → MATERIAL_PRESETS lookup
|
|
276
|
+
* - object with `properties` key → threejs-materials entry
|
|
277
|
+
* 3. Direct lookup in MATERIAL_PRESETS by tag name
|
|
278
|
+
* 4. No match → warning, return null
|
|
279
|
+
*
|
|
280
|
+
* @param tag - The material tag from a leaf node
|
|
281
|
+
* @param objectPath - The object path (for warning messages)
|
|
282
|
+
* @returns Resolved material definition or null if not found
|
|
283
|
+
*/
|
|
284
|
+
resolveMaterialTag(
|
|
285
|
+
tag: string,
|
|
286
|
+
objectPath: string,
|
|
287
|
+
): MaterialAppearance | MaterialXMaterial | null {
|
|
288
|
+
// Empty string is equivalent to no tag -- skip silently
|
|
289
|
+
if (tag === "") {
|
|
290
|
+
return null;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Check caches
|
|
294
|
+
const cachedPreset = this.resolvedMaterials.get(tag);
|
|
295
|
+
if (cachedPreset !== undefined) return cachedPreset;
|
|
296
|
+
|
|
297
|
+
const cachedMX = this.resolvedMaterialX.get(tag);
|
|
298
|
+
if (cachedMX !== undefined) return cachedMX;
|
|
299
|
+
|
|
300
|
+
// 1. Look up in user-defined materials table
|
|
301
|
+
if (this.materialsTable && tag in this.materialsTable) {
|
|
302
|
+
const entry = this.materialsTable[tag];
|
|
303
|
+
|
|
304
|
+
// String entry: "builtin:<preset-name>"
|
|
305
|
+
if (typeof entry === "string") {
|
|
306
|
+
if (entry.startsWith("builtin:")) {
|
|
307
|
+
const presetName = entry.slice(8);
|
|
308
|
+
const preset = MATERIAL_PRESETS[presetName];
|
|
309
|
+
if (preset) {
|
|
310
|
+
const resolved = { ...preset };
|
|
311
|
+
this.resolvedMaterials.set(tag, resolved);
|
|
312
|
+
return resolved;
|
|
313
|
+
}
|
|
314
|
+
logger.warn(
|
|
315
|
+
`Unknown builtin preset '${presetName}' referenced by '${tag}' on '${objectPath}'`,
|
|
316
|
+
);
|
|
317
|
+
return null;
|
|
318
|
+
}
|
|
319
|
+
logger.warn(
|
|
320
|
+
`Invalid material string '${entry}' for tag '${tag}' (expected "builtin:" prefix)`,
|
|
321
|
+
);
|
|
322
|
+
return null;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// MaterialXMaterial entry: object with `properties` key
|
|
326
|
+
if (isMaterialXMaterial(entry)) {
|
|
327
|
+
this.resolvedMaterialX.set(tag, entry);
|
|
328
|
+
return entry;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// MaterialAppearance entry: object with `builtin` key (preset + overrides)
|
|
332
|
+
if (typeof entry === "object" && "builtin" in entry) {
|
|
333
|
+
const appearance = entry as MaterialAppearance;
|
|
334
|
+
const presetName = appearance.builtin!;
|
|
335
|
+
const preset = MATERIAL_PRESETS[presetName];
|
|
336
|
+
if (!preset) {
|
|
337
|
+
logger.warn(
|
|
338
|
+
`Unknown builtin preset '${presetName}' referenced by '${tag}' on '${objectPath}'`,
|
|
339
|
+
);
|
|
340
|
+
return null;
|
|
341
|
+
}
|
|
342
|
+
const resolved: MaterialAppearance = { ...preset, ...appearance };
|
|
343
|
+
this.resolvedMaterials.set(tag, resolved);
|
|
344
|
+
return resolved;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Should not happen with current type, but guard anyway
|
|
348
|
+
logger.warn(`Unrecognised material entry for tag '${tag}' on '${objectPath}'`);
|
|
349
|
+
return null;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// 2. Direct lookup in built-in presets (leaf tag matches preset name)
|
|
353
|
+
const preset = MATERIAL_PRESETS[tag];
|
|
354
|
+
if (preset) {
|
|
355
|
+
const resolved = { ...preset };
|
|
356
|
+
this.resolvedMaterials.set(tag, resolved);
|
|
357
|
+
return resolved;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// 3. No match
|
|
361
|
+
logger.warn(`Unknown material tag '${tag}' on object '${objectPath}'`);
|
|
362
|
+
return null;
|
|
205
363
|
}
|
|
206
364
|
|
|
207
365
|
/**
|
|
@@ -469,6 +627,15 @@ class NestedGroup {
|
|
|
469
627
|
new THREE.BufferAttribute(normals, 3),
|
|
470
628
|
);
|
|
471
629
|
shapeGeometry.setIndex(new THREE.BufferAttribute(triangles, 1));
|
|
630
|
+
if (shape.uvs && shape.uvs.length > 0) {
|
|
631
|
+
const uvArray = shape.uvs instanceof Float32Array
|
|
632
|
+
? shape.uvs
|
|
633
|
+
: new Float32Array(shape.uvs);
|
|
634
|
+
shapeGeometry.setAttribute(
|
|
635
|
+
"uv",
|
|
636
|
+
new THREE.BufferAttribute(uvArray, 2),
|
|
637
|
+
);
|
|
638
|
+
}
|
|
472
639
|
group.shapeGeometry = shapeGeometry;
|
|
473
640
|
|
|
474
641
|
frontMaterial = this.materialFactory.createFrontFaceMaterial(
|
|
@@ -798,12 +965,21 @@ class NestedGroup {
|
|
|
798
965
|
group.add(this.renderLoop(shape));
|
|
799
966
|
} else {
|
|
800
967
|
const entry = shape as ShapeEntry;
|
|
968
|
+
// Propagate material tag from shapes data to local ShapeEntry
|
|
969
|
+
const materialTag = (shape as Shapes).material;
|
|
970
|
+
if (materialTag != null) {
|
|
971
|
+
entry.material = materialTag;
|
|
972
|
+
}
|
|
801
973
|
const has_texture = entry.texture != null;
|
|
802
974
|
const texture = has_texture ? entry.texture!.image : null;
|
|
803
975
|
const width = has_texture ? entry.texture!.width : null;
|
|
804
976
|
const height = has_texture ? entry.texture!.height : null;
|
|
805
977
|
const objectGroup = _render(entry, texture, width, height);
|
|
806
978
|
this.groups[entry.id] = objectGroup;
|
|
979
|
+
// Store material tag on ObjectGroup for Studio mode lookup
|
|
980
|
+
if (entry.material !== undefined && entry.material !== null) {
|
|
981
|
+
objectGroup.materialTag = entry.material;
|
|
982
|
+
}
|
|
807
983
|
group.add(objectGroup);
|
|
808
984
|
}
|
|
809
985
|
}
|
|
@@ -817,6 +993,9 @@ class NestedGroup {
|
|
|
817
993
|
if (this.shapes.format == "GDS") {
|
|
818
994
|
this.instances = this.shapes.instances || null;
|
|
819
995
|
}
|
|
996
|
+
this.materialsTable = this.shapes.materials || null;
|
|
997
|
+
this.resolvedMaterials.clear();
|
|
998
|
+
this.resolvedMaterialX.clear();
|
|
820
999
|
this.rootGroup = this.renderLoop(this.shapes);
|
|
821
1000
|
return this.rootGroup;
|
|
822
1001
|
}
|
|
@@ -1018,6 +1197,219 @@ class NestedGroup {
|
|
|
1018
1197
|
setZebraMappingMode(flag: ZebraMappingMode): void {
|
|
1019
1198
|
this._traverse("setZebraMappingMode", flag);
|
|
1020
1199
|
}
|
|
1200
|
+
|
|
1201
|
+
// ===========================================================================
|
|
1202
|
+
// Studio Mode
|
|
1203
|
+
// ===========================================================================
|
|
1204
|
+
|
|
1205
|
+
/**
|
|
1206
|
+
* Enter Studio mode: build and apply studio materials to all ObjectGroups.
|
|
1207
|
+
*
|
|
1208
|
+
* Material resolution per ObjectGroup:
|
|
1209
|
+
* 1. Resolve the material tag via `resolveMaterialTag()`
|
|
1210
|
+
* - MaterialXMaterial → `createStudioMaterialFromMaterialX`
|
|
1211
|
+
* - MaterialAppearance → `createStudioMaterial` (builtin presets)
|
|
1212
|
+
* - null (no tag) → fallback plastic-glossy preset tinted with CAD color
|
|
1213
|
+
* 2. Cache by sharing key for reuse across objects with the same tag+color
|
|
1214
|
+
* 3. Clone BackSide variant for renderback objects
|
|
1215
|
+
* 4. Auto-generate box-projected UVs when textured but geometry has no UVs
|
|
1216
|
+
*/
|
|
1217
|
+
async enterStudioMode(textureMapping: StudioTextureMapping = "triplanar"): Promise<string[]> {
|
|
1218
|
+
// Create TextureCache lazily
|
|
1219
|
+
if (!this._textureCache) {
|
|
1220
|
+
this._textureCache = new TextureCache();
|
|
1221
|
+
}
|
|
1222
|
+
// Track material tags that failed to resolve
|
|
1223
|
+
const unresolvedTags = new Set<string>();
|
|
1224
|
+
|
|
1225
|
+
// Iterate all ObjectGroups with front meshes
|
|
1226
|
+
for (const path in this.groups) {
|
|
1227
|
+
const obj = this.groups[path];
|
|
1228
|
+
if (!(obj instanceof ObjectGroup)) continue;
|
|
1229
|
+
if (!obj.front) continue;
|
|
1230
|
+
|
|
1231
|
+
// Determine material tag, leaf color, and leaf alpha
|
|
1232
|
+
const tag = obj.materialTag || "";
|
|
1233
|
+
const leafColor = obj.originalColor
|
|
1234
|
+
? "#" + obj.originalColor.getHexString()
|
|
1235
|
+
: "#707070";
|
|
1236
|
+
const leafAlpha = obj.alpha;
|
|
1237
|
+
|
|
1238
|
+
// Compute sharing key
|
|
1239
|
+
const sharingKey = `${tag}:${leafColor}:${leafAlpha}`;
|
|
1240
|
+
|
|
1241
|
+
// Check cached material for this key
|
|
1242
|
+
let studioMaterial = this._studioMaterialCache.get(sharingKey);
|
|
1243
|
+
|
|
1244
|
+
if (!studioMaterial) {
|
|
1245
|
+
// Resolve the tag
|
|
1246
|
+
const resolved = tag ? this.resolveMaterialTag(tag, path) : null;
|
|
1247
|
+
if (tag && !resolved) {
|
|
1248
|
+
unresolvedTags.add(tag);
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
// Per-object try/catch: a single failure should not abort the rest
|
|
1252
|
+
try {
|
|
1253
|
+
if (resolved && isMaterialXMaterial(resolved)) {
|
|
1254
|
+
// --- threejs-materials path ---
|
|
1255
|
+
studioMaterial = await this.materialFactory.createStudioMaterialFromMaterialX(
|
|
1256
|
+
resolved.properties,
|
|
1257
|
+
resolved.textureRepeat,
|
|
1258
|
+
this._textureCache as TextureCacheInterface,
|
|
1259
|
+
);
|
|
1260
|
+
if (materialXHasTextures(resolved)) {
|
|
1261
|
+
this._texturedMaterialKeys.add(sharingKey);
|
|
1262
|
+
}
|
|
1263
|
+
} else {
|
|
1264
|
+
// --- Builtin preset path (or fallback) ---
|
|
1265
|
+
let materialDef: MaterialAppearance;
|
|
1266
|
+
if (resolved) {
|
|
1267
|
+
materialDef = resolved;
|
|
1268
|
+
} else if (leafAlpha < 1) {
|
|
1269
|
+
// Fallback for transparent objects: acrylic-clear with
|
|
1270
|
+
// transmission matching the CAD alpha, tinted with CAD color
|
|
1271
|
+
const { color: _, ...acrylicClear } = MATERIAL_PRESETS["acrylic-clear"];
|
|
1272
|
+
materialDef = { ...acrylicClear, transmission: 1 - leafAlpha };
|
|
1273
|
+
} else {
|
|
1274
|
+
// Fallback: plastic-glossy tinted with CAD color
|
|
1275
|
+
const { color: _, ...plasticGlossy } = MATERIAL_PRESETS["plastic-glossy"];
|
|
1276
|
+
materialDef = plasticGlossy;
|
|
1277
|
+
}
|
|
1278
|
+
studioMaterial = await this.materialFactory.createStudioMaterial({
|
|
1279
|
+
materialDef,
|
|
1280
|
+
fallbackColor: leafColor,
|
|
1281
|
+
fallbackAlpha: leafAlpha,
|
|
1282
|
+
textureCache: this._textureCache as TextureCacheInterface,
|
|
1283
|
+
});
|
|
1284
|
+
if (materialHasTexture(materialDef)) {
|
|
1285
|
+
this._texturedMaterialKeys.add(sharingKey);
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
} catch (err) {
|
|
1289
|
+
logger.warn(
|
|
1290
|
+
`Studio material creation failed for "${path}" (tag="${tag}"), skipping`,
|
|
1291
|
+
err,
|
|
1292
|
+
);
|
|
1293
|
+
continue;
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
this._studioMaterialCache.set(sharingKey, studioMaterial);
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
// Triplanar mapping for textured materials.
|
|
1300
|
+
// "triplanar" mode: always use triplanar for textured materials
|
|
1301
|
+
// "parametric" mode: triplanar only when geometry has no UVs (fallback)
|
|
1302
|
+
const textured = this._texturedMaterialKeys.has(sharingKey);
|
|
1303
|
+
const hasUVs = obj.shapeGeometry?.getAttribute("uv") != null;
|
|
1304
|
+
const needsTriplanar =
|
|
1305
|
+
textured &&
|
|
1306
|
+
obj.shapeGeometry != null &&
|
|
1307
|
+
(textureMapping === "triplanar" || !hasUVs);
|
|
1308
|
+
|
|
1309
|
+
if (textured) {
|
|
1310
|
+
logger.debug(`Studio "${path}": ${needsTriplanar ? "using triplanar" : "using parametric UVs"}`);
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
if (needsTriplanar && studioMaterial instanceof THREE.MeshPhysicalMaterial) {
|
|
1314
|
+
const triKey = `${sharingKey}:tri:${path}`;
|
|
1315
|
+
let triMat = this._studioMaterialCache.get(triKey);
|
|
1316
|
+
if (!triMat) {
|
|
1317
|
+
triMat = studioMaterial.clone();
|
|
1318
|
+
applyTriplanarMapping(triMat as THREE.MeshPhysicalMaterial, obj.shapeGeometry!);
|
|
1319
|
+
this._studioMaterialCache.set(triKey, triMat);
|
|
1320
|
+
}
|
|
1321
|
+
studioMaterial = triMat;
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
// Build back-face variant if needed
|
|
1325
|
+
let studioBack: THREE.MeshPhysicalMaterial | null = null;
|
|
1326
|
+
if (obj.renderback && studioMaterial instanceof THREE.MeshPhysicalMaterial) {
|
|
1327
|
+
const backKey = needsTriplanar
|
|
1328
|
+
? `${sharingKey}:tri:${path}:back`
|
|
1329
|
+
: `${sharingKey}:back`;
|
|
1330
|
+
let cachedBack = this._studioMaterialCache.get(backKey);
|
|
1331
|
+
if (!cachedBack) {
|
|
1332
|
+
cachedBack = studioMaterial.clone();
|
|
1333
|
+
cachedBack.side = THREE.BackSide;
|
|
1334
|
+
if (needsTriplanar && obj.shapeGeometry) {
|
|
1335
|
+
applyTriplanarMapping(cachedBack as THREE.MeshPhysicalMaterial, obj.shapeGeometry);
|
|
1336
|
+
}
|
|
1337
|
+
this._studioMaterialCache.set(backKey, cachedBack);
|
|
1338
|
+
}
|
|
1339
|
+
studioBack = cachedBack as THREE.MeshPhysicalMaterial;
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
// Apply to ObjectGroup
|
|
1343
|
+
obj.enterStudioMode(
|
|
1344
|
+
studioMaterial instanceof THREE.MeshPhysicalMaterial ? studioMaterial : null,
|
|
1345
|
+
studioBack,
|
|
1346
|
+
);
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
this._isStudioMode = true;
|
|
1350
|
+
return [...unresolvedTags];
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
/**
|
|
1354
|
+
* Leave Studio mode: restore CAD materials on all ObjectGroups.
|
|
1355
|
+
* Does NOT clear the material cache (allows fast re-entry).
|
|
1356
|
+
*/
|
|
1357
|
+
leaveStudioMode(): void {
|
|
1358
|
+
for (const path in this.groups) {
|
|
1359
|
+
const obj = this.groups[path];
|
|
1360
|
+
if (!(obj instanceof ObjectGroup)) continue;
|
|
1361
|
+
obj.leaveStudioMode();
|
|
1362
|
+
}
|
|
1363
|
+
this._isStudioMode = false;
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
/**
|
|
1367
|
+
* Clear cached Studio materials so they are rebuilt on next enterStudioMode.
|
|
1368
|
+
*/
|
|
1369
|
+
clearStudioMaterialCache(): void {
|
|
1370
|
+
for (const [, material] of this._studioMaterialCache) {
|
|
1371
|
+
material.dispose();
|
|
1372
|
+
}
|
|
1373
|
+
this._studioMaterialCache.clear();
|
|
1374
|
+
this._texturedMaterialKeys.clear();
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
/**
|
|
1378
|
+
* Set edge visibility across all ObjectGroups while in Studio mode.
|
|
1379
|
+
* @param visible - Whether edges should be visible
|
|
1380
|
+
*/
|
|
1381
|
+
setStudioShowEdges(visible: boolean): void {
|
|
1382
|
+
for (const path in this.groups) {
|
|
1383
|
+
const obj = this.groups[path];
|
|
1384
|
+
if (!(obj instanceof ObjectGroup)) continue;
|
|
1385
|
+
obj.setStudioShowEdges(visible);
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
/**
|
|
1390
|
+
* Dispose all Studio mode resources (material cache + texture cache).
|
|
1391
|
+
*/
|
|
1392
|
+
private _disposeStudioResources(): void {
|
|
1393
|
+
// Leave studio mode if still active
|
|
1394
|
+
if (this._isStudioMode) {
|
|
1395
|
+
this.leaveStudioMode();
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
// Dispose cached studio materials
|
|
1399
|
+
for (const [, material] of this._studioMaterialCache) {
|
|
1400
|
+
material.dispose();
|
|
1401
|
+
}
|
|
1402
|
+
this._studioMaterialCache.clear();
|
|
1403
|
+
this._texturedMaterialKeys.clear();
|
|
1404
|
+
|
|
1405
|
+
// Dispose texture cache
|
|
1406
|
+
if (this._textureCache) {
|
|
1407
|
+
this._textureCache.disposeFull();
|
|
1408
|
+
this._textureCache = null;
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
this._isStudioMode = false;
|
|
1412
|
+
}
|
|
1021
1413
|
}
|
|
1022
1414
|
|
|
1023
1415
|
/**
|