three-cad-viewer 4.3.8 → 4.3.9
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/core/studio-manager.d.ts +7 -0
- package/dist/core/types.d.ts +11 -0
- package/dist/core/viewer.d.ts +1 -0
- package/dist/index.d.ts +5 -5
- package/dist/rendering/environment.d.ts +3 -19
- package/dist/rendering/material-factory.d.ts +8 -6
- package/dist/rendering/raycast.d.ts +1 -1
- package/dist/rendering/studio-composer.d.ts +1 -1
- package/dist/rendering/tree-model.d.ts +1 -1
- package/dist/three-cad-viewer.esm.js +866 -275
- package/dist/three-cad-viewer.esm.js.map +1 -1
- package/dist/three-cad-viewer.esm.min.js +2 -2
- package/dist/three-cad-viewer.js +866 -275
- package/dist/three-cad-viewer.min.js +2 -2
- package/dist/tools/cad_tools/tools.d.ts +1 -1
- package/dist/utils/utils.d.ts +1 -1
- package/package.json +4 -2
|
@@ -81590,9 +81590,13 @@ function toQuaternionTuple(arr) {
|
|
|
81590
81590
|
}
|
|
81591
81591
|
function isEqual(obj1, obj2, tol = 1e-9) {
|
|
81592
81592
|
if (Array.isArray(obj1) && Array.isArray(obj2)) {
|
|
81593
|
-
return (obj1.length === obj2.length &&
|
|
81593
|
+
return (obj1.length === obj2.length &&
|
|
81594
|
+
obj1.every((v, i) => isEqual(v, obj2[i], tol)));
|
|
81594
81595
|
}
|
|
81595
|
-
else if (obj1 !== null &&
|
|
81596
|
+
else if (obj1 !== null &&
|
|
81597
|
+
obj2 !== null &&
|
|
81598
|
+
typeof obj1 === "object" &&
|
|
81599
|
+
typeof obj2 === "object") {
|
|
81596
81600
|
const rec1 = obj1;
|
|
81597
81601
|
const rec2 = obj2;
|
|
81598
81602
|
const keys1 = Object.keys(rec1);
|
|
@@ -81627,7 +81631,10 @@ function disposeGeometry(geometry) {
|
|
|
81627
81631
|
gpuTracker.untrack("geometry", geometry);
|
|
81628
81632
|
geometry.dispose();
|
|
81629
81633
|
for (const attr of Object.values(geometry.attributes)) {
|
|
81630
|
-
if (attr &&
|
|
81634
|
+
if (attr &&
|
|
81635
|
+
typeof attr === "object" &&
|
|
81636
|
+
"dispose" in attr &&
|
|
81637
|
+
typeof attr.dispose === "function") {
|
|
81631
81638
|
attr.dispose();
|
|
81632
81639
|
}
|
|
81633
81640
|
}
|
|
@@ -81636,12 +81643,25 @@ function disposeGeometry(geometry) {
|
|
|
81636
81643
|
/** All texture map property names on MaterialLike (for iteration) */
|
|
81637
81644
|
const MATERIAL_TEXTURE_KEYS = [
|
|
81638
81645
|
// MeshStandardMaterial
|
|
81639
|
-
"map",
|
|
81640
|
-
"
|
|
81646
|
+
"map",
|
|
81647
|
+
"normalMap",
|
|
81648
|
+
"roughnessMap",
|
|
81649
|
+
"metalnessMap",
|
|
81650
|
+
"aoMap",
|
|
81651
|
+
"emissiveMap",
|
|
81652
|
+
"alphaMap",
|
|
81653
|
+
"bumpMap",
|
|
81641
81654
|
// MeshPhysicalMaterial
|
|
81642
|
-
"transmissionMap",
|
|
81643
|
-
"
|
|
81644
|
-
"
|
|
81655
|
+
"transmissionMap",
|
|
81656
|
+
"clearcoatMap",
|
|
81657
|
+
"clearcoatRoughnessMap",
|
|
81658
|
+
"clearcoatNormalMap",
|
|
81659
|
+
"thicknessMap",
|
|
81660
|
+
"specularIntensityMap",
|
|
81661
|
+
"specularColorMap",
|
|
81662
|
+
"sheenColorMap",
|
|
81663
|
+
"sheenRoughnessMap",
|
|
81664
|
+
"anisotropyMap",
|
|
81645
81665
|
];
|
|
81646
81666
|
/**
|
|
81647
81667
|
* Dispose a material and detach its texture references.
|
|
@@ -81770,14 +81790,16 @@ function isPoints(obj) {
|
|
|
81770
81790
|
* Accepts Object3D to allow use in controls where camera type is broader.
|
|
81771
81791
|
*/
|
|
81772
81792
|
function isOrthographicCamera(obj) {
|
|
81773
|
-
return "isOrthographicCamera" in obj &&
|
|
81793
|
+
return ("isOrthographicCamera" in obj &&
|
|
81794
|
+
obj.isOrthographicCamera === true);
|
|
81774
81795
|
}
|
|
81775
81796
|
/**
|
|
81776
81797
|
* Type guard to check if an object is a PerspectiveCamera.
|
|
81777
81798
|
* Accepts Object3D to allow use in controls where camera type is broader.
|
|
81778
81799
|
*/
|
|
81779
81800
|
function isPerspectiveCamera(obj) {
|
|
81780
|
-
return "isPerspectiveCamera" in obj &&
|
|
81801
|
+
return ("isPerspectiveCamera" in obj &&
|
|
81802
|
+
obj.isPerspectiveCamera === true);
|
|
81781
81803
|
}
|
|
81782
81804
|
/**
|
|
81783
81805
|
* Type guard to check if an Object3D is a LineSegments2 (fat line).
|
|
@@ -81795,7 +81817,8 @@ function hasColor(material) {
|
|
|
81795
81817
|
* Type guard to check if a material is a MeshStandardMaterial.
|
|
81796
81818
|
*/
|
|
81797
81819
|
function isMeshStandardMaterial(material) {
|
|
81798
|
-
return "isMeshStandardMaterial" in material &&
|
|
81820
|
+
return ("isMeshStandardMaterial" in material &&
|
|
81821
|
+
material.isMeshStandardMaterial === true);
|
|
81799
81822
|
}
|
|
81800
81823
|
const KeyMapper = new _KeyMapper();
|
|
81801
81824
|
class EventListenerManager {
|
|
@@ -82021,7 +82044,9 @@ class ZebraTool {
|
|
|
82021
82044
|
if (mesh.userData.excludeFromZebra)
|
|
82022
82045
|
return;
|
|
82023
82046
|
// Store original material (handle array case by taking first)
|
|
82024
|
-
const currentMaterial = Array.isArray(mesh.material)
|
|
82047
|
+
const currentMaterial = Array.isArray(mesh.material)
|
|
82048
|
+
? mesh.material[0]
|
|
82049
|
+
: mesh.material;
|
|
82025
82050
|
if (!this.originalMaterials.has(mesh.uuid)) {
|
|
82026
82051
|
this.originalMaterials.set(mesh.uuid, currentMaterial);
|
|
82027
82052
|
}
|
|
@@ -82033,7 +82058,8 @@ class ZebraTool {
|
|
|
82033
82058
|
if (hasColor(currentMaterial)) {
|
|
82034
82059
|
baseColor = currentMaterial.color.clone();
|
|
82035
82060
|
}
|
|
82036
|
-
else if (isMeshStandardMaterial(currentMaterial) &&
|
|
82061
|
+
else if (isMeshStandardMaterial(currentMaterial) &&
|
|
82062
|
+
currentMaterial.map) {
|
|
82037
82063
|
// If there's a texture but no color, use white as base
|
|
82038
82064
|
baseColor = new Color(1, 1, 1);
|
|
82039
82065
|
}
|
|
@@ -82386,7 +82412,8 @@ class ObjectGroup extends Group {
|
|
|
82386
82412
|
* Skips MeshBasicMaterial and other non-PBR materials.
|
|
82387
82413
|
*/
|
|
82388
82414
|
_forEachStandardMaterial(callback) {
|
|
82389
|
-
if (this.front &&
|
|
82415
|
+
if (this.front &&
|
|
82416
|
+
this.front.material instanceof MeshStandardMaterial) {
|
|
82390
82417
|
callback(this.front.material);
|
|
82391
82418
|
}
|
|
82392
82419
|
// back can also be MeshStandardMaterial (e.g., for polygon rendering)
|
|
@@ -82401,9 +82428,7 @@ class ObjectGroup extends Group {
|
|
|
82401
82428
|
highlight(flag) {
|
|
82402
82429
|
const hColor = this._getHighlightColor();
|
|
82403
82430
|
// Find primary material (front face, vertices, or edges)
|
|
82404
|
-
const primaryMaterial = this.front?.material ||
|
|
82405
|
-
this.vertices?.material ||
|
|
82406
|
-
this.edgeMaterial;
|
|
82431
|
+
const primaryMaterial = this.front?.material || this.vertices?.material || this.edgeMaterial;
|
|
82407
82432
|
if (primaryMaterial) {
|
|
82408
82433
|
this.widen(flag);
|
|
82409
82434
|
this._applyColorToMaterial(primaryMaterial, flag ? hColor : this.originalColor);
|
|
@@ -82549,12 +82574,29 @@ class ObjectGroup extends Group {
|
|
|
82549
82574
|
child1.material.visible = flag;
|
|
82550
82575
|
}
|
|
82551
82576
|
}
|
|
82552
|
-
if (this.back
|
|
82553
|
-
|
|
82554
|
-
|
|
82577
|
+
if (this.back) {
|
|
82578
|
+
// Hide-path: always hide the back when the shape is hidden, even
|
|
82579
|
+
// when !renderback. Otherwise a back face left visible by a prior
|
|
82580
|
+
// setBackVisible(true) (clip tab) would remain visible after the
|
|
82581
|
+
// front goes hidden, appearing as a ghost.
|
|
82582
|
+
// Show-path: only flip back to visible when renderback is set;
|
|
82583
|
+
// when !renderback, leave back.visible alone — the clip-tab's
|
|
82584
|
+
// setBackVisible() owns it.
|
|
82585
|
+
if (!flag) {
|
|
82586
|
+
if (this._isStudioMode) {
|
|
82587
|
+
this.back.visible = false;
|
|
82588
|
+
}
|
|
82589
|
+
else {
|
|
82590
|
+
this.back.material.visible = false;
|
|
82591
|
+
}
|
|
82555
82592
|
}
|
|
82556
|
-
else {
|
|
82557
|
-
this.
|
|
82593
|
+
else if (this.renderback) {
|
|
82594
|
+
if (this._isStudioMode) {
|
|
82595
|
+
this.back.visible = true;
|
|
82596
|
+
}
|
|
82597
|
+
else {
|
|
82598
|
+
this.back.material.visible = true;
|
|
82599
|
+
}
|
|
82558
82600
|
}
|
|
82559
82601
|
}
|
|
82560
82602
|
}
|
|
@@ -82768,10 +82810,16 @@ class ObjectGroup extends Group {
|
|
|
82768
82810
|
this._cadBackMaterial = this.back.material;
|
|
82769
82811
|
}
|
|
82770
82812
|
// Save original colors used by highlight/unhighlight
|
|
82771
|
-
this._cadOriginalColor = this.originalColor
|
|
82772
|
-
|
|
82813
|
+
this._cadOriginalColor = this.originalColor
|
|
82814
|
+
? this.originalColor.clone()
|
|
82815
|
+
: null;
|
|
82816
|
+
this._cadOriginalBackColor = this.originalBackColor
|
|
82817
|
+
? this.originalBackColor.clone()
|
|
82818
|
+
: null;
|
|
82773
82819
|
// Save edge visibility state
|
|
82774
|
-
this._cadEdgesVisible = this.edgeMaterial
|
|
82820
|
+
this._cadEdgesVisible = this.edgeMaterial
|
|
82821
|
+
? this.edgeMaterial.visible
|
|
82822
|
+
: null;
|
|
82775
82823
|
// --- Swap front material ---
|
|
82776
82824
|
if (this.front && studioFront) {
|
|
82777
82825
|
// Transfer per-object visibility to mesh.visible (NOT material.visible)
|
|
@@ -83288,12 +83336,18 @@ class TextureCache {
|
|
|
83288
83336
|
* @returns THREE.SRGBColorSpace or THREE.LinearSRGBColorSpace
|
|
83289
83337
|
*/
|
|
83290
83338
|
function getColorSpaceForMap(mapName) {
|
|
83291
|
-
return THREEJS_SRGB_MAPS.has(mapName)
|
|
83339
|
+
return THREEJS_SRGB_MAPS.has(mapName)
|
|
83340
|
+
? SRGBColorSpace
|
|
83341
|
+
: LinearSRGBColorSpace;
|
|
83292
83342
|
}
|
|
83293
83343
|
|
|
83294
83344
|
/** threejs-materials property keys that hold [r,g,b] color arrays (linear RGB). */
|
|
83295
83345
|
const COLOR_ARRAY_KEYS = new Set([
|
|
83296
|
-
"color",
|
|
83346
|
+
"color",
|
|
83347
|
+
"specularColor",
|
|
83348
|
+
"sheenColor",
|
|
83349
|
+
"emissive",
|
|
83350
|
+
"attenuationColor",
|
|
83297
83351
|
]);
|
|
83298
83352
|
/** Map from threejs-materials property names to Three.js texture map property names. */
|
|
83299
83353
|
const PROPERTY_TO_MAP = {
|
|
@@ -83368,7 +83422,7 @@ class MaterialFactory {
|
|
|
83368
83422
|
* Create a standard material for back faces with PBR properties.
|
|
83369
83423
|
* Used for polygon rendering where back faces need full shading.
|
|
83370
83424
|
*/
|
|
83371
|
-
createBackFaceStandardMaterial({ color, alpha, polygonOffsetUnits = 2.0, visible = true }, label) {
|
|
83425
|
+
createBackFaceStandardMaterial({ color, alpha, polygonOffsetUnits = 2.0, visible = true, }, label) {
|
|
83372
83426
|
const material = new MeshStandardMaterial({
|
|
83373
83427
|
...this._createBaseProps(alpha),
|
|
83374
83428
|
color: color,
|
|
@@ -83386,7 +83440,7 @@ class MaterialFactory {
|
|
|
83386
83440
|
* Create a basic material for back faces (no lighting/PBR).
|
|
83387
83441
|
* Used for shape rendering where back faces are simple fills.
|
|
83388
83442
|
*/
|
|
83389
|
-
createBackFaceBasicMaterial({ color, alpha, polygonOffsetUnits = 2.0, visible = true }, label) {
|
|
83443
|
+
createBackFaceBasicMaterial({ color, alpha, polygonOffsetUnits = 2.0, visible = true, }, label) {
|
|
83390
83444
|
const material = new MeshBasicMaterial({
|
|
83391
83445
|
...this._createBaseProps(alpha),
|
|
83392
83446
|
color: color,
|
|
@@ -83413,7 +83467,7 @@ class MaterialFactory {
|
|
|
83413
83467
|
/**
|
|
83414
83468
|
* Create a fat line material (LineMaterial from Three.js examples).
|
|
83415
83469
|
*/
|
|
83416
|
-
createEdgeMaterial({ lineWidth, color, vertexColors = false, visible = true, resolution }, label) {
|
|
83470
|
+
createEdgeMaterial({ lineWidth, color, vertexColors = false, visible = true, resolution, }, label) {
|
|
83417
83471
|
const material = new LineMaterial({
|
|
83418
83472
|
linewidth: lineWidth,
|
|
83419
83473
|
transparent: true,
|
|
@@ -83487,7 +83541,7 @@ class MaterialFactory {
|
|
|
83487
83541
|
* @param label - Optional label for GPU tracking
|
|
83488
83542
|
* @returns Configured MeshPhysicalMaterial (or MeshBasicMaterial if unlit)
|
|
83489
83543
|
*/
|
|
83490
|
-
async createStudioMaterial({ materialDef, fallbackColor, fallbackAlpha, textureCache }, label) {
|
|
83544
|
+
async createStudioMaterial({ materialDef, fallbackColor, fallbackAlpha, textureCache, }, label) {
|
|
83491
83545
|
const def = materialDef;
|
|
83492
83546
|
const side = def.doubleSided ? DoubleSide : FrontSide;
|
|
83493
83547
|
// --- Resolve base color and opacity ---
|
|
@@ -83635,11 +83689,13 @@ class MaterialFactory {
|
|
|
83635
83689
|
* @param values - Scalar PBR values from threejs-materials
|
|
83636
83690
|
* @param textures - Texture map references from threejs-materials
|
|
83637
83691
|
* @param textureRepeat - Optional [u, v] texture tiling applied to all loaded textures
|
|
83692
|
+
* @param textureRotation - Optional texture rotation in radians (counterclockwise),
|
|
83693
|
+
* pivoting around the texture center (0.5, 0.5)
|
|
83638
83694
|
* @param textureCache - TextureCache for resolving data URI textures
|
|
83639
83695
|
* @param label - Optional label for GPU tracking
|
|
83640
83696
|
* @returns Configured MeshPhysicalMaterial
|
|
83641
83697
|
*/
|
|
83642
|
-
async createStudioMaterialFromMaterialX(values, textures, textureRepeat, textureCache, label) {
|
|
83698
|
+
async createStudioMaterialFromMaterialX(values, textures, textureRepeat, textureRotation, textureCache, label) {
|
|
83643
83699
|
// --- Build material options from scalar values ---
|
|
83644
83700
|
const matOptions = {
|
|
83645
83701
|
flatShading: false,
|
|
@@ -83655,14 +83711,17 @@ class MaterialFactory {
|
|
|
83655
83711
|
}
|
|
83656
83712
|
for (const [key, value] of Object.entries(values)) {
|
|
83657
83713
|
// Skip displacement properties (not supported, would waste GPU memory)
|
|
83658
|
-
if (key === "displacement" ||
|
|
83714
|
+
if (key === "displacement" ||
|
|
83715
|
+
key === "displacementScale" ||
|
|
83716
|
+
key === "displacementBias")
|
|
83659
83717
|
continue;
|
|
83660
83718
|
// Color arrays → THREE.Color (already linear, no sRGB conversion)
|
|
83661
83719
|
if (COLOR_ARRAY_KEYS.has(key) && Array.isArray(value)) {
|
|
83662
83720
|
const [r, g, b] = value;
|
|
83663
83721
|
matOptions[key] = new Color(r, g, b);
|
|
83664
83722
|
}
|
|
83665
|
-
else if ((key === "normalScale" || key === "clearcoatNormalScale") &&
|
|
83723
|
+
else if ((key === "normalScale" || key === "clearcoatNormalScale") &&
|
|
83724
|
+
Array.isArray(value)) {
|
|
83666
83725
|
matOptions[key] = new Vector2(value[0], value[1]);
|
|
83667
83726
|
}
|
|
83668
83727
|
else if (key === "iridescenceThicknessRange" && Array.isArray(value)) {
|
|
@@ -83681,7 +83740,8 @@ class MaterialFactory {
|
|
|
83681
83740
|
matOptions.opacity = 1.0;
|
|
83682
83741
|
matOptions.depthWrite = true;
|
|
83683
83742
|
}
|
|
83684
|
-
else if (transparentVal === true ||
|
|
83743
|
+
else if (transparentVal === true ||
|
|
83744
|
+
(typeof opacityVal === "number" && opacityVal < 1.0)) {
|
|
83685
83745
|
matOptions.transparent = true;
|
|
83686
83746
|
matOptions.depthWrite = false;
|
|
83687
83747
|
}
|
|
@@ -83706,10 +83766,19 @@ class MaterialFactory {
|
|
|
83706
83766
|
: "normalTexture";
|
|
83707
83767
|
let tex = await textureCache.get(textureRef, roleForCache);
|
|
83708
83768
|
if (tex) {
|
|
83709
|
-
|
|
83769
|
+
const needsRepeat = textureRepeat &&
|
|
83770
|
+
(textureRepeat[0] !== 1 || textureRepeat[1] !== 1);
|
|
83771
|
+
const needsRotation = !!textureRotation;
|
|
83772
|
+
if (needsRepeat || needsRotation) {
|
|
83710
83773
|
// Clone to avoid mutating shared cached texture
|
|
83711
83774
|
tex = tex.clone();
|
|
83712
|
-
|
|
83775
|
+
if (needsRepeat) {
|
|
83776
|
+
tex.repeat.set(textureRepeat[0], textureRepeat[1]);
|
|
83777
|
+
}
|
|
83778
|
+
if (needsRotation) {
|
|
83779
|
+
tex.center.set(0.5, 0.5); // pivot around texture center
|
|
83780
|
+
tex.rotation = textureRotation;
|
|
83781
|
+
}
|
|
83713
83782
|
}
|
|
83714
83783
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
83715
83784
|
material[mapName] = tex;
|
|
@@ -83921,7 +83990,7 @@ const MATERIAL_PRESETS = {
|
|
|
83921
83990
|
// ---------------------------------------------------------------------------
|
|
83922
83991
|
// Metals -- Polished
|
|
83923
83992
|
// ---------------------------------------------------------------------------
|
|
83924
|
-
|
|
83993
|
+
chrome: {
|
|
83925
83994
|
name: "Chrome",
|
|
83926
83995
|
color: [0.98, 0.98, 0.98, 1],
|
|
83927
83996
|
metalness: 1.0,
|
|
@@ -83939,19 +84008,19 @@ const MATERIAL_PRESETS = {
|
|
|
83939
84008
|
metalness: 1.0,
|
|
83940
84009
|
roughness: 0.1,
|
|
83941
84010
|
},
|
|
83942
|
-
|
|
84011
|
+
gold: {
|
|
83943
84012
|
name: "Gold",
|
|
83944
84013
|
color: [1, 0.93, 0, 1],
|
|
83945
84014
|
metalness: 1.0,
|
|
83946
84015
|
roughness: 0.1,
|
|
83947
84016
|
},
|
|
83948
|
-
|
|
84017
|
+
copper: {
|
|
83949
84018
|
name: "Copper",
|
|
83950
84019
|
color: [0.98, 0.82, 0.76, 1],
|
|
83951
84020
|
metalness: 1.0,
|
|
83952
84021
|
roughness: 0.15,
|
|
83953
84022
|
},
|
|
83954
|
-
|
|
84023
|
+
brass: {
|
|
83955
84024
|
name: "Brass",
|
|
83956
84025
|
color: [0.95, 0.9, 0.7, 1],
|
|
83957
84026
|
metalness: 1.0,
|
|
@@ -83979,13 +84048,13 @@ const MATERIAL_PRESETS = {
|
|
|
83979
84048
|
metalness: 0.9,
|
|
83980
84049
|
roughness: 0.7,
|
|
83981
84050
|
},
|
|
83982
|
-
|
|
84051
|
+
titanium: {
|
|
83983
84052
|
name: "Titanium",
|
|
83984
84053
|
color: [0.81, 0.79, 0.77, 1],
|
|
83985
84054
|
metalness: 1.0,
|
|
83986
84055
|
roughness: 0.45,
|
|
83987
84056
|
},
|
|
83988
|
-
|
|
84057
|
+
galvanized: {
|
|
83989
84058
|
name: "Galvanized",
|
|
83990
84059
|
color: [0.88, 0.88, 0.9, 1],
|
|
83991
84060
|
metalness: 0.8,
|
|
@@ -84012,7 +84081,7 @@ const MATERIAL_PRESETS = {
|
|
|
84012
84081
|
metalness: 0.0,
|
|
84013
84082
|
roughness: 0.4,
|
|
84014
84083
|
},
|
|
84015
|
-
|
|
84084
|
+
nylon: {
|
|
84016
84085
|
name: "Nylon",
|
|
84017
84086
|
color: [0.95, 0.94, 0.92, 1],
|
|
84018
84087
|
metalness: 0.0,
|
|
@@ -84122,7 +84191,7 @@ const MATERIAL_PRESETS = {
|
|
|
84122
84191
|
roughness: 0.35,
|
|
84123
84192
|
anisotropy: 0.3,
|
|
84124
84193
|
},
|
|
84125
|
-
|
|
84194
|
+
concrete: {
|
|
84126
84195
|
name: "Concrete",
|
|
84127
84196
|
color: [0.83, 0.82, 0.8, 1],
|
|
84128
84197
|
metalness: 0.0,
|
|
@@ -84175,6 +84244,7 @@ varying vec3 vTriplanarNormal;
|
|
|
84175
84244
|
uniform vec3 triplanarOffset;
|
|
84176
84245
|
uniform float triplanarScale;
|
|
84177
84246
|
uniform vec2 triplanarRepeat;
|
|
84247
|
+
uniform float triplanarRotation;
|
|
84178
84248
|
|
|
84179
84249
|
// normalMatrix is only declared in the fragment shader for object-space
|
|
84180
84250
|
// normal maps. We need it for triplanar tangent-space normal mapping too.
|
|
@@ -84197,6 +84267,20 @@ void initTriplanarUVs() {
|
|
|
84197
84267
|
tri_uvX = p.yz * r;
|
|
84198
84268
|
tri_uvY = p.xz * r;
|
|
84199
84269
|
tri_uvZ = p.xy * r;
|
|
84270
|
+
|
|
84271
|
+
// Apply rotation (counterclockwise, radians) if non-zero. The texture
|
|
84272
|
+
// coords are unbounded in triplanar (derived from world-space position),
|
|
84273
|
+
// so the pivot for rotation is the UV origin — for a tiled/repeating
|
|
84274
|
+
// texture this is visually equivalent to any other pivot modulo tile
|
|
84275
|
+
// wrapping, and is the simplest choice.
|
|
84276
|
+
if (triplanarRotation != 0.0) {
|
|
84277
|
+
float c = cos(triplanarRotation);
|
|
84278
|
+
float s = sin(triplanarRotation);
|
|
84279
|
+
mat2 rot = mat2(c, -s, s, c);
|
|
84280
|
+
tri_uvX = rot * tri_uvX;
|
|
84281
|
+
tri_uvY = rot * tri_uvY;
|
|
84282
|
+
tri_uvZ = rot * tri_uvZ;
|
|
84283
|
+
}
|
|
84200
84284
|
}
|
|
84201
84285
|
|
|
84202
84286
|
// Sample a texture using the global triplanar UVs and blend weights.
|
|
@@ -84397,14 +84481,24 @@ function applyTriplanarMapping(material, geometry) {
|
|
|
84397
84481
|
// Uniform values (captured by closure, per-object)
|
|
84398
84482
|
const offset = bb.min.clone();
|
|
84399
84483
|
const scale = 1.0 / maxDim;
|
|
84400
|
-
// Read texture repeat from the first available texture map
|
|
84401
|
-
|
|
84402
|
-
|
|
84484
|
+
// Read texture repeat and rotation from the first available texture map.
|
|
84485
|
+
// texture.rotation is set by the MaterialX path when textureRotation is
|
|
84486
|
+
// provided; for triplanar sampling we read it back and apply via uniform
|
|
84487
|
+
// since the triplanar shader bypasses three.js's texture-matrix transform.
|
|
84488
|
+
const firstMap = material.map ??
|
|
84489
|
+
material.roughnessMap ??
|
|
84490
|
+
material.normalMap ??
|
|
84491
|
+
material.metalnessMap ??
|
|
84492
|
+
material.emissiveMap ??
|
|
84493
|
+
material.aoMap;
|
|
84494
|
+
const repeat = firstMap?.repeat?.clone() ?? new Vector2(1, 1);
|
|
84495
|
+
const rotation = firstMap?.rotation ?? 0;
|
|
84403
84496
|
material.onBeforeCompile = (shader) => {
|
|
84404
84497
|
// Custom uniforms
|
|
84405
84498
|
shader.uniforms.triplanarOffset = { value: offset };
|
|
84406
84499
|
shader.uniforms.triplanarScale = { value: scale };
|
|
84407
84500
|
shader.uniforms.triplanarRepeat = { value: repeat };
|
|
84501
|
+
shader.uniforms.triplanarRotation = { value: rotation };
|
|
84408
84502
|
// --- Vertex shader ---
|
|
84409
84503
|
// Declare varyings
|
|
84410
84504
|
shader.vertexShader = shader.vertexShader.replace("#include <common>", `#include <common>\n${TRIPLANAR_VARYINGS}`);
|
|
@@ -84475,11 +84569,22 @@ class CompoundGroup extends Group {
|
|
|
84475
84569
|
*/
|
|
84476
84570
|
/** Texture field names on MaterialAppearance that require UV coordinates. */
|
|
84477
84571
|
const TEXTURE_FIELDS = [
|
|
84478
|
-
"map",
|
|
84479
|
-
"
|
|
84480
|
-
"
|
|
84481
|
-
"
|
|
84482
|
-
"
|
|
84572
|
+
"map",
|
|
84573
|
+
"normalMap",
|
|
84574
|
+
"aoMap",
|
|
84575
|
+
"metalnessMap",
|
|
84576
|
+
"roughnessMap",
|
|
84577
|
+
"emissiveMap",
|
|
84578
|
+
"transmissionMap",
|
|
84579
|
+
"clearcoatMap",
|
|
84580
|
+
"clearcoatRoughnessMap",
|
|
84581
|
+
"clearcoatNormalMap",
|
|
84582
|
+
"thicknessMap",
|
|
84583
|
+
"specularIntensityMap",
|
|
84584
|
+
"specularColorMap",
|
|
84585
|
+
"sheenColorMap",
|
|
84586
|
+
"sheenRoughnessMap",
|
|
84587
|
+
"anisotropyMap",
|
|
84483
84588
|
];
|
|
84484
84589
|
/** Check whether a resolved MaterialAppearance references any texture. */
|
|
84485
84590
|
function materialHasTexture(def) {
|
|
@@ -85211,7 +85316,7 @@ class NestedGroup {
|
|
|
85211
85316
|
* 3. Clone BackSide variant for renderback objects
|
|
85212
85317
|
* 4. Auto-generate box-projected UVs when textured but geometry has no UVs
|
|
85213
85318
|
*/
|
|
85214
|
-
async enterStudioMode(textureMapping = "
|
|
85319
|
+
async enterStudioMode(textureMapping = "parametric") {
|
|
85215
85320
|
// Create TextureCache lazily
|
|
85216
85321
|
if (!this._textureCache) {
|
|
85217
85322
|
this._textureCache = new TextureCache();
|
|
@@ -85245,7 +85350,8 @@ class NestedGroup {
|
|
|
85245
85350
|
try {
|
|
85246
85351
|
if (resolved && isMaterialXMaterial(resolved)) {
|
|
85247
85352
|
// --- threejs-materials path ---
|
|
85248
|
-
studioMaterial =
|
|
85353
|
+
studioMaterial =
|
|
85354
|
+
await this.materialFactory.createStudioMaterialFromMaterialX(resolved.values, resolved.textures, resolved.textureRepeat, resolved.textureRotation, this._textureCache);
|
|
85249
85355
|
if (materialXHasTextures(resolved)) {
|
|
85250
85356
|
this._texturedMaterialKeys.add(sharingKey);
|
|
85251
85357
|
}
|
|
@@ -85295,7 +85401,8 @@ class NestedGroup {
|
|
|
85295
85401
|
if (textured) {
|
|
85296
85402
|
logger.debug(`Studio "${path}": ${needsTriplanar ? "using triplanar" : "using parametric UVs"}`);
|
|
85297
85403
|
}
|
|
85298
|
-
if (needsTriplanar &&
|
|
85404
|
+
if (needsTriplanar &&
|
|
85405
|
+
studioMaterial instanceof MeshPhysicalMaterial) {
|
|
85299
85406
|
const triKey = `${sharingKey}:tri:${path}`;
|
|
85300
85407
|
let triMat = this._studioMaterialCache.get(triKey);
|
|
85301
85408
|
if (!triMat) {
|
|
@@ -85307,7 +85414,8 @@ class NestedGroup {
|
|
|
85307
85414
|
}
|
|
85308
85415
|
// Build back-face variant if needed
|
|
85309
85416
|
let studioBack = null;
|
|
85310
|
-
if (obj.renderback &&
|
|
85417
|
+
if (obj.renderback &&
|
|
85418
|
+
studioMaterial instanceof MeshPhysicalMaterial) {
|
|
85311
85419
|
const backKey = needsTriplanar
|
|
85312
85420
|
? `${sharingKey}:tri:${path}:back`
|
|
85313
85421
|
: `${sharingKey}:back`;
|
|
@@ -85335,7 +85443,9 @@ class NestedGroup {
|
|
|
85335
85443
|
}
|
|
85336
85444
|
}
|
|
85337
85445
|
// Apply to ObjectGroup
|
|
85338
|
-
obj.enterStudioMode(studioMaterial instanceof MeshPhysicalMaterial
|
|
85446
|
+
obj.enterStudioMode(studioMaterial instanceof MeshPhysicalMaterial
|
|
85447
|
+
? studioMaterial
|
|
85448
|
+
: null, studioBack);
|
|
85339
85449
|
}
|
|
85340
85450
|
this._isStudioMode = true;
|
|
85341
85451
|
return [...unresolvedTags];
|
|
@@ -86598,7 +86708,10 @@ class TreeModel {
|
|
|
86598
86708
|
*/
|
|
86599
86709
|
_buildTreeStructure(data) {
|
|
86600
86710
|
const build = (data, path, level) => {
|
|
86601
|
-
const result = [
|
|
86711
|
+
const result = [
|
|
86712
|
+
States.unselected,
|
|
86713
|
+
States.unselected,
|
|
86714
|
+
];
|
|
86602
86715
|
const calcState = (states) => {
|
|
86603
86716
|
for (const s of [0, 1]) {
|
|
86604
86717
|
if (states[States.mixed][s] ||
|
|
@@ -87087,7 +87200,8 @@ class TreeView {
|
|
|
87087
87200
|
this.update = (prefix = null) => {
|
|
87088
87201
|
if (!this.container || !this.model)
|
|
87089
87202
|
return;
|
|
87090
|
-
const visibleElements = this.getVisibleElements().filter((p) => p instanceof HTMLElement &&
|
|
87203
|
+
const visibleElements = this.getVisibleElements().filter((p) => p instanceof HTMLElement &&
|
|
87204
|
+
(prefix == null || (p.dataset.path || "").startsWith(prefix)));
|
|
87091
87205
|
for (const el of visibleElements) {
|
|
87092
87206
|
const path = el.dataset.path || "";
|
|
87093
87207
|
const node = this.findNodeByPath(path);
|
|
@@ -87723,7 +87837,8 @@ class TreeView {
|
|
|
87723
87837
|
this.showChildContainer(node);
|
|
87724
87838
|
const el = this.getDomNode(path);
|
|
87725
87839
|
if (el != null) {
|
|
87726
|
-
this.scrollContainer.scrollTop =
|
|
87840
|
+
this.scrollContainer.scrollTop =
|
|
87841
|
+
el.offsetTop - this.scrollContainer.offsetTop;
|
|
87727
87842
|
}
|
|
87728
87843
|
if (this.debug) {
|
|
87729
87844
|
logger.debug("update => collapsePath");
|
|
@@ -87744,7 +87859,8 @@ class TreeView {
|
|
|
87744
87859
|
this.model.setExpandedLevel(level);
|
|
87745
87860
|
const el = this.getDomNode(this.getNodePath(this.root));
|
|
87746
87861
|
if (el != null) {
|
|
87747
|
-
this.scrollContainer.scrollTop =
|
|
87862
|
+
this.scrollContainer.scrollTop =
|
|
87863
|
+
el.offsetTop - this.scrollContainer.offsetTop;
|
|
87748
87864
|
}
|
|
87749
87865
|
// Multiple updates to ensure all levels are rendered
|
|
87750
87866
|
const maxIterations = level === -1 ? this.maxLevel : level;
|
|
@@ -87977,7 +88093,9 @@ class CenteredPlane extends Plane {
|
|
|
87977
88093
|
*/
|
|
87978
88094
|
// @ts-expect-error -- THREE.Plane.clone() returns `this`, but we need a concrete CenteredPlane
|
|
87979
88095
|
clone() {
|
|
87980
|
-
return new CenteredPlane(this.normal.clone(), this.centeredConstant, [
|
|
88096
|
+
return new CenteredPlane(this.normal.clone(), this.centeredConstant, [
|
|
88097
|
+
...this.center,
|
|
88098
|
+
]);
|
|
87981
88099
|
}
|
|
87982
88100
|
}
|
|
87983
88101
|
// ============================================================================
|
|
@@ -88187,7 +88305,9 @@ class Clipping extends Group {
|
|
|
88187
88305
|
let j = 0;
|
|
88188
88306
|
for (const path in this.nestedGroup.groups) {
|
|
88189
88307
|
const group = this.nestedGroup.groups[path];
|
|
88190
|
-
if (group instanceof ObjectGroup &&
|
|
88308
|
+
if (group instanceof ObjectGroup &&
|
|
88309
|
+
group.subtype === "solid" &&
|
|
88310
|
+
group.front) {
|
|
88191
88311
|
// Store color for each plane-solid combination (mirrors _planeMeshGroup order)
|
|
88192
88312
|
const frontMesh = group.front;
|
|
88193
88313
|
const material = frontMesh.material;
|
|
@@ -88707,7 +88827,8 @@ class ShapeRenderer {
|
|
|
88707
88827
|
}
|
|
88708
88828
|
else {
|
|
88709
88829
|
// Non-binary format: nested number[][] arrays
|
|
88710
|
-
if (!Array.isArray(shape.triangles) ||
|
|
88830
|
+
if (!Array.isArray(shape.triangles) ||
|
|
88831
|
+
!Array.isArray(shape.triangles[0])) {
|
|
88711
88832
|
throw new Error("Expected nested array for triangles in non-binary format");
|
|
88712
88833
|
}
|
|
88713
88834
|
// After validation, we know shape.triangles is number[][] (TypeScript can't infer this)
|
|
@@ -88835,7 +88956,8 @@ class ShapeRenderer {
|
|
|
88835
88956
|
else {
|
|
88836
88957
|
// Non-binary format: nested number[][] arrays
|
|
88837
88958
|
const edgesRaw = shape.edges;
|
|
88838
|
-
if (!Array.isArray(edgesRaw) ||
|
|
88959
|
+
if (!Array.isArray(edgesRaw) ||
|
|
88960
|
+
(edgesRaw.length > 0 && !Array.isArray(edgesRaw[0]))) {
|
|
88839
88961
|
throw new Error("Expected nested array for edges in non-binary format");
|
|
88840
88962
|
}
|
|
88841
88963
|
// After validation, we know this is number[][] (TypeScript can't infer from the check)
|
|
@@ -92980,8 +93102,14 @@ const defaultDirections = {
|
|
|
92980
93102
|
rear: { pos: new Vector3(0, 1, 0), quat: null },
|
|
92981
93103
|
left: { pos: new Vector3(-1, 0, 0), quat: null },
|
|
92982
93104
|
right: { pos: new Vector3(1, 0, 0), quat: null },
|
|
92983
|
-
top: {
|
|
92984
|
-
|
|
93105
|
+
top: {
|
|
93106
|
+
pos: new Vector3(0, 0, 1),
|
|
93107
|
+
quat: new Quaternion(0, 0, 0, 1),
|
|
93108
|
+
},
|
|
93109
|
+
bottom: {
|
|
93110
|
+
pos: new Vector3(0, 0, -1),
|
|
93111
|
+
quat: new Quaternion(1, 0, 0, 0),
|
|
93112
|
+
},
|
|
92985
93113
|
},
|
|
92986
93114
|
legacy: {
|
|
92987
93115
|
// legacy Z up
|
|
@@ -93505,7 +93633,10 @@ class Raycaster {
|
|
|
93505
93633
|
const object = obj.object;
|
|
93506
93634
|
// Accept Mesh (faces), Points (vertices), and Line (edges)
|
|
93507
93635
|
const isValidType = isMesh(object) || isPoints(object) || isLine(object);
|
|
93508
|
-
if (isValidType &&
|
|
93636
|
+
if (isValidType &&
|
|
93637
|
+
object.visible &&
|
|
93638
|
+
!Array.isArray(object.material) &&
|
|
93639
|
+
object.material.visible) {
|
|
93509
93640
|
validObjs.push(obj);
|
|
93510
93641
|
}
|
|
93511
93642
|
}
|
|
@@ -93525,7 +93656,9 @@ class Raycaster {
|
|
|
93525
93656
|
const isValidType = isMesh(obj) || isPoints(obj) || isLine(obj);
|
|
93526
93657
|
if (!isValidType)
|
|
93527
93658
|
continue;
|
|
93528
|
-
if (!obj.visible ||
|
|
93659
|
+
if (!obj.visible ||
|
|
93660
|
+
Array.isArray(obj.material) ||
|
|
93661
|
+
!obj.material.visible)
|
|
93529
93662
|
continue;
|
|
93530
93663
|
const objectGroup = object.object.parent;
|
|
93531
93664
|
if (!isObjectGroup(objectGroup))
|
|
@@ -93554,7 +93687,9 @@ class Raycaster {
|
|
|
93554
93687
|
* Type guard to check if a value is a number array of specific length.
|
|
93555
93688
|
*/
|
|
93556
93689
|
function isNumberArray(value, length) {
|
|
93557
|
-
return Array.isArray(value) &&
|
|
93690
|
+
return (Array.isArray(value) &&
|
|
93691
|
+
value.length === length &&
|
|
93692
|
+
value.every((v) => typeof v === "number"));
|
|
93558
93693
|
}
|
|
93559
93694
|
/**
|
|
93560
93695
|
* Type guard to check if a value is a Record<string, number[]> (bounding box data).
|
|
@@ -93705,9 +93840,17 @@ class Panel {
|
|
|
93705
93840
|
* Skip list for technical fields that should not be rendered in panels.
|
|
93706
93841
|
*/
|
|
93707
93842
|
const SKIP_KEYS = [
|
|
93708
|
-
"type",
|
|
93709
|
-
"
|
|
93710
|
-
"
|
|
93843
|
+
"type",
|
|
93844
|
+
"tool_type",
|
|
93845
|
+
"subtype",
|
|
93846
|
+
"info",
|
|
93847
|
+
"refpoint",
|
|
93848
|
+
"refpoint1",
|
|
93849
|
+
"refpoint2",
|
|
93850
|
+
"shape_type",
|
|
93851
|
+
"geom_type",
|
|
93852
|
+
"groups",
|
|
93853
|
+
"result",
|
|
93711
93854
|
];
|
|
93712
93855
|
/**
|
|
93713
93856
|
* Render entries from a group object into a tbody.
|
|
@@ -94125,7 +94268,10 @@ class Measurement {
|
|
|
94125
94268
|
e.stopPropagation();
|
|
94126
94269
|
};
|
|
94127
94270
|
this._movePanel = () => {
|
|
94128
|
-
if (!this.panel ||
|
|
94271
|
+
if (!this.panel ||
|
|
94272
|
+
!this.viewer ||
|
|
94273
|
+
!this.viewer.camera ||
|
|
94274
|
+
!this.panel.isVisible())
|
|
94129
94275
|
return;
|
|
94130
94276
|
const canvasRect = this.viewer.renderer.domElement.getBoundingClientRect();
|
|
94131
94277
|
const panelRect = this.panel.html.getBoundingClientRect();
|
|
@@ -94325,8 +94471,15 @@ class Measurement {
|
|
|
94325
94471
|
responseData = {
|
|
94326
94472
|
groups: [
|
|
94327
94473
|
{ distance: 2.345, info: "center" },
|
|
94328
|
-
{
|
|
94329
|
-
|
|
94474
|
+
{
|
|
94475
|
+
"point 1": this.point1.toArray(),
|
|
94476
|
+
"point 2": this.point2.toArray(),
|
|
94477
|
+
},
|
|
94478
|
+
{
|
|
94479
|
+
angle: 43.21,
|
|
94480
|
+
"reference 1": "Plane (Face)",
|
|
94481
|
+
"reference 2": "Plane (Face)",
|
|
94482
|
+
},
|
|
94330
94483
|
],
|
|
94331
94484
|
type: "backend_response",
|
|
94332
94485
|
refpoint1: this.point1.toArray(),
|
|
@@ -94345,10 +94498,21 @@ class Measurement {
|
|
|
94345
94498
|
geom_type: "EllipseArc",
|
|
94346
94499
|
refpoint: this.point1.toArray(),
|
|
94347
94500
|
groups: [
|
|
94348
|
-
{
|
|
94501
|
+
{
|
|
94502
|
+
center: this.point1.toArray(),
|
|
94503
|
+
"major radius": 0.4,
|
|
94504
|
+
"minor radius": 0.2,
|
|
94505
|
+
},
|
|
94349
94506
|
{ start: [2.4, -1, 0.0], end: [1.8, -0.8267949192431111, 0.0] },
|
|
94350
94507
|
{ length: 0.6868592404716374 },
|
|
94351
|
-
{
|
|
94508
|
+
{
|
|
94509
|
+
bb: {
|
|
94510
|
+
min: [1.8, -1, 0.0],
|
|
94511
|
+
center: [2.1, -0.9, 0.0],
|
|
94512
|
+
max: [2.4, -0.8, 0.0],
|
|
94513
|
+
size: [0.56, 0.2, 0.0],
|
|
94514
|
+
},
|
|
94515
|
+
},
|
|
94352
94516
|
],
|
|
94353
94517
|
};
|
|
94354
94518
|
}
|
|
@@ -94460,7 +94624,8 @@ class DistanceMeasurement extends Measurement {
|
|
|
94460
94624
|
this.debug = debug;
|
|
94461
94625
|
}
|
|
94462
94626
|
_createPanel() {
|
|
94463
|
-
if (isDistancePanel(this.panel) &&
|
|
94627
|
+
if (isDistancePanel(this.panel) &&
|
|
94628
|
+
isDistanceResponseData(this.responseData)) {
|
|
94464
94629
|
this.panel.createTable(this.responseData);
|
|
94465
94630
|
}
|
|
94466
94631
|
}
|
|
@@ -94520,7 +94685,8 @@ class PropertiesMeasurement extends Measurement {
|
|
|
94520
94685
|
this.debug = debug;
|
|
94521
94686
|
}
|
|
94522
94687
|
_createPanel() {
|
|
94523
|
-
if (isPropertiesPanel(this.panel) &&
|
|
94688
|
+
if (isPropertiesPanel(this.panel) &&
|
|
94689
|
+
isPropertiesResponseData(this.responseData)) {
|
|
94524
94690
|
this.panel.createTable(this.responseData);
|
|
94525
94691
|
}
|
|
94526
94692
|
}
|
|
@@ -94528,7 +94694,8 @@ class PropertiesMeasurement extends Measurement {
|
|
|
94528
94694
|
return 1;
|
|
94529
94695
|
}
|
|
94530
94696
|
_getPoint() {
|
|
94531
|
-
if (isPropertiesResponseData(this.responseData) &&
|
|
94697
|
+
if (isPropertiesResponseData(this.responseData) &&
|
|
94698
|
+
this.responseData.refpoint) {
|
|
94532
94699
|
this.point1 = new Vector3(...this.responseData.refpoint);
|
|
94533
94700
|
}
|
|
94534
94701
|
}
|
|
@@ -94795,7 +94962,7 @@ class Tools {
|
|
|
94795
94962
|
}
|
|
94796
94963
|
}
|
|
94797
94964
|
|
|
94798
|
-
const version = "4.3.
|
|
94965
|
+
const version = "4.3.9";
|
|
94799
94966
|
|
|
94800
94967
|
/**
|
|
94801
94968
|
* Clean room environment for Studio mode PMREM generation.
|
|
@@ -94900,7 +95067,7 @@ function createCove(length, radius, segments, wall) {
|
|
|
94900
95067
|
const indices = [];
|
|
94901
95068
|
const sign = -1 ;
|
|
94902
95069
|
for (let i = 0; i <= segments; i++) {
|
|
94903
|
-
const angle = (i / segments) * Math.PI / 2;
|
|
95070
|
+
const angle = ((i / segments) * Math.PI) / 2;
|
|
94904
95071
|
const h = sign * radius * Math.sin(angle); // horizontal offset toward wall
|
|
94905
95072
|
const y = radius * (1 - Math.cos(angle)); // vertical offset above floor
|
|
94906
95073
|
const nh = sign * Math.sin(angle); // normal toward wall
|
|
@@ -95449,7 +95616,7 @@ function halfToFloat(h) {
|
|
|
95449
95616
|
}
|
|
95450
95617
|
if (exponent === 31) {
|
|
95451
95618
|
// Infinity or NaN
|
|
95452
|
-
return mantissa ? NaN :
|
|
95619
|
+
return mantissa ? NaN : sign ? -Infinity : Infinity;
|
|
95453
95620
|
}
|
|
95454
95621
|
return (sign ? -1 : 1) * Math.pow(2, exponent - 15) * (1 + mantissa / 1024);
|
|
95455
95622
|
}
|
|
@@ -95480,7 +95647,7 @@ function detectDominantLights(data, width, height) {
|
|
|
95480
95647
|
for (let sy = 0; sy < height; sy++) {
|
|
95481
95648
|
const gy = Math.min(Math.floor((sy / height) * GRID_H), GRID_H - 1);
|
|
95482
95649
|
// cos(latitude) weighting: equator has more area than poles
|
|
95483
|
-
const phi = (
|
|
95650
|
+
const phi = (0.5 - sy / height) * Math.PI;
|
|
95484
95651
|
const cosWeight = Math.cos(phi);
|
|
95485
95652
|
for (let sx = 0; sx < width; sx++) {
|
|
95486
95653
|
const gx = Math.min(Math.floor((sx / width) * GRID_W), GRID_W - 1);
|
|
@@ -95519,7 +95686,9 @@ function detectDominantLights(data, width, height) {
|
|
|
95519
95686
|
}
|
|
95520
95687
|
}
|
|
95521
95688
|
// 2. Compute median luminance and threshold
|
|
95522
|
-
const sorted = Array.from(grid)
|
|
95689
|
+
const sorted = Array.from(grid)
|
|
95690
|
+
.filter((v) => v > 0)
|
|
95691
|
+
.sort((a, b) => a - b);
|
|
95523
95692
|
if (sorted.length === 0) {
|
|
95524
95693
|
return { lights: [], wasAnalyzed: true };
|
|
95525
95694
|
}
|
|
@@ -95615,9 +95784,9 @@ function detectDominantLights(data, width, height) {
|
|
|
95615
95784
|
let color;
|
|
95616
95785
|
if (colorTotal > 0) {
|
|
95617
95786
|
color = [
|
|
95618
|
-
c.totalR / colorTotal * 3,
|
|
95619
|
-
c.totalG / colorTotal * 3,
|
|
95620
|
-
c.totalB / colorTotal * 3,
|
|
95787
|
+
(c.totalR / colorTotal) * 3,
|
|
95788
|
+
(c.totalG / colorTotal) * 3,
|
|
95789
|
+
(c.totalB / colorTotal) * 3,
|
|
95621
95790
|
];
|
|
95622
95791
|
// Clamp to 0-1
|
|
95623
95792
|
color = [
|
|
@@ -95778,14 +95947,6 @@ class EnvironmentManager {
|
|
|
95778
95947
|
constructor(options = {}) {
|
|
95779
95948
|
/** Cached PMREM render targets keyed by environment name or URL */
|
|
95780
95949
|
this._cache = new Map();
|
|
95781
|
-
/**
|
|
95782
|
-
* Cached raw equirectangular HDR textures keyed by the same name/URL.
|
|
95783
|
-
* Preserved (not disposed after PMREM generation) so `scene.background`
|
|
95784
|
-
* can sample the original HDR at full source resolution instead of the
|
|
95785
|
-
* 256² PMREM cubemap. Only populated by `_loadHdr` — procedural
|
|
95786
|
-
* environments have no source HDR.
|
|
95787
|
-
*/
|
|
95788
|
-
this._hdrCache = new Map();
|
|
95789
95950
|
/** Cached light detection results keyed by environment name or URL */
|
|
95790
95951
|
this._lightDetectionCache = new Map();
|
|
95791
95952
|
/** In-flight load promises keyed by environment name or URL */
|
|
@@ -95800,13 +95961,6 @@ class EnvironmentManager {
|
|
|
95800
95961
|
this._hdrLoader = null;
|
|
95801
95962
|
/** The last loaded PMREM texture (stateful — used by apply() for IBL) */
|
|
95802
95963
|
this._currentTexture = null;
|
|
95803
|
-
/**
|
|
95804
|
-
* Raw HDR texture corresponding to `_currentTexture`, used for
|
|
95805
|
-
* `scene.background` to keep the backdrop at source resolution. Null when
|
|
95806
|
-
* the current environment is procedural ("studio" RoomEnvironment) — in
|
|
95807
|
-
* that case the background falls back to `_currentTexture` (the PMREM).
|
|
95808
|
-
*/
|
|
95809
|
-
this._currentBackgroundTexture = null;
|
|
95810
95964
|
/** Whether this manager has been disposed */
|
|
95811
95965
|
this._disposed = false;
|
|
95812
95966
|
/**
|
|
@@ -95831,7 +95985,8 @@ class EnvironmentManager {
|
|
|
95831
95985
|
* re-apply once the texture is ready.
|
|
95832
95986
|
*/
|
|
95833
95987
|
this._deferredApply = null;
|
|
95834
|
-
this._userOverrides =
|
|
95988
|
+
this._userOverrides =
|
|
95989
|
+
options.presetUrls ?? {};
|
|
95835
95990
|
this._presetUrls = {
|
|
95836
95991
|
..._buildPresetUrls(false),
|
|
95837
95992
|
...this._userOverrides,
|
|
@@ -95860,7 +96015,6 @@ class EnvironmentManager {
|
|
|
95860
96015
|
}
|
|
95861
96016
|
if (name === "none") {
|
|
95862
96017
|
this._currentTexture = null;
|
|
95863
|
-
this._currentBackgroundTexture = null;
|
|
95864
96018
|
return null;
|
|
95865
96019
|
}
|
|
95866
96020
|
// Check cache first (name is the cache key for presets; URL string for custom)
|
|
@@ -95869,7 +96023,6 @@ class EnvironmentManager {
|
|
|
95869
96023
|
if (cached) {
|
|
95870
96024
|
logger.debug(`Environment "${cacheKey}" loaded from cache`);
|
|
95871
96025
|
this._currentTexture = cached.texture;
|
|
95872
|
-
this._currentBackgroundTexture = this._hdrCache.get(cacheKey) ?? null;
|
|
95873
96026
|
return cached.texture;
|
|
95874
96027
|
}
|
|
95875
96028
|
// Check in-flight promise — await and set _currentTexture
|
|
@@ -95878,7 +96031,6 @@ class EnvironmentManager {
|
|
|
95878
96031
|
logger.debug(`Environment "${cacheKey}" already loading, reusing promise`);
|
|
95879
96032
|
const texture = await inflight;
|
|
95880
96033
|
this._currentTexture = texture;
|
|
95881
|
-
this._currentBackgroundTexture = this._hdrCache.get(cacheKey) ?? null;
|
|
95882
96034
|
return texture;
|
|
95883
96035
|
}
|
|
95884
96036
|
// Start new load
|
|
@@ -95887,7 +96039,6 @@ class EnvironmentManager {
|
|
|
95887
96039
|
try {
|
|
95888
96040
|
const texture = await promise;
|
|
95889
96041
|
this._currentTexture = texture;
|
|
95890
|
-
this._currentBackgroundTexture = this._hdrCache.get(cacheKey) ?? null;
|
|
95891
96042
|
// Self-healing: if apply() was called with "environment" background
|
|
95892
96043
|
// while texture was null, re-apply now that the texture is ready.
|
|
95893
96044
|
if (this._deferredApply) {
|
|
@@ -95927,8 +96078,13 @@ class EnvironmentManager {
|
|
|
95927
96078
|
scene.environmentIntensity = envIntensity;
|
|
95928
96079
|
// HDR maps assume Y-up; rotate 90° around X to align with Z-up scenes.
|
|
95929
96080
|
// Additional rotation for user-controlled azimuthal rotation.
|
|
96081
|
+
// Note: Z-up branch uses "ZYX" Euler order so the matrix is
|
|
96082
|
+
// Rz(rotY) · Rx(PI/2). With the default "XYZ" order, the rotation
|
|
96083
|
+
// would land on the wrong axis (around World -Y, the depth axis,
|
|
96084
|
+
// instead of World Z, the vertical axis) because of how matrix
|
|
96085
|
+
// multiplication composes the X-tilt with the user rotation.
|
|
95930
96086
|
if (upIsZ) {
|
|
95931
|
-
scene.environmentRotation.set(Math.PI / 2, 0, rotY);
|
|
96087
|
+
scene.environmentRotation.set(Math.PI / 2, 0, rotY, "ZYX");
|
|
95932
96088
|
}
|
|
95933
96089
|
else {
|
|
95934
96090
|
scene.environmentRotation.set(0, rotY, 0);
|
|
@@ -95965,13 +96121,14 @@ class EnvironmentManager {
|
|
|
95965
96121
|
break;
|
|
95966
96122
|
case "environment":
|
|
95967
96123
|
if (this._currentTexture) {
|
|
95968
|
-
//
|
|
95969
|
-
//
|
|
95970
|
-
//
|
|
95971
|
-
|
|
96124
|
+
// Use PMREM (CubeUVReflectionMapping) for the background. Raw
|
|
96125
|
+
// equirectangular HDR can't be used here: three.js's WebGLBackground
|
|
96126
|
+
// routes non-cubemap textures through a flat planeMesh path that
|
|
96127
|
+
// ignores scene.backgroundRotation, so env-rotation breaks. PMREM
|
|
96128
|
+
// takes the cubemap path and rotation works correctly.
|
|
95972
96129
|
// Always use render-to-texture with a fixed-FOV bgCamera so the
|
|
95973
96130
|
// background zoom level is identical in perspective and ortho modes.
|
|
95974
|
-
this._setupEnvBackground(scene,
|
|
96131
|
+
this._setupEnvBackground(scene, this._currentTexture, upIsZ, rotY);
|
|
95975
96132
|
this._deferredApply = null;
|
|
95976
96133
|
}
|
|
95977
96134
|
else {
|
|
@@ -96056,15 +96213,11 @@ class EnvironmentManager {
|
|
|
96056
96213
|
this._lightDetectionCache.delete(slug);
|
|
96057
96214
|
logger.debug(`Evicted cached environment "${slug}" for resolution switch`);
|
|
96058
96215
|
}
|
|
96059
|
-
const cachedHdr = this._hdrCache.get(slug);
|
|
96060
|
-
if (cachedHdr) {
|
|
96061
|
-
gpuTracker.untrack("texture", cachedHdr);
|
|
96062
|
-
cachedHdr.dispose();
|
|
96063
|
-
this._hdrCache.delete(slug);
|
|
96064
|
-
}
|
|
96065
96216
|
}
|
|
96066
96217
|
// Reload the current environment at the new resolution
|
|
96067
|
-
if (currentEnvName &&
|
|
96218
|
+
if (currentEnvName &&
|
|
96219
|
+
currentEnvName !== "none" &&
|
|
96220
|
+
currentEnvName !== "studio") {
|
|
96068
96221
|
return this.loadEnvironment(currentEnvName, renderer);
|
|
96069
96222
|
}
|
|
96070
96223
|
return this._currentTexture;
|
|
@@ -96122,7 +96275,9 @@ class EnvironmentManager {
|
|
|
96122
96275
|
const size = renderer.getDrawingBufferSize(_bgSizeVec);
|
|
96123
96276
|
const w = size.x;
|
|
96124
96277
|
const h = size.y;
|
|
96125
|
-
if (!this._bgRenderTarget ||
|
|
96278
|
+
if (!this._bgRenderTarget ||
|
|
96279
|
+
this._bgRenderTarget.width !== w ||
|
|
96280
|
+
this._bgRenderTarget.height !== h) {
|
|
96126
96281
|
this._bgRenderTarget?.dispose();
|
|
96127
96282
|
this._bgRenderTarget = new WebGLRenderTarget(w, h);
|
|
96128
96283
|
}
|
|
@@ -96160,7 +96315,6 @@ class EnvironmentManager {
|
|
|
96160
96315
|
dispose() {
|
|
96161
96316
|
this._disposed = true;
|
|
96162
96317
|
this._currentTexture = null;
|
|
96163
|
-
this._currentBackgroundTexture = null;
|
|
96164
96318
|
this._deferredApply = null;
|
|
96165
96319
|
this._teardownEnvBackground();
|
|
96166
96320
|
this._bgScene = null;
|
|
@@ -96176,13 +96330,6 @@ class EnvironmentManager {
|
|
|
96176
96330
|
logger.debug(`Disposed cached environment render target: ${key}`);
|
|
96177
96331
|
}
|
|
96178
96332
|
this._cache.clear();
|
|
96179
|
-
// Dispose all cached raw HDR textures
|
|
96180
|
-
for (const [key, hdrTexture] of this._hdrCache) {
|
|
96181
|
-
gpuTracker.untrack("texture", hdrTexture);
|
|
96182
|
-
hdrTexture.dispose();
|
|
96183
|
-
logger.debug(`Disposed cached HDR background: ${key}`);
|
|
96184
|
-
}
|
|
96185
|
-
this._hdrCache.clear();
|
|
96186
96333
|
this._lightDetectionCache.clear();
|
|
96187
96334
|
// Clear in-flight promises (they'll resolve but won't be cached)
|
|
96188
96335
|
this._inflight.clear();
|
|
@@ -96218,8 +96365,9 @@ class EnvironmentManager {
|
|
|
96218
96365
|
this._bgScene.background = texture;
|
|
96219
96366
|
this._bgScene.backgroundIntensity = 1.0;
|
|
96220
96367
|
this._bgScene.backgroundBlurriness = 0;
|
|
96368
|
+
// See apply() for the "ZYX" rationale.
|
|
96221
96369
|
if (upIsZ) {
|
|
96222
|
-
this._bgScene.backgroundRotation.set(Math.PI / 2, 0, rotY);
|
|
96370
|
+
this._bgScene.backgroundRotation.set(Math.PI / 2, 0, rotY, "ZYX");
|
|
96223
96371
|
}
|
|
96224
96372
|
else {
|
|
96225
96373
|
this._bgScene.backgroundRotation.set(0, rotY, 0);
|
|
@@ -96337,10 +96485,9 @@ class EnvironmentManager {
|
|
|
96337
96485
|
* Load an HDR file and generate a PMREM texture from it.
|
|
96338
96486
|
*
|
|
96339
96487
|
* Uses HDRLoader to fetch the .hdr file, then PMREMGenerator.fromEquirectangular()
|
|
96340
|
-
* to create the PMREM cubemap
|
|
96341
|
-
*
|
|
96342
|
-
*
|
|
96343
|
-
* instead of the 256² PMREM cubemap.
|
|
96488
|
+
* to create the PMREM cubemap. The source equirectangular HDR is disposed
|
|
96489
|
+
* after PMREM generation. The PMREM texture itself serves as both the IBL
|
|
96490
|
+
* environment and the background (in "environment" mode).
|
|
96344
96491
|
*
|
|
96345
96492
|
* @param url - URL of the .hdr file
|
|
96346
96493
|
* @param cacheKey - Cache key for the resulting PMREM render target
|
|
@@ -96366,19 +96513,21 @@ class EnvironmentManager {
|
|
|
96366
96513
|
const renderTarget = pmremGenerator.fromEquirectangular(hdrTexture);
|
|
96367
96514
|
// Analyze HDR pixel data for dominant light sources BEFORE disposing.
|
|
96368
96515
|
// hdrTexture.image.data is Uint16Array (HalfFloatType) from HDRLoader.
|
|
96369
|
-
if (hdrTexture.image?.data &&
|
|
96516
|
+
if (hdrTexture.image?.data &&
|
|
96517
|
+
hdrTexture.image.width &&
|
|
96518
|
+
hdrTexture.image.height) {
|
|
96370
96519
|
const result = detectDominantLights(hdrTexture.image.data, hdrTexture.image.width, hdrTexture.image.height);
|
|
96371
96520
|
this._lightDetectionCache.set(cacheKey, result);
|
|
96372
96521
|
}
|
|
96373
|
-
//
|
|
96374
|
-
//
|
|
96375
|
-
//
|
|
96376
|
-
|
|
96377
|
-
//
|
|
96522
|
+
// Dispose the source equirectangular texture (PMREM is now in GPU memory).
|
|
96523
|
+
// Note: we cannot use the raw HDR for scene.background because three.js's
|
|
96524
|
+
// WebGLBackground routes non-cubemap textures through a flat planeMesh
|
|
96525
|
+
// path that ignores backgroundRotation; PMREM (CubeUVReflectionMapping)
|
|
96526
|
+
// takes the cubemap path which respects rotation.
|
|
96527
|
+
hdrTexture.dispose();
|
|
96528
|
+
// Cache render target and track its texture
|
|
96378
96529
|
this._cache.set(cacheKey, renderTarget);
|
|
96379
|
-
this._hdrCache.set(cacheKey, hdrTexture);
|
|
96380
96530
|
gpuTracker.trackTexture(renderTarget.texture, `PMREM environment: ${cacheKey}`);
|
|
96381
|
-
gpuTracker.trackTexture(hdrTexture, `HDR background: ${cacheKey}`);
|
|
96382
96531
|
logger.debug(`Loaded HDR environment from "${url}", cached as "${cacheKey}"`);
|
|
96383
96532
|
return renderTarget.texture;
|
|
96384
96533
|
}
|
|
@@ -96448,7 +96597,8 @@ class StudioFloor {
|
|
|
96448
96597
|
*/
|
|
96449
96598
|
setShadowIntensity(intensity) {
|
|
96450
96599
|
if (this._shadowPlane) {
|
|
96451
|
-
this._shadowPlane.material.opacity =
|
|
96600
|
+
this._shadowPlane.material.opacity =
|
|
96601
|
+
intensity * 1.0;
|
|
96452
96602
|
}
|
|
96453
96603
|
}
|
|
96454
96604
|
/** Dispose all GPU resources. */
|
|
@@ -96464,7 +96614,10 @@ class StudioFloor {
|
|
|
96464
96614
|
_createShadowPlane(zPosition, sceneSize) {
|
|
96465
96615
|
const floorSize = sceneSize * 6;
|
|
96466
96616
|
const geometry = new PlaneGeometry(floorSize, floorSize);
|
|
96467
|
-
const material = new ShadowMaterial({
|
|
96617
|
+
const material = new ShadowMaterial({
|
|
96618
|
+
opacity: 0.5,
|
|
96619
|
+
depthWrite: false,
|
|
96620
|
+
});
|
|
96468
96621
|
const plane = new Mesh(geometry, material);
|
|
96469
96622
|
plane.position.z = zPosition;
|
|
96470
96623
|
plane.receiveShadow = true;
|
|
@@ -103925,13 +104078,17 @@ const $05f6997e4b65da14$export$ed4ee5d1e55474a5 = {
|
|
|
103925
104078
|
* Only instantiated when Studio mode is active. Non-Studio rendering
|
|
103926
104079
|
* bypasses this entirely and uses direct `renderer.render()`.
|
|
103927
104080
|
*/
|
|
103928
|
-
//
|
|
103929
|
-
//
|
|
103930
|
-
//
|
|
104081
|
+
// Tone mapping runs as a post-process effect in the EffectPass, before SMAA,
|
|
104082
|
+
// so SMAA's edge detection sees LDR luma in its calibrated [0,1] range.
|
|
104083
|
+
// Per-fragment tone mapping in the main RenderPass would be a no-op:
|
|
104084
|
+
// three.js forces NoToneMapping when rendering to a non-canvas render target
|
|
104085
|
+
// (see WebGLPrograms.getParameters), and the composer's input is a HalfFloat
|
|
104086
|
+
// HDR FBO. Exposure is read from renderer.toneMappingExposure by three.js's
|
|
104087
|
+
// shared <tonemapping_pars_fragment> chunk that ToneMappingEffect includes.
|
|
103931
104088
|
const TONE_MAP_MODE = {
|
|
103932
|
-
|
|
103933
|
-
|
|
103934
|
-
|
|
104089
|
+
neutral: ToneMappingMode.NEUTRAL,
|
|
104090
|
+
ACES: ToneMappingMode.ACES_FILMIC,
|
|
104091
|
+
none: ToneMappingMode.LINEAR,
|
|
103935
104092
|
};
|
|
103936
104093
|
// Scratch color to avoid per-frame allocation
|
|
103937
104094
|
const _savedClearColor = new Color();
|
|
@@ -103989,7 +104146,7 @@ class StudioComposer {
|
|
|
103989
104146
|
* @param width - Canvas width in pixels
|
|
103990
104147
|
* @param height - Canvas height in pixels
|
|
103991
104148
|
*/
|
|
103992
|
-
constructor(renderer, scene, camera, width, height) {
|
|
104149
|
+
constructor(renderer, scene, camera, width, height, onSmaaReady) {
|
|
103993
104150
|
/** Solid background color to protect from tone mapping, or null. */
|
|
103994
104151
|
this._bgProtectColor = null;
|
|
103995
104152
|
// Shadow mask pipeline — two separate masks to avoid depth-discontinuity halos
|
|
@@ -104010,15 +104167,17 @@ class StudioComposer {
|
|
|
104010
104167
|
this._camera = camera;
|
|
104011
104168
|
this._width = width;
|
|
104012
104169
|
this._height = height;
|
|
104013
|
-
//
|
|
104014
|
-
//
|
|
104170
|
+
// ToneMappingEffect handles the tone curve in the EffectPass. The
|
|
104171
|
+
// postprocessing library requires renderer.toneMapping = NoToneMapping
|
|
104172
|
+
// so the renderer doesn't try to apply it as well.
|
|
104015
104173
|
this._renderer.toneMapping = NoToneMapping;
|
|
104016
104174
|
// HDR pipeline with HalfFloat framebuffer.
|
|
104017
|
-
// multisampling =
|
|
104018
|
-
//
|
|
104175
|
+
// multisampling = 4: WebGL2 MSAA on the composer's input RT. Most GPUs
|
|
104176
|
+
// clamp half-float MSAA to 4 samples anyway, and Studio mode applies
|
|
104177
|
+
// additional supersampling via renderer.setPixelRatio to compensate.
|
|
104019
104178
|
this._composer = new EffectComposer(renderer, {
|
|
104020
104179
|
frameBufferType: HalfFloatType,
|
|
104021
|
-
multisampling:
|
|
104180
|
+
multisampling: 4,
|
|
104022
104181
|
});
|
|
104023
104182
|
// --- Pass 1: scene render ---
|
|
104024
104183
|
this._renderPass = new RenderPass(scene, camera);
|
|
@@ -104038,11 +104197,23 @@ class StudioComposer {
|
|
|
104038
104197
|
this._n8aoPass.enabled = false; // off by default
|
|
104039
104198
|
this._composer.addPass(this._n8aoPass);
|
|
104040
104199
|
// --- Pass 3: shadow mask compositing + tone mapping + SMAA ---
|
|
104200
|
+
// ShadowMask runs first in linear HDR (where shadow math is correct).
|
|
104201
|
+
// ToneMapping next so SMAA sees LDR luma in its calibrated [0,1] range.
|
|
104041
104202
|
this._toneMappingEffect = new ToneMappingEffect({
|
|
104042
104203
|
mode: ToneMappingMode.NEUTRAL,
|
|
104043
104204
|
});
|
|
104044
|
-
const smaaEffect = new SMAAEffect({ preset: SMAAPreset.
|
|
104045
|
-
//
|
|
104205
|
+
const smaaEffect = new SMAAEffect({ preset: SMAAPreset.ULTRA });
|
|
104206
|
+
// SMAA loads its lookup textures (search/area) asynchronously via
|
|
104207
|
+
// `new Image(); image.src = "data:..."`. Even though the source is a
|
|
104208
|
+
// data URL, the `load` event fires in a microtask — so the very first
|
|
104209
|
+
// render after composer construction has no SMAA textures attached and
|
|
104210
|
+
// produces aliased edges. We notify on `load` so the caller can trigger
|
|
104211
|
+
// another render and the user sees AA without having to interact.
|
|
104212
|
+
if (onSmaaReady) {
|
|
104213
|
+
// postprocessing's TS types only declare "change"; SMAA dispatches
|
|
104214
|
+
// "load" at runtime when the lookup textures finish decoding.
|
|
104215
|
+
smaaEffect.addEventListener("load", () => onSmaaReady());
|
|
104216
|
+
}
|
|
104046
104217
|
this._shadowMaskEffect = new ShadowMaskEffect();
|
|
104047
104218
|
this._effectPass = new EffectPass(camera, this._shadowMaskEffect, this._toneMappingEffect, smaaEffect);
|
|
104048
104219
|
this._composer.addPass(this._effectPass);
|
|
@@ -104110,6 +104281,9 @@ class StudioComposer {
|
|
|
104110
104281
|
else {
|
|
104111
104282
|
this._toneMappingEffect.mode = mapped;
|
|
104112
104283
|
}
|
|
104284
|
+
// ToneMappingEffect's GLSL reads toneMappingExposure from the
|
|
104285
|
+
// <tonemapping_pars_fragment> chunk, which three.js auto-populates
|
|
104286
|
+
// from this renderer property each frame.
|
|
104113
104287
|
this._renderer.toneMappingExposure = exposure;
|
|
104114
104288
|
}
|
|
104115
104289
|
// -----------------------------------------------------------------------
|
|
@@ -104209,7 +104383,9 @@ class StudioComposer {
|
|
|
104209
104383
|
this._composer.setSize(width, height, false);
|
|
104210
104384
|
this._n8aoPass.setSize(width, height);
|
|
104211
104385
|
// Resize shadow mask RTs at half resolution
|
|
104212
|
-
if (this._shadowMaskRT &&
|
|
104386
|
+
if (this._shadowMaskRT &&
|
|
104387
|
+
this._blurredObjectMaskRT &&
|
|
104388
|
+
this._blurredFloorMaskRT) {
|
|
104213
104389
|
const halfW = Math.max(1, Math.floor(width / 2));
|
|
104214
104390
|
const halfH = Math.max(1, Math.floor(height / 2));
|
|
104215
104391
|
this._shadowMaskRT.setSize(halfW, halfH);
|
|
@@ -104236,8 +104412,11 @@ class StudioComposer {
|
|
|
104236
104412
|
render(deltaTime) {
|
|
104237
104413
|
// Two-pass shadow mask: objects and floor are blurred separately to
|
|
104238
104414
|
// avoid depth-discontinuity halos at their boundary.
|
|
104239
|
-
if (this._shadowMaskEnabled &&
|
|
104240
|
-
|
|
104415
|
+
if (this._shadowMaskEnabled &&
|
|
104416
|
+
this._shadowMaskRT &&
|
|
104417
|
+
this._blurPass &&
|
|
104418
|
+
this._blurredObjectMaskRT &&
|
|
104419
|
+
this._blurredFloorMaskRT) {
|
|
104241
104420
|
this._renderer.shadowMap.autoUpdate = false;
|
|
104242
104421
|
this._renderer.shadowMap.needsUpdate = true;
|
|
104243
104422
|
// Pass 1: object shadow mask (floor hidden, generates shadow map)
|
|
@@ -104248,8 +104427,10 @@ class StudioComposer {
|
|
|
104248
104427
|
this._renderShadowMask("floor");
|
|
104249
104428
|
this._blurPass.render(this._renderer, this._shadowMaskRT, this._blurredFloorMaskRT);
|
|
104250
104429
|
// Feed both blurred masks to the compositing effect
|
|
104251
|
-
this._shadowMaskEffect.uniforms.get("shadowMaskObjects").value =
|
|
104252
|
-
|
|
104430
|
+
this._shadowMaskEffect.uniforms.get("shadowMaskObjects").value =
|
|
104431
|
+
this._blurredObjectMaskRT.texture;
|
|
104432
|
+
this._shadowMaskEffect.uniforms.get("shadowMaskFloor").value =
|
|
104433
|
+
this._blurredFloorMaskRT.texture;
|
|
104253
104434
|
this._renderer.shadowMap.autoUpdate = true;
|
|
104254
104435
|
}
|
|
104255
104436
|
// Hide floor during main render — blurred shadow mask provides floor shadow
|
|
@@ -104382,26 +104563,86 @@ class StudioComposer {
|
|
|
104382
104563
|
*/
|
|
104383
104564
|
const STATE_KEYS = new Set([
|
|
104384
104565
|
// Display
|
|
104385
|
-
"theme",
|
|
104386
|
-
"
|
|
104387
|
-
"
|
|
104566
|
+
"theme",
|
|
104567
|
+
"cadWidth",
|
|
104568
|
+
"treeWidth",
|
|
104569
|
+
"treeHeight",
|
|
104570
|
+
"height",
|
|
104571
|
+
"pinning",
|
|
104572
|
+
"glass",
|
|
104573
|
+
"tools",
|
|
104574
|
+
"keymap",
|
|
104575
|
+
"newTreeBehavior",
|
|
104576
|
+
"measureTools",
|
|
104577
|
+
"selectTool",
|
|
104578
|
+
"explodeTool",
|
|
104579
|
+
"zscaleTool",
|
|
104580
|
+
"zebraTool",
|
|
104581
|
+
"studioTool",
|
|
104582
|
+
"measurementDebug",
|
|
104388
104583
|
// Render
|
|
104389
|
-
"ambientIntensity",
|
|
104390
|
-
"
|
|
104584
|
+
"ambientIntensity",
|
|
104585
|
+
"directIntensity",
|
|
104586
|
+
"metalness",
|
|
104587
|
+
"roughness",
|
|
104588
|
+
"defaultOpacity",
|
|
104589
|
+
"edgeColor",
|
|
104590
|
+
"normalLen",
|
|
104391
104591
|
// Viewer
|
|
104392
|
-
"axes",
|
|
104393
|
-
"
|
|
104394
|
-
"
|
|
104395
|
-
"
|
|
104396
|
-
"
|
|
104592
|
+
"axes",
|
|
104593
|
+
"axes0",
|
|
104594
|
+
"grid",
|
|
104595
|
+
"ortho",
|
|
104596
|
+
"transparent",
|
|
104597
|
+
"blackEdges",
|
|
104598
|
+
"collapse",
|
|
104599
|
+
"clipIntersection",
|
|
104600
|
+
"clipPlaneHelpers",
|
|
104601
|
+
"clipObjectColors",
|
|
104602
|
+
"clipNormal0",
|
|
104603
|
+
"clipNormal1",
|
|
104604
|
+
"clipNormal2",
|
|
104605
|
+
"clipSlider0",
|
|
104606
|
+
"clipSlider1",
|
|
104607
|
+
"clipSlider2",
|
|
104608
|
+
"control",
|
|
104609
|
+
"holroyd",
|
|
104610
|
+
"up",
|
|
104611
|
+
"ticks",
|
|
104612
|
+
"gridFontSize",
|
|
104613
|
+
"centerGrid",
|
|
104614
|
+
"position",
|
|
104615
|
+
"quaternion",
|
|
104616
|
+
"target",
|
|
104617
|
+
"zoom",
|
|
104618
|
+
"panSpeed",
|
|
104619
|
+
"rotateSpeed",
|
|
104620
|
+
"zoomSpeed",
|
|
104621
|
+
"timeit",
|
|
104397
104622
|
// Zebra
|
|
104398
|
-
"zebraCount",
|
|
104623
|
+
"zebraCount",
|
|
104624
|
+
"zebraOpacity",
|
|
104625
|
+
"zebraDirection",
|
|
104626
|
+
"zebraColorScheme",
|
|
104627
|
+
"zebraMappingMode",
|
|
104399
104628
|
// Studio
|
|
104400
|
-
"studioEnvironment",
|
|
104401
|
-
"
|
|
104402
|
-
"
|
|
104629
|
+
"studioEnvironment",
|
|
104630
|
+
"studioEnvIntensity",
|
|
104631
|
+
"studioBackground",
|
|
104632
|
+
"studioToneMapping",
|
|
104633
|
+
"studioExposure",
|
|
104634
|
+
"studio4kEnvMaps",
|
|
104635
|
+
"studioTextureMapping",
|
|
104636
|
+
"studioEnvRotation",
|
|
104637
|
+
"studioShadowIntensity",
|
|
104638
|
+
"studioShadowSoftness",
|
|
104639
|
+
"studioAOIntensity",
|
|
104403
104640
|
// Runtime
|
|
104404
|
-
"activeTool",
|
|
104641
|
+
"activeTool",
|
|
104642
|
+
"animationMode",
|
|
104643
|
+
"animationSliderValue",
|
|
104644
|
+
"zscaleActive",
|
|
104645
|
+
"highlightedButton",
|
|
104405
104646
|
"activeTab",
|
|
104406
104647
|
]);
|
|
104407
104648
|
/**
|
|
@@ -104608,7 +104849,8 @@ class ViewerState {
|
|
|
104608
104849
|
const value = updates[key];
|
|
104609
104850
|
// Skip undefined/null, except for keys where null is a valid value (reset to default)
|
|
104610
104851
|
const KEYS_WITH_VALID_NULL = ["position", "quaternion", "target"];
|
|
104611
|
-
if (value === undefined ||
|
|
104852
|
+
if (value === undefined ||
|
|
104853
|
+
(value === null && !KEYS_WITH_VALID_NULL.includes(key)))
|
|
104612
104854
|
continue;
|
|
104613
104855
|
const oldValue = this._state[key];
|
|
104614
104856
|
if (!valuesEqual(oldValue, value)) {
|
|
@@ -104634,8 +104876,13 @@ class ViewerState {
|
|
|
104634
104876
|
* Converts Vector3Tuple/QuaternionTuple to THREE objects.
|
|
104635
104877
|
*/
|
|
104636
104878
|
updateViewerState(options, notify = true) {
|
|
104637
|
-
// Extract properties that need conversion to THREE objects
|
|
104638
|
-
|
|
104879
|
+
// Extract properties that need conversion to THREE objects.
|
|
104880
|
+
// `tab` is also extracted: it's not a state key directly (state uses
|
|
104881
|
+
// `activeTab`), and setting activeTab here would trigger switchToTab
|
|
104882
|
+
// before the scene is built. Viewer.render() applies it after
|
|
104883
|
+
// scene-building completes (suppressing the CAD-mode paint when a
|
|
104884
|
+
// non-default tab is the target).
|
|
104885
|
+
const { tab: _tab, clipNormal0, clipNormal1, clipNormal2, position, quaternion, target, ...rest } = options;
|
|
104639
104886
|
const converted = { ...rest };
|
|
104640
104887
|
// Convert tuple values to THREE objects
|
|
104641
104888
|
if (clipNormal0 !== undefined) {
|
|
@@ -104651,7 +104898,9 @@ class ViewerState {
|
|
|
104651
104898
|
converted.position = position ? new Vector3(...position) : null;
|
|
104652
104899
|
}
|
|
104653
104900
|
if (quaternion !== undefined) {
|
|
104654
|
-
converted.quaternion = quaternion
|
|
104901
|
+
converted.quaternion = quaternion
|
|
104902
|
+
? new Quaternion(...quaternion)
|
|
104903
|
+
: null;
|
|
104655
104904
|
}
|
|
104656
104905
|
if (target !== undefined) {
|
|
104657
104906
|
converted.target = target ? new Vector3(...target) : null;
|
|
@@ -104775,9 +105024,15 @@ class ViewerState {
|
|
|
104775
105024
|
// Apply transform if defined (e.g., slider 0-1000 → relative 0-1)
|
|
104776
105025
|
const transform = STATE_NOTIFICATION_TRANSFORM[key];
|
|
104777
105026
|
const notifyChange = transform
|
|
104778
|
-
? {
|
|
105027
|
+
? {
|
|
105028
|
+
old: change.old != null ? transform(change.old) : null,
|
|
105029
|
+
new: transform(change.new),
|
|
105030
|
+
}
|
|
104779
105031
|
: change;
|
|
104780
|
-
this._externalNotifyCallback({
|
|
105032
|
+
this._externalNotifyCallback({
|
|
105033
|
+
key: notificationKey,
|
|
105034
|
+
change: notifyChange,
|
|
105035
|
+
});
|
|
104781
105036
|
}
|
|
104782
105037
|
}
|
|
104783
105038
|
}
|
|
@@ -104855,12 +105110,39 @@ ViewerState.DISPLAY_DEFAULTS = {
|
|
|
104855
105110
|
glass: false,
|
|
104856
105111
|
tools: true,
|
|
104857
105112
|
keymap: {
|
|
104858
|
-
shift: "shiftKey",
|
|
104859
|
-
|
|
104860
|
-
|
|
104861
|
-
|
|
104862
|
-
|
|
104863
|
-
|
|
105113
|
+
shift: "shiftKey",
|
|
105114
|
+
ctrl: "ctrlKey",
|
|
105115
|
+
meta: "metaKey",
|
|
105116
|
+
alt: "altKey",
|
|
105117
|
+
axes: "a",
|
|
105118
|
+
axes0: "A",
|
|
105119
|
+
grid: "g",
|
|
105120
|
+
gridxy: "G",
|
|
105121
|
+
perspective: "p",
|
|
105122
|
+
transparent: "t",
|
|
105123
|
+
blackedges: "b",
|
|
105124
|
+
reset: "R",
|
|
105125
|
+
resize: "r",
|
|
105126
|
+
iso: "5",
|
|
105127
|
+
front: "1",
|
|
105128
|
+
rear: "3",
|
|
105129
|
+
top: "8",
|
|
105130
|
+
bottom: "2",
|
|
105131
|
+
left: "4",
|
|
105132
|
+
right: "6",
|
|
105133
|
+
explode: "x",
|
|
105134
|
+
zscale: "L",
|
|
105135
|
+
distance: "D",
|
|
105136
|
+
properties: "P",
|
|
105137
|
+
select: "S",
|
|
105138
|
+
help: "h",
|
|
105139
|
+
play: " ",
|
|
105140
|
+
stop: "Escape",
|
|
105141
|
+
tree: "T",
|
|
105142
|
+
clip: "C",
|
|
105143
|
+
material: "M",
|
|
105144
|
+
zebra: "Z",
|
|
105145
|
+
studio: "s",
|
|
104864
105146
|
},
|
|
104865
105147
|
newTreeBehavior: true,
|
|
104866
105148
|
measureTools: true,
|
|
@@ -104938,7 +105220,12 @@ ViewerState.STUDIO_MODE_DEFAULTS = {
|
|
|
104938
105220
|
studioToneMapping: "neutral",
|
|
104939
105221
|
studioExposure: 1.0,
|
|
104940
105222
|
studio4kEnvMaps: false,
|
|
104941
|
-
|
|
105223
|
+
// "parametric" is the right default when the tessellator emits UVs:
|
|
105224
|
+
// each object's `nestedgroup.ts:applyTriplanarMapping` only kicks in
|
|
105225
|
+
// when the chosen mode is "triplanar" OR the geometry has no `uv`
|
|
105226
|
+
// attribute. So with the default "parametric", textured objects with
|
|
105227
|
+
// UVs use them; objects without UVs auto-fall back to triplanar.
|
|
105228
|
+
studioTextureMapping: "parametric",
|
|
104942
105229
|
studioEnvRotation: 0,
|
|
104943
105230
|
studioShadowIntensity: 0.5,
|
|
104944
105231
|
studioShadowSoftness: 0.2,
|
|
@@ -104975,6 +105262,13 @@ class StudioManager {
|
|
|
104975
105262
|
this._active = false;
|
|
104976
105263
|
this._savedClippingState = null;
|
|
104977
105264
|
this._shadowLights = [];
|
|
105265
|
+
/**
|
|
105266
|
+
* Renderer pixel ratio saved on Studio entry, restored on leave.
|
|
105267
|
+
* Studio mode bumps the pixel ratio to apply supersampling, which
|
|
105268
|
+
* compensates for low DPR (e.g., VSCode webviews report DPR=1 even
|
|
105269
|
+
* on Retina displays) and improves AA on shallow-angle edges.
|
|
105270
|
+
*/
|
|
105271
|
+
this._savedPixelRatio = null;
|
|
104978
105272
|
// -------------------------------------------------------------------------
|
|
104979
105273
|
// Mode enter/leave
|
|
104980
105274
|
// -------------------------------------------------------------------------
|
|
@@ -105026,9 +105320,28 @@ class StudioManager {
|
|
|
105026
105320
|
this._ctx.getDirectLight().intensity = 0;
|
|
105027
105321
|
// Floor
|
|
105028
105322
|
this._configureFloor();
|
|
105323
|
+
// Studio-only supersampling. Bump pixel ratio so the renderer draws to
|
|
105324
|
+
// a higher-resolution buffer; the browser downsamples to the canvas
|
|
105325
|
+
// display size, giving smooth AA on shallow-angle silhouettes that
|
|
105326
|
+
// MSAA alone leaves stair-stepped. Especially important in webview
|
|
105327
|
+
// hosts (e.g., VSCode) where window.devicePixelRatio is reported as 1
|
|
105328
|
+
// even on Retina displays. Restored in leaveStudioMode.
|
|
105329
|
+
this._savedPixelRatio = renderer.getPixelRatio();
|
|
105330
|
+
const targetPixelRatio = Math.max(2, window.devicePixelRatio);
|
|
105331
|
+
if (targetPixelRatio !== this._savedPixelRatio) {
|
|
105332
|
+
renderer.setPixelRatio(targetPixelRatio);
|
|
105333
|
+
renderer.setSize(state.get("cadWidth"), state.get("height"));
|
|
105334
|
+
}
|
|
105029
105335
|
// Create composer (must be before shadows)
|
|
105030
105336
|
if (!this._composer) {
|
|
105031
|
-
this._composer = new StudioComposer(renderer, scene, camera.getCamera(), state.get("cadWidth"), state.get("height"))
|
|
105337
|
+
this._composer = new StudioComposer(renderer, scene, camera.getCamera(), state.get("cadWidth"), state.get("height"), () => {
|
|
105338
|
+
// SMAA finished loading its async lookup textures. Re-render so
|
|
105339
|
+
// the first visible frame has anti-aliasing — without this the
|
|
105340
|
+
// user sees aliased edges until they interact with the scene.
|
|
105341
|
+
if (this._active && this._ctx.isRendered()) {
|
|
105342
|
+
this._ctx.update(true, false);
|
|
105343
|
+
}
|
|
105344
|
+
});
|
|
105032
105345
|
}
|
|
105033
105346
|
// Shadows (requires composer)
|
|
105034
105347
|
if (state.get("studioShadowIntensity") > 0) {
|
|
@@ -105052,6 +105365,12 @@ class StudioManager {
|
|
|
105052
105365
|
this._composer.dispose();
|
|
105053
105366
|
this._composer = null;
|
|
105054
105367
|
}
|
|
105368
|
+
// Restore pixel ratio if we bumped it before failure
|
|
105369
|
+
if (this._savedPixelRatio !== null) {
|
|
105370
|
+
renderer.setPixelRatio(this._savedPixelRatio);
|
|
105371
|
+
renderer.setSize(state.get("cadWidth"), state.get("height"));
|
|
105372
|
+
this._savedPixelRatio = null;
|
|
105373
|
+
}
|
|
105055
105374
|
this._active = false;
|
|
105056
105375
|
logger.error("Unexpected error entering studio mode", err);
|
|
105057
105376
|
}
|
|
@@ -105068,6 +105387,12 @@ class StudioManager {
|
|
|
105068
105387
|
this._composer.dispose();
|
|
105069
105388
|
this._composer = null;
|
|
105070
105389
|
}
|
|
105390
|
+
// Restore the renderer's pixel ratio that was bumped on Studio entry.
|
|
105391
|
+
if (this._savedPixelRatio !== null) {
|
|
105392
|
+
renderer.setPixelRatio(this._savedPixelRatio);
|
|
105393
|
+
renderer.setSize(state.get("cadWidth"), state.get("height"));
|
|
105394
|
+
this._savedPixelRatio = null;
|
|
105395
|
+
}
|
|
105071
105396
|
// 3. Remove environment, disable shadows
|
|
105072
105397
|
this.envManager.remove(this._ctx.getScene());
|
|
105073
105398
|
this._setShadowsEnabled(false);
|
|
@@ -105200,7 +105525,9 @@ class StudioManager {
|
|
|
105200
105525
|
state.subscribe("studioEnvironment", (change) => {
|
|
105201
105526
|
if (!isActive())
|
|
105202
105527
|
return;
|
|
105203
|
-
this.envManager
|
|
105528
|
+
this.envManager
|
|
105529
|
+
.loadEnvironment(change.new, this._ctx.renderer)
|
|
105530
|
+
.then(() => {
|
|
105204
105531
|
if (!isActive())
|
|
105205
105532
|
return;
|
|
105206
105533
|
reapplyEnv();
|
|
@@ -105209,7 +105536,8 @@ class StudioManager {
|
|
|
105209
105536
|
}
|
|
105210
105537
|
this._ctx.update(true, false);
|
|
105211
105538
|
this._ctx.dispatchEvent(new Event("tcv-studio-ready"));
|
|
105212
|
-
})
|
|
105539
|
+
})
|
|
105540
|
+
.catch((err) => {
|
|
105213
105541
|
logger.error("Unexpected error loading studio environment", err);
|
|
105214
105542
|
this._ctx.dispatchEvent(new Event("tcv-studio-ready"));
|
|
105215
105543
|
});
|
|
@@ -105300,7 +105628,9 @@ class StudioManager {
|
|
|
105300
105628
|
if (!isActive())
|
|
105301
105629
|
return;
|
|
105302
105630
|
const envName = state.get("studioEnvironment");
|
|
105303
|
-
this.envManager
|
|
105631
|
+
this.envManager
|
|
105632
|
+
.setUse4kEnvMaps(change.new, envName, this._ctx.renderer)
|
|
105633
|
+
.then(() => {
|
|
105304
105634
|
if (!isActive())
|
|
105305
105635
|
return;
|
|
105306
105636
|
reapplyEnv();
|
|
@@ -105438,7 +105768,9 @@ class StudioManager {
|
|
|
105438
105768
|
this.floor.setShadowsEnabled(true);
|
|
105439
105769
|
this._ctx.getScene().traverse((obj) => {
|
|
105440
105770
|
if (obj instanceof Mesh && obj.material) {
|
|
105441
|
-
const mats = Array.isArray(obj.material)
|
|
105771
|
+
const mats = Array.isArray(obj.material)
|
|
105772
|
+
? obj.material
|
|
105773
|
+
: [obj.material];
|
|
105442
105774
|
for (const m of mats) {
|
|
105443
105775
|
m.needsUpdate = true;
|
|
105444
105776
|
}
|
|
@@ -105699,7 +106031,9 @@ class Viewer {
|
|
|
105699
106031
|
return this._rendered;
|
|
105700
106032
|
}
|
|
105701
106033
|
/** Environment manager — proxied from StudioManager for display.ts access. */
|
|
105702
|
-
get envManager() {
|
|
106034
|
+
get envManager() {
|
|
106035
|
+
return this._studioManager.envManager;
|
|
106036
|
+
}
|
|
105703
106037
|
// ---------------------------------------------------------------------------
|
|
105704
106038
|
// Constructor & Initialization
|
|
105705
106039
|
// ---------------------------------------------------------------------------
|
|
@@ -105712,6 +106046,10 @@ class Viewer {
|
|
|
105712
106046
|
* @param updateMarker - enforce to redraw orientation marker after every ui activity
|
|
105713
106047
|
*/
|
|
105714
106048
|
constructor(display, options, notifyCallback, pinAsPngCallback = null, updateMarker = true) {
|
|
106049
|
+
// Grid size from the previous render, used to decide whether the new
|
|
106050
|
+
// geometry is "the same model" for clip-slider preservation.
|
|
106051
|
+
// Survives clear() so reused viewers remember the previous geometry.
|
|
106052
|
+
this._previousGridSize = 0;
|
|
105715
106053
|
// ---------------------------------------------------------------------------
|
|
105716
106054
|
// Render Loop & Scene Updates
|
|
105717
106055
|
// ---------------------------------------------------------------------------
|
|
@@ -105762,6 +106100,20 @@ class Viewer {
|
|
|
105762
106100
|
this.update = (updateMarker, notify = true) => {
|
|
105763
106101
|
if (!this.ready)
|
|
105764
106102
|
return;
|
|
106103
|
+
// Skip painting while Studio mode is mid-async-load: composer hasn't
|
|
106104
|
+
// been created yet, so a fall-through to renderer.render() would paint
|
|
106105
|
+
// the scene with CAD materials (Studio's material swap is also async).
|
|
106106
|
+
// Without this guard, any setter that calls update() — setCameraZoom,
|
|
106107
|
+
// setView, setExplode, setTool, etc. — would paint a CAD-materials
|
|
106108
|
+
// frame before Studio's first proper paint, which is visible as a
|
|
106109
|
+
// 0.5–1 sec CAD render before Studio takes over. Studio's tab
|
|
106110
|
+
// handler does its own update() at completion, which is when the
|
|
106111
|
+
// first painted frame should appear. State changes still propagate
|
|
106112
|
+
// synchronously and are picked up by that eventual paint.
|
|
106113
|
+
if (this.state.get("activeTab") === "studio" &&
|
|
106114
|
+
!this._studioManager.hasComposer) {
|
|
106115
|
+
return;
|
|
106116
|
+
}
|
|
105765
106117
|
if (this._externalGl) {
|
|
105766
106118
|
this.renderer.resetState();
|
|
105767
106119
|
}
|
|
@@ -105799,7 +106151,10 @@ class Viewer {
|
|
|
105799
106151
|
}
|
|
105800
106152
|
if (updateMarker) {
|
|
105801
106153
|
this.renderer.clearDepth(); // ensure orientation Marker is at the top
|
|
105802
|
-
this.rendered.orientationMarker.update(this.rendered.camera
|
|
106154
|
+
this.rendered.orientationMarker.update(this.rendered.camera
|
|
106155
|
+
.getPosition()
|
|
106156
|
+
.clone()
|
|
106157
|
+
.sub(this.rendered.controls.getTarget()), this.rendered.camera.getQuaternion());
|
|
105803
106158
|
this.rendered.orientationMarker.render(this.renderer);
|
|
105804
106159
|
}
|
|
105805
106160
|
if (this.animation) {
|
|
@@ -105866,6 +106221,11 @@ class Viewer {
|
|
|
105866
106221
|
if (!isObjectGroup(objectGroup))
|
|
105867
106222
|
continue;
|
|
105868
106223
|
objectGroup.setShapeVisible(compactTree[0] === 1);
|
|
106224
|
+
// Re-apply clip-mode back visibility when re-showing — see
|
|
106225
|
+
// matching comment in setObject().
|
|
106226
|
+
if (compactTree[0] === 1 && this.expandedNestedGroup.backVisible) {
|
|
106227
|
+
objectGroup.setBackVisible(true);
|
|
106228
|
+
}
|
|
105869
106229
|
objectGroup.setEdgesVisible(compactTree[1] === 1);
|
|
105870
106230
|
// Sync state (unless disabled = 3)
|
|
105871
106231
|
if (leafState[0] !== 3)
|
|
@@ -105897,6 +106257,11 @@ class Viewer {
|
|
|
105897
106257
|
}
|
|
105898
106258
|
}
|
|
105899
106259
|
objectGroup.setShapeVisible(shapeVisible);
|
|
106260
|
+
// Re-apply clip-mode back visibility when re-showing — see
|
|
106261
|
+
// matching comment in setObject().
|
|
106262
|
+
if (shapeVisible && this.compactNestedGroup.backVisible) {
|
|
106263
|
+
objectGroup.setBackVisible(true);
|
|
106264
|
+
}
|
|
105900
106265
|
objectGroup.setEdgesVisible(edgeVisible);
|
|
105901
106266
|
// Sync compact state (unless disabled = 3)
|
|
105902
106267
|
if (compactTree[0] !== 3)
|
|
@@ -106038,6 +106403,14 @@ class Viewer {
|
|
|
106038
106403
|
if (objectGroup != null && objectGroup instanceof ObjectGroup) {
|
|
106039
106404
|
if (iconNumber === 0) {
|
|
106040
106405
|
objectGroup.setShapeVisible(state === 1);
|
|
106406
|
+
// When re-showing while clip-tab is active, re-apply the clip-mode
|
|
106407
|
+
// back-visibility for this object. setShapeVisible's show-path
|
|
106408
|
+
// doesn't touch back when !renderback (clip-tab owns it), so a
|
|
106409
|
+
// previously-hidden object would otherwise come back with front
|
|
106410
|
+
// visible but back still hidden — looking hollow under clipping.
|
|
106411
|
+
if (state === 1 && this.rendered.nestedGroup.backVisible) {
|
|
106412
|
+
objectGroup.setBackVisible(true);
|
|
106413
|
+
}
|
|
106041
106414
|
}
|
|
106042
106415
|
else {
|
|
106043
106416
|
objectGroup.setEdgesVisible(state === 1);
|
|
@@ -107275,7 +107648,10 @@ class Viewer {
|
|
|
107275
107648
|
this.hasAnimationLoop = false;
|
|
107276
107649
|
this.display = display;
|
|
107277
107650
|
if (options.keymap) {
|
|
107278
|
-
this.setKeyMap({
|
|
107651
|
+
this.setKeyMap({
|
|
107652
|
+
...ViewerState.DISPLAY_DEFAULTS.keymap,
|
|
107653
|
+
...options.keymap,
|
|
107654
|
+
});
|
|
107279
107655
|
}
|
|
107280
107656
|
else {
|
|
107281
107657
|
this.setKeyMap(ViewerState.DISPLAY_DEFAULTS.keymap);
|
|
@@ -107596,7 +107972,8 @@ class Viewer {
|
|
|
107596
107972
|
this.renderer.renderLists.dispose();
|
|
107597
107973
|
this.renderer.dispose();
|
|
107598
107974
|
// Skip context loss for externally provided WebGL contexts
|
|
107599
|
-
if (!this._externalGl &&
|
|
107975
|
+
if (!this._externalGl &&
|
|
107976
|
+
typeof this.renderer.forceContextLoss === "function") {
|
|
107600
107977
|
this.renderer.forceContextLoss();
|
|
107601
107978
|
}
|
|
107602
107979
|
console.debug("three-cad-viewer: WebGL context disposed");
|
|
@@ -107699,6 +108076,19 @@ class Viewer {
|
|
|
107699
108076
|
deepDispose(this.compactNestedGroup);
|
|
107700
108077
|
this.compactNestedGroup = null;
|
|
107701
108078
|
}
|
|
108079
|
+
// Reset scene-derived fields so the next render() recomputes them
|
|
108080
|
+
// from the new geometry. Without this, reuse (clear() + render())
|
|
108081
|
+
// re-uses stale values from the previous scene, producing wrong
|
|
108082
|
+
// camera framing and stale bookkeeping.
|
|
108083
|
+
this.bbox = null;
|
|
108084
|
+
this.bb_max = 0;
|
|
108085
|
+
this.bb_radius = 0;
|
|
108086
|
+
this.lastBbox = null;
|
|
108087
|
+
this.materialSettings = null;
|
|
108088
|
+
this.renderOptions = null;
|
|
108089
|
+
this.tree = null;
|
|
108090
|
+
this.compactTree = null;
|
|
108091
|
+
this.expandedTree = null;
|
|
107702
108092
|
}
|
|
107703
108093
|
/**
|
|
107704
108094
|
* Build nestedGroup and treeview for initial render.
|
|
@@ -107997,16 +108387,28 @@ class Viewer {
|
|
|
107997
108387
|
this.setDirectLight(this.state.get("directIntensity"));
|
|
107998
108388
|
this.display.setSliderLimits(this.gridSize / 2);
|
|
107999
108389
|
this.display.syncClipSlidersFromState();
|
|
108000
|
-
// Compute clip slider values (used later after ready=true)
|
|
108001
|
-
|
|
108002
|
-
|
|
108003
|
-
|
|
108004
|
-
|
|
108005
|
-
|
|
108006
|
-
|
|
108007
|
-
|
|
108008
|
-
|
|
108009
|
-
|
|
108390
|
+
// Compute clip slider values (used later after ready=true).
|
|
108391
|
+
//
|
|
108392
|
+
// Three-tier policy:
|
|
108393
|
+
// 1. Caller passed a value (viewerOptions.clipSliderN != null) → use
|
|
108394
|
+
// it. Caller intent always wins.
|
|
108395
|
+
// 2. Same geometry as last render (gridSize unchanged) AND state has
|
|
108396
|
+
// a real value (≠ -1, the default sentinel) → reuse state. This
|
|
108397
|
+
// preserves the user's slider drag when re-rendering the same
|
|
108398
|
+
// model.
|
|
108399
|
+
// 3. New geometry (or first render) → default to gridSize/2.
|
|
108400
|
+
const gridSizeChanged = this._previousGridSize !== this.gridSize;
|
|
108401
|
+
this._previousGridSize = this.gridSize;
|
|
108402
|
+
const resolveSlider = (passed, stateValue) => {
|
|
108403
|
+
if (passed != null)
|
|
108404
|
+
return passed;
|
|
108405
|
+
if (!gridSizeChanged && stateValue !== -1)
|
|
108406
|
+
return stateValue;
|
|
108407
|
+
return this.gridSize / 2;
|
|
108408
|
+
};
|
|
108409
|
+
const clipSlider0 = resolveSlider(viewerOptions.clipSlider0, this.state.get("clipSlider0"));
|
|
108410
|
+
const clipSlider1 = resolveSlider(viewerOptions.clipSlider1, this.state.get("clipSlider1"));
|
|
108411
|
+
const clipSlider2 = resolveSlider(viewerOptions.clipSlider2, this.state.get("clipSlider2"));
|
|
108010
108412
|
nestedGroup.setClipPlanes(clipping.clipPlanes);
|
|
108011
108413
|
this.setLocalClipping(false); // only allow clipping when Clipping tab is selected
|
|
108012
108414
|
clipping.setVisible(false);
|
|
@@ -108027,15 +108429,29 @@ class Viewer {
|
|
|
108027
108429
|
this.display.showToolsPanel(false);
|
|
108028
108430
|
this.rendered.orientationMarker.setVisible(false);
|
|
108029
108431
|
}
|
|
108030
|
-
// Apply clip settings AFTER ready=true (clip setters check this.ready)
|
|
108031
|
-
//
|
|
108032
|
-
|
|
108033
|
-
|
|
108034
|
-
|
|
108035
|
-
//
|
|
108036
|
-
|
|
108037
|
-
|
|
108038
|
-
|
|
108432
|
+
// Apply clip settings AFTER ready=true (clip setters check this.ready).
|
|
108433
|
+
//
|
|
108434
|
+
// Same three-tier policy as clipSlider above (caller wins → reuse state
|
|
108435
|
+
// on same geometry → reset on new geometry). The default normals are
|
|
108436
|
+
// the axis-aligned planes that match the Clipping subsystem's own
|
|
108437
|
+
// DEFAULT_NORMALS.
|
|
108438
|
+
//
|
|
108439
|
+
// Always passing a non-null normal means setClipNormal also handles the
|
|
108440
|
+
// slider write (it calls setClipSlider internally), so no separate
|
|
108441
|
+
// setClipSlider follow-up is needed here.
|
|
108442
|
+
const resolveNormal = (passed, stateValue, defaultTuple) => {
|
|
108443
|
+
if (passed != null)
|
|
108444
|
+
return passed;
|
|
108445
|
+
if (!gridSizeChanged)
|
|
108446
|
+
return [stateValue.x, stateValue.y, stateValue.z];
|
|
108447
|
+
return defaultTuple;
|
|
108448
|
+
};
|
|
108449
|
+
const clipNormal0 = resolveNormal(viewerOptions.clipNormal0, this.state.get("clipNormal0"), [-1, 0, 0]);
|
|
108450
|
+
const clipNormal1 = resolveNormal(viewerOptions.clipNormal1, this.state.get("clipNormal1"), [0, -1, 0]);
|
|
108451
|
+
const clipNormal2 = resolveNormal(viewerOptions.clipNormal2, this.state.get("clipNormal2"), [0, 0, -1]);
|
|
108452
|
+
this.setClipNormal(0, clipNormal0, clipSlider0, true);
|
|
108453
|
+
this.setClipNormal(1, clipNormal1, clipSlider1, true);
|
|
108454
|
+
this.setClipNormal(2, clipNormal2, clipSlider2, true);
|
|
108039
108455
|
this.setClipIntersection(viewerOptions.clipIntersection ?? false, true);
|
|
108040
108456
|
this.setClipObjectColorCaps(viewerOptions.clipObjectColors ?? false, true);
|
|
108041
108457
|
this.setClipPlaneHelpers(viewerOptions.clipPlaneHelpers ?? false, true);
|
|
@@ -108047,15 +108463,38 @@ class Viewer {
|
|
|
108047
108463
|
// Computed values from controls/camera
|
|
108048
108464
|
target: { old: null, new: toVector3Tuple(controls.target.toArray()) },
|
|
108049
108465
|
target0: { old: null, new: toVector3Tuple(controls.target0.toArray()) },
|
|
108050
|
-
position: {
|
|
108051
|
-
|
|
108466
|
+
position: {
|
|
108467
|
+
old: null,
|
|
108468
|
+
new: this.rendered.camera.getPosition().toArray(),
|
|
108469
|
+
},
|
|
108470
|
+
quaternion: {
|
|
108471
|
+
old: null,
|
|
108472
|
+
new: this.rendered.camera.getQuaternion().toArray(),
|
|
108473
|
+
},
|
|
108052
108474
|
zoom: { old: null, new: this.rendered.camera.getZoom() },
|
|
108053
108475
|
// All config values from state
|
|
108054
108476
|
...this.state.getAllNotifiable(),
|
|
108055
108477
|
});
|
|
108056
108478
|
}
|
|
108057
108479
|
timer.split("notification done");
|
|
108058
|
-
|
|
108480
|
+
// Initial paint and tab-landing logic.
|
|
108481
|
+
//
|
|
108482
|
+
// viewerOptions.tab can request a non-tree tab as the landing
|
|
108483
|
+
// tab. To avoid a CAD-mode → target-tab flicker, we skip the default
|
|
108484
|
+
// CAD update() in that case and let the activeTab subscription's
|
|
108485
|
+
// switchToTab handler paint the right content (or, for studio, show
|
|
108486
|
+
// the spinner over a blank canvas while async setup runs).
|
|
108487
|
+
const targetTab = viewerOptions.tab ?? "tree";
|
|
108488
|
+
if (targetTab === "tree") {
|
|
108489
|
+
this.update(true, false);
|
|
108490
|
+
}
|
|
108491
|
+
else {
|
|
108492
|
+
// setActiveTab fires the subscription synchronously; switchToTab
|
|
108493
|
+
// either paints (clip / zebra / material) or initiates Studio's
|
|
108494
|
+
// async load (showing the spinner). The first painted frame the
|
|
108495
|
+
// user sees is the target tab, not CAD.
|
|
108496
|
+
this.setActiveTab(targetTab);
|
|
108497
|
+
}
|
|
108059
108498
|
treeview.update();
|
|
108060
108499
|
this.display.setTheme(this.state.get("theme"));
|
|
108061
108500
|
this.setZebraCount(this.state.get("zebraCount"));
|
|
@@ -108456,7 +108895,9 @@ class Viewer {
|
|
|
108456
108895
|
// Store current state
|
|
108457
108896
|
const camera = this.rendered.camera.getCamera();
|
|
108458
108897
|
const zoom = camera.zoom; // For orthographic cameras
|
|
108459
|
-
const offset = camera.position
|
|
108898
|
+
const offset = camera.position
|
|
108899
|
+
.clone()
|
|
108900
|
+
.sub(this.rendered.controls.getTarget());
|
|
108460
108901
|
// Update position and target
|
|
108461
108902
|
camera.position.copy(targetVec.clone().add(offset));
|
|
108462
108903
|
camera.updateWorldMatrix(true, false);
|
|
@@ -108691,7 +109132,10 @@ class Viewer {
|
|
|
108691
109132
|
version: partData.version,
|
|
108692
109133
|
id: wrapperId,
|
|
108693
109134
|
name: "__addPart_tmp__",
|
|
108694
|
-
loc: [
|
|
109135
|
+
loc: [
|
|
109136
|
+
[0, 0, 0],
|
|
109137
|
+
[0, 0, 0, 1],
|
|
109138
|
+
],
|
|
108695
109139
|
parts: [partData],
|
|
108696
109140
|
};
|
|
108697
109141
|
const wrapperGroup = nestedGroup.renderLoop(wrapper);
|
|
@@ -108871,8 +109315,7 @@ class Viewer {
|
|
|
108871
109315
|
else {
|
|
108872
109316
|
const edgePosAttr = group.edges.geometry.getAttribute("position");
|
|
108873
109317
|
sameEdges =
|
|
108874
|
-
edgePosAttr != null &&
|
|
108875
|
-
edgePosAttr.count === flatLen(shape.edges) / 3;
|
|
109318
|
+
edgePosAttr != null && edgePosAttr.count === flatLen(shape.edges) / 3;
|
|
108876
109319
|
}
|
|
108877
109320
|
}
|
|
108878
109321
|
if (!sameVertices || !sameTriangles || !sameEdges) {
|
|
@@ -109003,7 +109446,8 @@ class Viewer {
|
|
|
109003
109446
|
// Only rebuild stencils if geometry grew beyond the region that stencils
|
|
109004
109447
|
// were last built for. Shrinking geometry still fits within existing
|
|
109005
109448
|
// stencils, so skip the expensive rebuild in that case.
|
|
109006
|
-
const newCSize = 1.1 *
|
|
109449
|
+
const newCSize = 1.1 *
|
|
109450
|
+
Math.max(Math.abs(this.bbox.min.length()), Math.abs(this.bbox.max.length()));
|
|
109007
109451
|
if (newCSize > this._stencilCSize + 1e-6) {
|
|
109008
109452
|
this._stencilCSize = newCSize;
|
|
109009
109453
|
const clipping = this.rendered.clipping;
|
|
@@ -109049,9 +109493,7 @@ class Viewer {
|
|
|
109049
109493
|
}
|
|
109050
109494
|
const min = new Vector3(bb.xmin, bb.ymin, bb.zmin);
|
|
109051
109495
|
const max = new Vector3(bb.xmax, bb.ymax, bb.zmax);
|
|
109052
|
-
const center = new Vector3()
|
|
109053
|
-
.addVectors(min, max)
|
|
109054
|
-
.multiplyScalar(0.5);
|
|
109496
|
+
const center = new Vector3().addVectors(min, max).multiplyScalar(0.5);
|
|
109055
109497
|
const requiredCSize = 1.1 * Math.max(Math.abs(min.length()), Math.abs(max.length()));
|
|
109056
109498
|
if (requiredCSize > this._stencilCSize + 1e-6) {
|
|
109057
109499
|
this._stencilCSize = requiredCSize;
|
|
@@ -109278,7 +109720,8 @@ class Viewer {
|
|
|
109278
109720
|
if (value === undefined)
|
|
109279
109721
|
continue;
|
|
109280
109722
|
if (modifierKeys.has(key)) {
|
|
109281
|
-
modifiers[key] =
|
|
109723
|
+
modifiers[key] =
|
|
109724
|
+
value;
|
|
109282
109725
|
}
|
|
109283
109726
|
else {
|
|
109284
109727
|
actions[key] = value;
|
|
@@ -109334,6 +109777,7 @@ class Viewer {
|
|
|
109334
109777
|
*/
|
|
109335
109778
|
resizeCadView(cadWidth, treeWidth, height, glass = false) {
|
|
109336
109779
|
this.state.set("cadWidth", cadWidth);
|
|
109780
|
+
this.state.set("treeWidth", treeWidth);
|
|
109337
109781
|
this.state.set("height", height);
|
|
109338
109782
|
// Adapt renderer dimensions
|
|
109339
109783
|
this.renderer.setSize(cadWidth, height);
|
|
@@ -109391,7 +109835,9 @@ class Slider {
|
|
|
109391
109835
|
* @param notify - Whether to trigger the notification
|
|
109392
109836
|
*/
|
|
109393
109837
|
this._notify = (value, notify = true) => {
|
|
109394
|
-
if (this.type == "plane" &&
|
|
109838
|
+
if (this.type == "plane" &&
|
|
109839
|
+
this.notifyCallback &&
|
|
109840
|
+
this.index !== undefined) {
|
|
109395
109841
|
const change = {};
|
|
109396
109842
|
change[`clip_slider_${this.index - 1}`] = parseFloat(String(value));
|
|
109397
109843
|
this.notifyCallback(change, notify);
|
|
@@ -109435,7 +109881,8 @@ class Slider {
|
|
|
109435
109881
|
this.onSetSlider = options.onSetSlider || null;
|
|
109436
109882
|
const sliderEl = container.getElementsByClassName(`tcv_sld_value_${index}`)[0];
|
|
109437
109883
|
const inputEl = container.getElementsByClassName(`tcv_inp_value_${index}`)[0];
|
|
109438
|
-
if (!(sliderEl instanceof HTMLInputElement) ||
|
|
109884
|
+
if (!(sliderEl instanceof HTMLInputElement) ||
|
|
109885
|
+
!(inputEl instanceof HTMLInputElement)) {
|
|
109439
109886
|
throw new Error(`Slider elements not found for index "${index}" in container`);
|
|
109440
109887
|
}
|
|
109441
109888
|
this.slider = sliderEl;
|
|
@@ -109943,20 +110390,112 @@ function px(val) {
|
|
|
109943
110390
|
return `${val}px`;
|
|
109944
110391
|
}
|
|
109945
110392
|
const MAT_EDITOR_PARAMS = [
|
|
109946
|
-
{
|
|
109947
|
-
|
|
109948
|
-
|
|
109949
|
-
|
|
109950
|
-
|
|
109951
|
-
|
|
109952
|
-
|
|
109953
|
-
|
|
110393
|
+
{
|
|
110394
|
+
key: "metalness",
|
|
110395
|
+
label: "Metallic",
|
|
110396
|
+
min: 0,
|
|
110397
|
+
max: 1,
|
|
110398
|
+
step: 0.01,
|
|
110399
|
+
group: "PBR Core",
|
|
110400
|
+
},
|
|
110401
|
+
{
|
|
110402
|
+
key: "roughness",
|
|
110403
|
+
label: "Roughness",
|
|
110404
|
+
min: 0,
|
|
110405
|
+
max: 1,
|
|
110406
|
+
step: 0.01,
|
|
110407
|
+
group: "PBR Core",
|
|
110408
|
+
},
|
|
110409
|
+
{
|
|
110410
|
+
key: "clearcoat",
|
|
110411
|
+
label: "Clearcoat",
|
|
110412
|
+
min: 0,
|
|
110413
|
+
max: 1,
|
|
110414
|
+
step: 0.01,
|
|
110415
|
+
group: "Clearcoat",
|
|
110416
|
+
},
|
|
110417
|
+
{
|
|
110418
|
+
key: "clearcoatRoughness",
|
|
110419
|
+
label: "Clearcoat Rough.",
|
|
110420
|
+
min: 0,
|
|
110421
|
+
max: 1,
|
|
110422
|
+
step: 0.01,
|
|
110423
|
+
group: "Clearcoat",
|
|
110424
|
+
},
|
|
110425
|
+
{
|
|
110426
|
+
key: "transmission",
|
|
110427
|
+
label: "Transmission",
|
|
110428
|
+
min: 0,
|
|
110429
|
+
max: 1,
|
|
110430
|
+
step: 0.01,
|
|
110431
|
+
group: "Transmission",
|
|
110432
|
+
},
|
|
110433
|
+
{
|
|
110434
|
+
key: "ior",
|
|
110435
|
+
label: "IOR",
|
|
110436
|
+
min: 1.0,
|
|
110437
|
+
max: 2.5,
|
|
110438
|
+
step: 0.01,
|
|
110439
|
+
group: "Transmission",
|
|
110440
|
+
},
|
|
110441
|
+
{
|
|
110442
|
+
key: "thickness",
|
|
110443
|
+
label: "Thickness",
|
|
110444
|
+
min: 0,
|
|
110445
|
+
max: 10,
|
|
110446
|
+
step: 0.1,
|
|
110447
|
+
group: "Transmission",
|
|
110448
|
+
},
|
|
110449
|
+
{
|
|
110450
|
+
key: "attenuationDistance",
|
|
110451
|
+
label: "Atten. Distance",
|
|
110452
|
+
min: 0,
|
|
110453
|
+
max: 100,
|
|
110454
|
+
step: 0.5,
|
|
110455
|
+
group: "Transmission",
|
|
110456
|
+
infinity: true,
|
|
110457
|
+
},
|
|
109954
110458
|
{ key: "sheen", label: "Sheen", min: 0, max: 1, step: 0.01, group: "Sheen" },
|
|
109955
|
-
{
|
|
109956
|
-
|
|
109957
|
-
|
|
109958
|
-
|
|
109959
|
-
|
|
110459
|
+
{
|
|
110460
|
+
key: "sheenRoughness",
|
|
110461
|
+
label: "Sheen Roughness",
|
|
110462
|
+
min: 0,
|
|
110463
|
+
max: 1,
|
|
110464
|
+
step: 0.01,
|
|
110465
|
+
group: "Sheen",
|
|
110466
|
+
},
|
|
110467
|
+
{
|
|
110468
|
+
key: "specularIntensity",
|
|
110469
|
+
label: "Specular Intensity",
|
|
110470
|
+
min: 0,
|
|
110471
|
+
max: 2,
|
|
110472
|
+
step: 0.01,
|
|
110473
|
+
group: "Specular",
|
|
110474
|
+
},
|
|
110475
|
+
{
|
|
110476
|
+
key: "anisotropy",
|
|
110477
|
+
label: "Anisotropy",
|
|
110478
|
+
min: 0,
|
|
110479
|
+
max: 1,
|
|
110480
|
+
step: 0.01,
|
|
110481
|
+
group: "Anisotropy",
|
|
110482
|
+
},
|
|
110483
|
+
{
|
|
110484
|
+
key: "anisotropyRotation",
|
|
110485
|
+
label: "Anisotropy Rotation",
|
|
110486
|
+
min: 0,
|
|
110487
|
+
max: 6.28,
|
|
110488
|
+
step: 0.01,
|
|
110489
|
+
group: "Anisotropy",
|
|
110490
|
+
},
|
|
110491
|
+
{
|
|
110492
|
+
key: "emissiveIntensity",
|
|
110493
|
+
label: "Emissive Intensity",
|
|
110494
|
+
min: 0,
|
|
110495
|
+
max: 5,
|
|
110496
|
+
step: 0.1,
|
|
110497
|
+
group: "Emissive",
|
|
110498
|
+
},
|
|
109960
110499
|
];
|
|
109961
110500
|
function _formatMatValue(value, step) {
|
|
109962
110501
|
const decimals = step < 0.1 ? 2 : 1;
|
|
@@ -110405,7 +110944,13 @@ class Display {
|
|
|
110405
110944
|
if (!(e.target instanceof HTMLSelectElement))
|
|
110406
110945
|
return;
|
|
110407
110946
|
const value = e.target.value;
|
|
110408
|
-
if (value === "grey" ||
|
|
110947
|
+
if (value === "grey" ||
|
|
110948
|
+
value === "darkgrey" ||
|
|
110949
|
+
value === "white" ||
|
|
110950
|
+
value === "gradient" ||
|
|
110951
|
+
value === "gradient-dark" ||
|
|
110952
|
+
value === "environment" ||
|
|
110953
|
+
value === "transparent") {
|
|
110409
110954
|
this.state.set("studioBackground", value);
|
|
110410
110955
|
}
|
|
110411
110956
|
};
|
|
@@ -110623,7 +111168,8 @@ class Display {
|
|
|
110623
111168
|
// Skip if target is a text-entry input element (but allow buttons/checkboxes)
|
|
110624
111169
|
const target = e.target;
|
|
110625
111170
|
if ((target instanceof HTMLInputElement &&
|
|
110626
|
-
target.type !== "button" &&
|
|
111171
|
+
target.type !== "button" &&
|
|
111172
|
+
target.type !== "checkbox") ||
|
|
110627
111173
|
target instanceof HTMLTextAreaElement ||
|
|
110628
111174
|
target instanceof HTMLSelectElement) {
|
|
110629
111175
|
return;
|
|
@@ -110767,7 +111313,7 @@ class Display {
|
|
|
110767
111313
|
this._spinnerEl = this.container.querySelector(".tcv_studio_spinner");
|
|
110768
111314
|
this._warningBannerEl = this.container.querySelector(".tcv_warning_banner");
|
|
110769
111315
|
this.container.addEventListener("tcv-material-warnings", ((e) => {
|
|
110770
|
-
this._showWarningBanner(`Unresolved material tag(s): ${e.detail.map(t => `"${t}"`).join(", ")}`);
|
|
111316
|
+
this._showWarningBanner(`Unresolved material tag(s): ${e.detail.map((t) => `"${t}"`).join(", ")}`);
|
|
110771
111317
|
}));
|
|
110772
111318
|
this.tabTree = this.getElement("tcv_tab_tree");
|
|
110773
111319
|
this.tabClip = this.getElement("tcv_tab_clip");
|
|
@@ -111585,7 +112131,8 @@ class Display {
|
|
|
111585
112131
|
attachCanvas(canvasElement) {
|
|
111586
112132
|
// If the canvas is already attached elsewhere
|
|
111587
112133
|
// do not re-parent it into this display.
|
|
111588
|
-
if (canvasElement.parentElement &&
|
|
112134
|
+
if (canvasElement.parentElement &&
|
|
112135
|
+
canvasElement.parentElement !== this.cadView) {
|
|
111589
112136
|
listeners.add(canvasElement, "click", () => {
|
|
111590
112137
|
if (this.help_shown) {
|
|
111591
112138
|
this.showHelp(false);
|
|
@@ -111636,7 +112183,8 @@ class Display {
|
|
|
111636
112183
|
_deactivateToolsForStudio() {
|
|
111637
112184
|
// If a tool is currently active, deactivate it cleanly
|
|
111638
112185
|
const activeTool = this.state.get("activeTool");
|
|
111639
|
-
if (activeTool &&
|
|
112186
|
+
if (activeTool &&
|
|
112187
|
+
["distance", "properties", "angle", "select"].includes(activeTool)) {
|
|
111640
112188
|
this.clickButtons[activeTool]?.set(false);
|
|
111641
112189
|
this.setTool(activeTool, false);
|
|
111642
112190
|
// setTool→toggleTab(false) silently sets activeTab to "tree" (no notification).
|
|
@@ -111744,7 +112292,13 @@ class Display {
|
|
|
111744
112292
|
});
|
|
111745
112293
|
}
|
|
111746
112294
|
// Update tab styling
|
|
111747
|
-
[
|
|
112295
|
+
[
|
|
112296
|
+
this.tabTree,
|
|
112297
|
+
this.tabClip,
|
|
112298
|
+
this.tabZebra,
|
|
112299
|
+
this.tabMaterial,
|
|
112300
|
+
this.tabStudio,
|
|
112301
|
+
].forEach((tabEl) => {
|
|
111748
112302
|
tabEl.classList.add("tcv_tab-unselected");
|
|
111749
112303
|
tabEl.classList.remove("tcv_tab-selected");
|
|
111750
112304
|
});
|
|
@@ -111841,11 +112395,15 @@ class Display {
|
|
|
111841
112395
|
originalMat = currentMat;
|
|
111842
112396
|
mat = currentMat.clone();
|
|
111843
112397
|
// Preserve triplanar mapping if the original material uses it
|
|
111844
|
-
if (currentMat.customProgramCacheKey() === "triplanar" &&
|
|
112398
|
+
if (currentMat.customProgramCacheKey() === "triplanar" &&
|
|
112399
|
+
object.shapeGeometry) {
|
|
111845
112400
|
applyTriplanarMapping(mat, object.shapeGeometry);
|
|
111846
112401
|
}
|
|
111847
112402
|
object.front.material = mat;
|
|
111848
|
-
this._matEditorClones.set(objectPath, {
|
|
112403
|
+
this._matEditorClones.set(objectPath, {
|
|
112404
|
+
original: originalMat,
|
|
112405
|
+
clone: mat,
|
|
112406
|
+
});
|
|
111849
112407
|
}
|
|
111850
112408
|
// Restore elements that _showMatEditorHint may have hidden
|
|
111851
112409
|
const resetBtn = dialog.querySelector(".tcv_mat_editor_reset");
|
|
@@ -111896,7 +112454,9 @@ class Display {
|
|
|
111896
112454
|
try {
|
|
111897
112455
|
groups = this.viewer.rendered.nestedGroup.groups;
|
|
111898
112456
|
}
|
|
111899
|
-
catch {
|
|
112457
|
+
catch {
|
|
112458
|
+
/* not rendered */
|
|
112459
|
+
}
|
|
111900
112460
|
for (const [path, { original, clone }] of this._matEditorClones.entries()) {
|
|
111901
112461
|
// Restore original material on mesh before disposing the clone
|
|
111902
112462
|
if (groups) {
|
|
@@ -111958,7 +112518,8 @@ class Display {
|
|
|
111958
112518
|
if (!(currentMat instanceof MeshPhysicalMaterial))
|
|
111959
112519
|
continue;
|
|
111960
112520
|
const clone = currentMat.clone();
|
|
111961
|
-
if (currentMat.customProgramCacheKey() === "triplanar" &&
|
|
112521
|
+
if (currentMat.customProgramCacheKey() === "triplanar" &&
|
|
112522
|
+
group.shapeGeometry) {
|
|
111962
112523
|
applyTriplanarMapping(clone, group.shapeGeometry);
|
|
111963
112524
|
}
|
|
111964
112525
|
for (const [key, value] of Object.entries(changes)) {
|
|
@@ -111966,7 +112527,10 @@ class Display {
|
|
|
111966
112527
|
clone[key] = value;
|
|
111967
112528
|
}
|
|
111968
112529
|
group.front.material = clone;
|
|
111969
|
-
this._matEditorClones.set(path, {
|
|
112530
|
+
this._matEditorClones.set(path, {
|
|
112531
|
+
original: currentMat,
|
|
112532
|
+
clone,
|
|
112533
|
+
});
|
|
111970
112534
|
}
|
|
111971
112535
|
this._savedMatEditorChanges.clear();
|
|
111972
112536
|
}
|
|
@@ -112009,7 +112573,10 @@ class Display {
|
|
|
112009
112573
|
startX = e.clientX;
|
|
112010
112574
|
startY = e.clientY;
|
|
112011
112575
|
const rect = dialog.getBoundingClientRect();
|
|
112012
|
-
const parentRect = dialog.offsetParent?.getBoundingClientRect() ?? {
|
|
112576
|
+
const parentRect = dialog.offsetParent?.getBoundingClientRect() ?? {
|
|
112577
|
+
left: 0,
|
|
112578
|
+
top: 0,
|
|
112579
|
+
};
|
|
112013
112580
|
origLeft = rect.left - parentRect.left;
|
|
112014
112581
|
origTop = rect.top - parentRect.top;
|
|
112015
112582
|
// Switch from right-positioning to left-positioning for drag
|
|
@@ -112023,8 +112590,8 @@ class Display {
|
|
|
112023
112590
|
return;
|
|
112024
112591
|
const dx = e.clientX - startX;
|
|
112025
112592
|
const dy = e.clientY - startY;
|
|
112026
|
-
dialog.style.left =
|
|
112027
|
-
dialog.style.top =
|
|
112593
|
+
dialog.style.left = origLeft + dx + "px";
|
|
112594
|
+
dialog.style.top = origTop + dy + "px";
|
|
112028
112595
|
}, { signal });
|
|
112029
112596
|
document.addEventListener("mouseup", () => {
|
|
112030
112597
|
dragging = false;
|
|
@@ -112042,7 +112609,8 @@ class Display {
|
|
|
112042
112609
|
}
|
|
112043
112610
|
let currentValue = // eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
112044
112611
|
material[param.key];
|
|
112045
|
-
const isInfinity = param.infinity === true &&
|
|
112612
|
+
const isInfinity = param.infinity === true &&
|
|
112613
|
+
(currentValue === Infinity || currentValue == null);
|
|
112046
112614
|
if (isInfinity)
|
|
112047
112615
|
currentValue = param.max;
|
|
112048
112616
|
this._buildMatEditorRow(content, param, currentValue ?? 0, isInfinity, originalMat);
|
|
@@ -112054,8 +112622,8 @@ class Display {
|
|
|
112054
112622
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
112055
112623
|
const origValue = originalMat[param.key];
|
|
112056
112624
|
const isChanged = (v) => param.infinity
|
|
112057
|
-
?
|
|
112058
|
-
|
|
112625
|
+
? v >= param.max !== (origValue === Infinity || origValue == null) ||
|
|
112626
|
+
(v < param.max && Math.abs(v - origValue) > param.step * 0.5)
|
|
112059
112627
|
: Math.abs(v - origValue) > param.step * 0.5;
|
|
112060
112628
|
const label = document.createElement("label");
|
|
112061
112629
|
label.className = "tcv_mat_editor_label";
|
|
@@ -112075,7 +112643,9 @@ class Display {
|
|
|
112075
112643
|
const valueDisplay = document.createElement("input");
|
|
112076
112644
|
valueDisplay.className = "tcv_clip_input";
|
|
112077
112645
|
valueDisplay.readOnly = true;
|
|
112078
|
-
valueDisplay.value = isInfinity
|
|
112646
|
+
valueDisplay.value = isInfinity
|
|
112647
|
+
? "\u221E"
|
|
112648
|
+
: _formatMatValue(value, param.step);
|
|
112079
112649
|
slider.addEventListener("input", () => {
|
|
112080
112650
|
const newValue = parseFloat(slider.value);
|
|
112081
112651
|
const result = this.viewer.getSelectedObjectGroup();
|
|
@@ -112164,7 +112734,8 @@ class Display {
|
|
|
112164
112734
|
this.studioShadowIntensitySlider?.setValueFromState(state.get("studioShadowIntensity") * 100);
|
|
112165
112735
|
this.studioShadowSoftnessSlider?.setValueFromState(state.get("studioShadowSoftness") * 100);
|
|
112166
112736
|
this.studioAOIntensitySlider?.setValueFromState(state.get("studioAOIntensity") * 10);
|
|
112167
|
-
this.getInputElement("tcv_studio_4k_env_maps").checked =
|
|
112737
|
+
this.getInputElement("tcv_studio_4k_env_maps").checked =
|
|
112738
|
+
state.get("studio4kEnvMaps");
|
|
112168
112739
|
this._syncEnvDropdown(state.get("studioEnvironment"));
|
|
112169
112740
|
const bgEl = this.container.querySelector(".tcv_studio_background");
|
|
112170
112741
|
if (bgEl instanceof HTMLSelectElement)
|
|
@@ -112193,7 +112764,10 @@ class Display {
|
|
|
112193
112764
|
}
|
|
112194
112765
|
else {
|
|
112195
112766
|
// Add or update a "Custom" optgroup with the custom HDR entry
|
|
112196
|
-
const label = envName
|
|
112767
|
+
const label = envName
|
|
112768
|
+
.split("/")
|
|
112769
|
+
.pop()
|
|
112770
|
+
?.replace(/\.hdr$/i, "") || "Custom HDR";
|
|
112197
112771
|
let customGroup = el.querySelector("optgroup[data-custom]");
|
|
112198
112772
|
if (customGroup) {
|
|
112199
112773
|
const opt = customGroup.querySelector("option");
|
|
@@ -112224,7 +112798,8 @@ class Display {
|
|
|
112224
112798
|
const isPreset = this.viewer.envManager.isPreset(envName);
|
|
112225
112799
|
cb.disabled = !isPreset;
|
|
112226
112800
|
if (!isPreset) {
|
|
112227
|
-
cb.title =
|
|
112801
|
+
cb.title =
|
|
112802
|
+
"4K switching is only available for built-in Poly Haven presets";
|
|
112228
112803
|
}
|
|
112229
112804
|
else {
|
|
112230
112805
|
cb.title = "";
|
|
@@ -112257,8 +112832,17 @@ class Display {
|
|
|
112257
112832
|
_dispatchAction(action) {
|
|
112258
112833
|
// Toggle buttons
|
|
112259
112834
|
const toggleActions = [
|
|
112260
|
-
"axes",
|
|
112261
|
-
"
|
|
112835
|
+
"axes",
|
|
112836
|
+
"axes0",
|
|
112837
|
+
"grid",
|
|
112838
|
+
"perspective",
|
|
112839
|
+
"transparent",
|
|
112840
|
+
"blackedges",
|
|
112841
|
+
"explode",
|
|
112842
|
+
"zscale",
|
|
112843
|
+
"distance",
|
|
112844
|
+
"properties",
|
|
112845
|
+
"select",
|
|
112262
112846
|
];
|
|
112263
112847
|
if (toggleActions.includes(action)) {
|
|
112264
112848
|
this._toggleClickButton(action);
|
|
@@ -112468,6 +113052,7 @@ class Display {
|
|
|
112468
113052
|
* @public
|
|
112469
113053
|
*/
|
|
112470
113054
|
setTheme(theme) {
|
|
113055
|
+
let resolved;
|
|
112471
113056
|
if (theme === "dark" ||
|
|
112472
113057
|
(theme === "browser" &&
|
|
112473
113058
|
window.matchMedia("(prefers-color-scheme: dark)").matches)) {
|
|
@@ -112479,7 +113064,7 @@ class Display {
|
|
|
112479
113064
|
this.viewer.gridHelper.update(this.viewer.getCameraZoom(), true, "dark");
|
|
112480
113065
|
}
|
|
112481
113066
|
this.viewer.update(true);
|
|
112482
|
-
|
|
113067
|
+
resolved = "dark";
|
|
112483
113068
|
}
|
|
112484
113069
|
else {
|
|
112485
113070
|
this.container.setAttribute("data-theme", "light");
|
|
@@ -112490,8 +113075,14 @@ class Display {
|
|
|
112490
113075
|
this.viewer.gridHelper.update(this.viewer.getCameraZoom(), true, "light");
|
|
112491
113076
|
}
|
|
112492
113077
|
this.viewer.update(true);
|
|
112493
|
-
|
|
112494
|
-
}
|
|
113078
|
+
resolved = "light";
|
|
113079
|
+
}
|
|
113080
|
+
// Keep state.theme in sync with the DOM. Without this, paths that call
|
|
113081
|
+
// setTheme directly (matchMedia listener, viewer.setTheme, MutationObserver
|
|
113082
|
+
// bridges) would update the DOM while leaving state.theme stale, and the
|
|
113083
|
+
// next viewer.render() would re-apply the stale state value
|
|
113084
|
+
this.viewer.state.set("theme", resolved, false);
|
|
113085
|
+
return resolved;
|
|
112495
113086
|
}
|
|
112496
113087
|
}
|
|
112497
113088
|
|