three-cad-viewer 4.1.2 → 4.2.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 +91 -0
- package/dist/core/types.d.ts +260 -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 +142 -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 +64 -27
- package/dist/scene/objectgroup.d.ts +47 -0
- package/dist/three-cad-viewer.css +339 -29
- package/dist/three-cad-viewer.esm.js +27567 -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 +27486 -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 +682 -0
- package/src/core/types.ts +328 -9
- package/src/core/viewer-state.ts +84 -4
- package/src/core/viewer.ts +453 -22
- package/src/index.ts +25 -1
- package/src/rendering/environment.ts +840 -0
- package/src/rendering/light-detection.ts +327 -0
- package/src/rendering/material-factory.ts +456 -2
- package/src/rendering/material-presets.ts +303 -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 +1020 -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 +399 -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/nestedgroup.ts
CHANGED
|
@@ -10,16 +10,27 @@ 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,
|
|
19
|
+
TextureEntry,
|
|
16
20
|
} from "../core/types";
|
|
21
|
+
import { isMaterialXMaterial } from "../core/types";
|
|
22
|
+
import { MATERIAL_PRESETS } from "../rendering/material-presets.js";
|
|
23
|
+
import { logger } from "../utils/logger.js";
|
|
24
|
+
import { TextureCache } from "../rendering/texture-cache.js";
|
|
25
|
+
import type { TextureCacheInterface } from "../rendering/material-factory.js";
|
|
26
|
+
import { applyTriplanarMapping } from "../rendering/triplanar.js";
|
|
17
27
|
|
|
18
28
|
interface ShapeData {
|
|
19
29
|
vertices: Float32Array | number[][];
|
|
20
30
|
normals: Float32Array | number[][];
|
|
21
31
|
triangles: Uint32Array | number[][];
|
|
22
32
|
edges?: Float32Array | number[][];
|
|
33
|
+
uvs?: Float32Array | number[];
|
|
23
34
|
}
|
|
24
35
|
|
|
25
36
|
interface EdgeData {
|
|
@@ -57,6 +68,7 @@ interface ShapeEntry {
|
|
|
57
68
|
geomtype?: number | null;
|
|
58
69
|
subtype?: string | null;
|
|
59
70
|
texture?: { image: TextureData; width: number; height: number };
|
|
71
|
+
material?: string;
|
|
60
72
|
}
|
|
61
73
|
|
|
62
74
|
interface ShapeTree {
|
|
@@ -113,6 +125,32 @@ class CompoundGroup extends THREE.Group {
|
|
|
113
125
|
*
|
|
114
126
|
* @internal - This is an internal class used by Viewer
|
|
115
127
|
*/
|
|
128
|
+
|
|
129
|
+
/** Texture field names on MaterialAppearance that require UV coordinates. */
|
|
130
|
+
const TEXTURE_FIELDS = [
|
|
131
|
+
"map", "normalMap", "aoMap",
|
|
132
|
+
"metalnessMap", "roughnessMap", "emissiveMap", "transmissionMap",
|
|
133
|
+
"clearcoatMap", "clearcoatRoughnessMap", "clearcoatNormalMap",
|
|
134
|
+
"thicknessMap", "specularIntensityMap", "specularColorMap",
|
|
135
|
+
"sheenColorMap", "sheenRoughnessMap", "anisotropyMap",
|
|
136
|
+
] as const;
|
|
137
|
+
|
|
138
|
+
/** Check whether a resolved MaterialAppearance references any texture. */
|
|
139
|
+
function materialHasTexture(def: MaterialAppearance): boolean {
|
|
140
|
+
for (const f of TEXTURE_FIELDS) {
|
|
141
|
+
if ((def as Record<string, unknown>)[f]) return true;
|
|
142
|
+
}
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Check whether a threejs-materials entry has texture references in its properties. */
|
|
147
|
+
function materialXHasTextures(entry: MaterialXMaterial): boolean {
|
|
148
|
+
for (const [, prop] of Object.entries(entry.properties)) {
|
|
149
|
+
if (prop.texture) return true;
|
|
150
|
+
}
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
|
|
116
154
|
class NestedGroup {
|
|
117
155
|
shapes!: Shapes;
|
|
118
156
|
width: number;
|
|
@@ -134,6 +172,16 @@ class NestedGroup {
|
|
|
134
172
|
groups!: GroupsMap; // Initialized to {} in constructor
|
|
135
173
|
clipPlanes: THREE.Plane[] | null;
|
|
136
174
|
materialFactory: MaterialFactory;
|
|
175
|
+
texturesTable: Record<string, TextureEntry> | null;
|
|
176
|
+
materialsTable: Record<string, string | MaterialXMaterial | MaterialAppearance> | null;
|
|
177
|
+
resolvedMaterials: Map<string, MaterialAppearance>;
|
|
178
|
+
/** Cache for threejs-materials entries resolved from the materials table */
|
|
179
|
+
resolvedMaterialX: Map<string, MaterialXMaterial>;
|
|
180
|
+
private _textureCache: TextureCache | null;
|
|
181
|
+
private _studioMaterialCache: Map<string, THREE.MeshPhysicalMaterial | THREE.MeshBasicMaterial>;
|
|
182
|
+
/** Sharing keys of materials that have textures (for UV generation on cache hits) */
|
|
183
|
+
private _texturedMaterialKeys: Set<string>;
|
|
184
|
+
private _isStudioMode: boolean;
|
|
137
185
|
|
|
138
186
|
/**
|
|
139
187
|
* Create a NestedGroup for rendering CAD geometry.
|
|
@@ -181,6 +229,15 @@ class NestedGroup {
|
|
|
181
229
|
|
|
182
230
|
this.clipPlanes = null;
|
|
183
231
|
|
|
232
|
+
this.texturesTable = null;
|
|
233
|
+
this.materialsTable = null;
|
|
234
|
+
this.resolvedMaterials = new Map();
|
|
235
|
+
this.resolvedMaterialX = new Map();
|
|
236
|
+
this._textureCache = null;
|
|
237
|
+
this._studioMaterialCache = new Map();
|
|
238
|
+
this._texturedMaterialKeys = new Set();
|
|
239
|
+
this._isStudioMode = false;
|
|
240
|
+
|
|
184
241
|
this.materialFactory = new MaterialFactory({
|
|
185
242
|
defaultOpacity: opacity,
|
|
186
243
|
metalness: metalness,
|
|
@@ -202,6 +259,111 @@ class NestedGroup {
|
|
|
202
259
|
deepDispose(this.rootGroup);
|
|
203
260
|
this.rootGroup = null;
|
|
204
261
|
}
|
|
262
|
+
this._disposeStudioResources();
|
|
263
|
+
this.resolvedMaterials.clear();
|
|
264
|
+
this.resolvedMaterialX.clear();
|
|
265
|
+
this.texturesTable = null;
|
|
266
|
+
this.materialsTable = null;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Resolve a material tag to its definition.
|
|
271
|
+
*
|
|
272
|
+
* Returns either a MaterialAppearance (for builtin presets) or a
|
|
273
|
+
* MaterialXMaterial (for threejs-materials entries). The caller must check the
|
|
274
|
+
* return type to determine which factory method to use.
|
|
275
|
+
*
|
|
276
|
+
* Resolution order:
|
|
277
|
+
* 1. Check caches (resolvedMaterials / resolvedMaterialX)
|
|
278
|
+
* 2. Look up in root-level `materials` table:
|
|
279
|
+
* - string starting with "builtin:" → MATERIAL_PRESETS lookup
|
|
280
|
+
* - object with `properties` key → threejs-materials entry
|
|
281
|
+
* 3. Direct lookup in MATERIAL_PRESETS by tag name
|
|
282
|
+
* 4. No match → warning, return null
|
|
283
|
+
*
|
|
284
|
+
* @param tag - The material tag from a leaf node
|
|
285
|
+
* @param objectPath - The object path (for warning messages)
|
|
286
|
+
* @returns Resolved material definition or null if not found
|
|
287
|
+
*/
|
|
288
|
+
resolveMaterialTag(
|
|
289
|
+
tag: string,
|
|
290
|
+
objectPath: string,
|
|
291
|
+
): MaterialAppearance | MaterialXMaterial | null {
|
|
292
|
+
// Empty string is equivalent to no tag -- skip silently
|
|
293
|
+
if (tag === "") {
|
|
294
|
+
return null;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Check caches
|
|
298
|
+
const cachedPreset = this.resolvedMaterials.get(tag);
|
|
299
|
+
if (cachedPreset !== undefined) return cachedPreset;
|
|
300
|
+
|
|
301
|
+
const cachedMX = this.resolvedMaterialX.get(tag);
|
|
302
|
+
if (cachedMX !== undefined) return cachedMX;
|
|
303
|
+
|
|
304
|
+
// 1. Look up in user-defined materials table
|
|
305
|
+
if (this.materialsTable && tag in this.materialsTable) {
|
|
306
|
+
const entry = this.materialsTable[tag];
|
|
307
|
+
|
|
308
|
+
// String entry: "builtin:<preset-name>"
|
|
309
|
+
if (typeof entry === "string") {
|
|
310
|
+
if (entry.startsWith("builtin:")) {
|
|
311
|
+
const presetName = entry.slice(8);
|
|
312
|
+
const preset = MATERIAL_PRESETS[presetName];
|
|
313
|
+
if (preset) {
|
|
314
|
+
const resolved = { ...preset };
|
|
315
|
+
this.resolvedMaterials.set(tag, resolved);
|
|
316
|
+
return resolved;
|
|
317
|
+
}
|
|
318
|
+
logger.warn(
|
|
319
|
+
`Unknown builtin preset '${presetName}' referenced by '${tag}' on '${objectPath}'`,
|
|
320
|
+
);
|
|
321
|
+
return null;
|
|
322
|
+
}
|
|
323
|
+
logger.warn(
|
|
324
|
+
`Invalid material string '${entry}' for tag '${tag}' (expected "builtin:" prefix)`,
|
|
325
|
+
);
|
|
326
|
+
return null;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// MaterialXMaterial entry: object with `properties` key
|
|
330
|
+
if (isMaterialXMaterial(entry)) {
|
|
331
|
+
this.resolvedMaterialX.set(tag, entry);
|
|
332
|
+
return entry;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// MaterialAppearance entry: object with `builtin` key (preset + overrides)
|
|
336
|
+
if (typeof entry === "object" && "builtin" in entry) {
|
|
337
|
+
const appearance = entry as MaterialAppearance;
|
|
338
|
+
const presetName = appearance.builtin!;
|
|
339
|
+
const preset = MATERIAL_PRESETS[presetName];
|
|
340
|
+
if (!preset) {
|
|
341
|
+
logger.warn(
|
|
342
|
+
`Unknown builtin preset '${presetName}' referenced by '${tag}' on '${objectPath}'`,
|
|
343
|
+
);
|
|
344
|
+
return null;
|
|
345
|
+
}
|
|
346
|
+
const resolved: MaterialAppearance = { ...preset, ...appearance };
|
|
347
|
+
this.resolvedMaterials.set(tag, resolved);
|
|
348
|
+
return resolved;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Should not happen with current type, but guard anyway
|
|
352
|
+
logger.warn(`Unrecognised material entry for tag '${tag}' on '${objectPath}'`);
|
|
353
|
+
return null;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// 2. Direct lookup in built-in presets (leaf tag matches preset name)
|
|
357
|
+
const preset = MATERIAL_PRESETS[tag];
|
|
358
|
+
if (preset) {
|
|
359
|
+
const resolved = { ...preset };
|
|
360
|
+
this.resolvedMaterials.set(tag, resolved);
|
|
361
|
+
return resolved;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// 3. No match
|
|
365
|
+
logger.warn(`Unknown material tag '${tag}' on object '${objectPath}'`);
|
|
366
|
+
return null;
|
|
205
367
|
}
|
|
206
368
|
|
|
207
369
|
/**
|
|
@@ -469,6 +631,15 @@ class NestedGroup {
|
|
|
469
631
|
new THREE.BufferAttribute(normals, 3),
|
|
470
632
|
);
|
|
471
633
|
shapeGeometry.setIndex(new THREE.BufferAttribute(triangles, 1));
|
|
634
|
+
if (shape.uvs && shape.uvs.length > 0) {
|
|
635
|
+
const uvArray = shape.uvs instanceof Float32Array
|
|
636
|
+
? shape.uvs
|
|
637
|
+
: new Float32Array(shape.uvs);
|
|
638
|
+
shapeGeometry.setAttribute(
|
|
639
|
+
"uv",
|
|
640
|
+
new THREE.BufferAttribute(uvArray, 2),
|
|
641
|
+
);
|
|
642
|
+
}
|
|
472
643
|
group.shapeGeometry = shapeGeometry;
|
|
473
644
|
|
|
474
645
|
frontMaterial = this.materialFactory.createFrontFaceMaterial(
|
|
@@ -798,12 +969,21 @@ class NestedGroup {
|
|
|
798
969
|
group.add(this.renderLoop(shape));
|
|
799
970
|
} else {
|
|
800
971
|
const entry = shape as ShapeEntry;
|
|
972
|
+
// Propagate material tag from shapes data to local ShapeEntry
|
|
973
|
+
const materialTag = (shape as Shapes).material;
|
|
974
|
+
if (materialTag != null) {
|
|
975
|
+
entry.material = materialTag;
|
|
976
|
+
}
|
|
801
977
|
const has_texture = entry.texture != null;
|
|
802
978
|
const texture = has_texture ? entry.texture!.image : null;
|
|
803
979
|
const width = has_texture ? entry.texture!.width : null;
|
|
804
980
|
const height = has_texture ? entry.texture!.height : null;
|
|
805
981
|
const objectGroup = _render(entry, texture, width, height);
|
|
806
982
|
this.groups[entry.id] = objectGroup;
|
|
983
|
+
// Store material tag on ObjectGroup for Studio mode lookup
|
|
984
|
+
if (entry.material !== undefined && entry.material !== null) {
|
|
985
|
+
objectGroup.materialTag = entry.material;
|
|
986
|
+
}
|
|
807
987
|
group.add(objectGroup);
|
|
808
988
|
}
|
|
809
989
|
}
|
|
@@ -817,6 +997,10 @@ class NestedGroup {
|
|
|
817
997
|
if (this.shapes.format == "GDS") {
|
|
818
998
|
this.instances = this.shapes.instances || null;
|
|
819
999
|
}
|
|
1000
|
+
this.texturesTable = this.shapes.textures || null;
|
|
1001
|
+
this.materialsTable = this.shapes.materials || null;
|
|
1002
|
+
this.resolvedMaterials.clear();
|
|
1003
|
+
this.resolvedMaterialX.clear();
|
|
820
1004
|
this.rootGroup = this.renderLoop(this.shapes);
|
|
821
1005
|
return this.rootGroup;
|
|
822
1006
|
}
|
|
@@ -1018,6 +1202,221 @@ class NestedGroup {
|
|
|
1018
1202
|
setZebraMappingMode(flag: ZebraMappingMode): void {
|
|
1019
1203
|
this._traverse("setZebraMappingMode", flag);
|
|
1020
1204
|
}
|
|
1205
|
+
|
|
1206
|
+
// ===========================================================================
|
|
1207
|
+
// Studio Mode
|
|
1208
|
+
// ===========================================================================
|
|
1209
|
+
|
|
1210
|
+
/**
|
|
1211
|
+
* Enter Studio mode: build and apply studio materials to all ObjectGroups.
|
|
1212
|
+
*
|
|
1213
|
+
* Material resolution per ObjectGroup:
|
|
1214
|
+
* 1. Resolve the material tag via `resolveMaterialTag()`
|
|
1215
|
+
* - MaterialXMaterial → `createStudioMaterialFromMaterialX`
|
|
1216
|
+
* - MaterialAppearance → `createStudioMaterial` (builtin presets)
|
|
1217
|
+
* - null (no tag) → fallback plastic-glossy preset tinted with CAD color
|
|
1218
|
+
* 2. Cache by sharing key for reuse across objects with the same tag+color
|
|
1219
|
+
* 3. Clone BackSide variant for renderback objects
|
|
1220
|
+
* 4. Auto-generate box-projected UVs when textured but geometry has no UVs
|
|
1221
|
+
*/
|
|
1222
|
+
async enterStudioMode(textureMapping: StudioTextureMapping = "triplanar"): Promise<string[]> {
|
|
1223
|
+
// Create TextureCache lazily
|
|
1224
|
+
if (!this._textureCache) {
|
|
1225
|
+
this._textureCache = new TextureCache();
|
|
1226
|
+
}
|
|
1227
|
+
this._textureCache.setTexturesTable(this.texturesTable ?? undefined);
|
|
1228
|
+
|
|
1229
|
+
// Track material tags that failed to resolve
|
|
1230
|
+
const unresolvedTags = new Set<string>();
|
|
1231
|
+
|
|
1232
|
+
// Iterate all ObjectGroups with front meshes
|
|
1233
|
+
for (const path in this.groups) {
|
|
1234
|
+
const obj = this.groups[path];
|
|
1235
|
+
if (!(obj instanceof ObjectGroup)) continue;
|
|
1236
|
+
if (!obj.front) continue;
|
|
1237
|
+
|
|
1238
|
+
// Determine material tag, leaf color, and leaf alpha
|
|
1239
|
+
const tag = obj.materialTag || "";
|
|
1240
|
+
const leafColor = obj.originalColor
|
|
1241
|
+
? "#" + obj.originalColor.getHexString()
|
|
1242
|
+
: "#707070";
|
|
1243
|
+
const leafAlpha = obj.alpha;
|
|
1244
|
+
|
|
1245
|
+
// Compute sharing key
|
|
1246
|
+
const sharingKey = `${tag}:${leafColor}:${leafAlpha}`;
|
|
1247
|
+
|
|
1248
|
+
// Check cached material for this key
|
|
1249
|
+
let studioMaterial = this._studioMaterialCache.get(sharingKey);
|
|
1250
|
+
|
|
1251
|
+
if (!studioMaterial) {
|
|
1252
|
+
// Resolve the tag
|
|
1253
|
+
const resolved = tag ? this.resolveMaterialTag(tag, path) : null;
|
|
1254
|
+
if (tag && !resolved) {
|
|
1255
|
+
unresolvedTags.add(tag);
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
// Per-object try/catch: a single failure should not abort the rest
|
|
1259
|
+
try {
|
|
1260
|
+
if (resolved && isMaterialXMaterial(resolved)) {
|
|
1261
|
+
// --- threejs-materials path ---
|
|
1262
|
+
studioMaterial = await this.materialFactory.createStudioMaterialFromMaterialX(
|
|
1263
|
+
resolved.properties,
|
|
1264
|
+
resolved.textureRepeat,
|
|
1265
|
+
this._textureCache as TextureCacheInterface,
|
|
1266
|
+
);
|
|
1267
|
+
if (materialXHasTextures(resolved)) {
|
|
1268
|
+
this._texturedMaterialKeys.add(sharingKey);
|
|
1269
|
+
}
|
|
1270
|
+
} else {
|
|
1271
|
+
// --- Builtin preset path (or fallback) ---
|
|
1272
|
+
let materialDef: MaterialAppearance;
|
|
1273
|
+
if (resolved) {
|
|
1274
|
+
materialDef = resolved;
|
|
1275
|
+
} else if (leafAlpha < 1) {
|
|
1276
|
+
// Fallback for transparent objects: acrylic-clear with
|
|
1277
|
+
// transmission matching the CAD alpha, tinted with CAD color
|
|
1278
|
+
const { color: _, ...acrylicClear } = MATERIAL_PRESETS["acrylic-clear"];
|
|
1279
|
+
materialDef = { ...acrylicClear, transmission: 1 - leafAlpha };
|
|
1280
|
+
} else {
|
|
1281
|
+
// Fallback: plastic-glossy tinted with CAD color
|
|
1282
|
+
const { color: _, ...plasticGlossy } = MATERIAL_PRESETS["plastic-glossy"];
|
|
1283
|
+
materialDef = plasticGlossy;
|
|
1284
|
+
}
|
|
1285
|
+
studioMaterial = await this.materialFactory.createStudioMaterial({
|
|
1286
|
+
materialDef,
|
|
1287
|
+
fallbackColor: leafColor,
|
|
1288
|
+
fallbackAlpha: leafAlpha,
|
|
1289
|
+
textureCache: this._textureCache as TextureCacheInterface,
|
|
1290
|
+
});
|
|
1291
|
+
if (materialHasTexture(materialDef)) {
|
|
1292
|
+
this._texturedMaterialKeys.add(sharingKey);
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
} catch (err) {
|
|
1296
|
+
logger.warn(
|
|
1297
|
+
`Studio material creation failed for "${path}" (tag="${tag}"), skipping`,
|
|
1298
|
+
err,
|
|
1299
|
+
);
|
|
1300
|
+
continue;
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
this._studioMaterialCache.set(sharingKey, studioMaterial);
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
// Triplanar mapping for textured materials.
|
|
1307
|
+
// "triplanar" mode: always use triplanar for textured materials
|
|
1308
|
+
// "parametric" mode: triplanar only when geometry has no UVs (fallback)
|
|
1309
|
+
const textured = this._texturedMaterialKeys.has(sharingKey);
|
|
1310
|
+
const hasUVs = obj.shapeGeometry?.getAttribute("uv") != null;
|
|
1311
|
+
const needsTriplanar =
|
|
1312
|
+
textured &&
|
|
1313
|
+
obj.shapeGeometry != null &&
|
|
1314
|
+
(textureMapping === "triplanar" || !hasUVs);
|
|
1315
|
+
|
|
1316
|
+
if (textured) {
|
|
1317
|
+
logger.debug(`Studio "${path}": ${needsTriplanar ? "using triplanar" : "using parametric UVs"}`);
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
if (needsTriplanar && studioMaterial instanceof THREE.MeshPhysicalMaterial) {
|
|
1321
|
+
const triKey = `${sharingKey}:tri:${path}`;
|
|
1322
|
+
let triMat = this._studioMaterialCache.get(triKey);
|
|
1323
|
+
if (!triMat) {
|
|
1324
|
+
triMat = studioMaterial.clone();
|
|
1325
|
+
applyTriplanarMapping(triMat as THREE.MeshPhysicalMaterial, obj.shapeGeometry!);
|
|
1326
|
+
this._studioMaterialCache.set(triKey, triMat);
|
|
1327
|
+
}
|
|
1328
|
+
studioMaterial = triMat;
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
// Build back-face variant if needed
|
|
1332
|
+
let studioBack: THREE.MeshPhysicalMaterial | null = null;
|
|
1333
|
+
if (obj.renderback && studioMaterial instanceof THREE.MeshPhysicalMaterial) {
|
|
1334
|
+
const backKey = needsTriplanar
|
|
1335
|
+
? `${sharingKey}:tri:${path}:back`
|
|
1336
|
+
: `${sharingKey}:back`;
|
|
1337
|
+
let cachedBack = this._studioMaterialCache.get(backKey);
|
|
1338
|
+
if (!cachedBack) {
|
|
1339
|
+
cachedBack = studioMaterial.clone();
|
|
1340
|
+
cachedBack.side = THREE.BackSide;
|
|
1341
|
+
if (needsTriplanar && obj.shapeGeometry) {
|
|
1342
|
+
applyTriplanarMapping(cachedBack as THREE.MeshPhysicalMaterial, obj.shapeGeometry);
|
|
1343
|
+
}
|
|
1344
|
+
this._studioMaterialCache.set(backKey, cachedBack);
|
|
1345
|
+
}
|
|
1346
|
+
studioBack = cachedBack as THREE.MeshPhysicalMaterial;
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
// Apply to ObjectGroup
|
|
1350
|
+
obj.enterStudioMode(
|
|
1351
|
+
studioMaterial instanceof THREE.MeshPhysicalMaterial ? studioMaterial : null,
|
|
1352
|
+
studioBack,
|
|
1353
|
+
);
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
this._isStudioMode = true;
|
|
1357
|
+
return [...unresolvedTags];
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
/**
|
|
1361
|
+
* Leave Studio mode: restore CAD materials on all ObjectGroups.
|
|
1362
|
+
* Does NOT clear the material cache (allows fast re-entry).
|
|
1363
|
+
*/
|
|
1364
|
+
leaveStudioMode(): void {
|
|
1365
|
+
for (const path in this.groups) {
|
|
1366
|
+
const obj = this.groups[path];
|
|
1367
|
+
if (!(obj instanceof ObjectGroup)) continue;
|
|
1368
|
+
obj.leaveStudioMode();
|
|
1369
|
+
}
|
|
1370
|
+
this._isStudioMode = false;
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
/**
|
|
1374
|
+
* Clear cached Studio materials so they are rebuilt on next enterStudioMode.
|
|
1375
|
+
*/
|
|
1376
|
+
clearStudioMaterialCache(): void {
|
|
1377
|
+
for (const [, material] of this._studioMaterialCache) {
|
|
1378
|
+
material.dispose();
|
|
1379
|
+
}
|
|
1380
|
+
this._studioMaterialCache.clear();
|
|
1381
|
+
this._texturedMaterialKeys.clear();
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
/**
|
|
1385
|
+
* Set edge visibility across all ObjectGroups while in Studio mode.
|
|
1386
|
+
* @param visible - Whether edges should be visible
|
|
1387
|
+
*/
|
|
1388
|
+
setStudioShowEdges(visible: boolean): void {
|
|
1389
|
+
for (const path in this.groups) {
|
|
1390
|
+
const obj = this.groups[path];
|
|
1391
|
+
if (!(obj instanceof ObjectGroup)) continue;
|
|
1392
|
+
obj.setStudioShowEdges(visible);
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
/**
|
|
1397
|
+
* Dispose all Studio mode resources (material cache + texture cache).
|
|
1398
|
+
*/
|
|
1399
|
+
private _disposeStudioResources(): void {
|
|
1400
|
+
// Leave studio mode if still active
|
|
1401
|
+
if (this._isStudioMode) {
|
|
1402
|
+
this.leaveStudioMode();
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
// Dispose cached studio materials
|
|
1406
|
+
for (const [, material] of this._studioMaterialCache) {
|
|
1407
|
+
material.dispose();
|
|
1408
|
+
}
|
|
1409
|
+
this._studioMaterialCache.clear();
|
|
1410
|
+
this._texturedMaterialKeys.clear();
|
|
1411
|
+
|
|
1412
|
+
// Dispose texture cache
|
|
1413
|
+
if (this._textureCache) {
|
|
1414
|
+
this._textureCache.disposeFull();
|
|
1415
|
+
this._textureCache = null;
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
this._isStudioMode = false;
|
|
1419
|
+
}
|
|
1021
1420
|
}
|
|
1022
1421
|
|
|
1023
1422
|
/**
|