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
package/dist/three-cad-viewer.js
CHANGED
|
@@ -81596,9 +81596,13 @@ void main() {
|
|
|
81596
81596
|
}
|
|
81597
81597
|
function isEqual(obj1, obj2, tol = 1e-9) {
|
|
81598
81598
|
if (Array.isArray(obj1) && Array.isArray(obj2)) {
|
|
81599
|
-
return (obj1.length === obj2.length &&
|
|
81599
|
+
return (obj1.length === obj2.length &&
|
|
81600
|
+
obj1.every((v, i) => isEqual(v, obj2[i], tol)));
|
|
81600
81601
|
}
|
|
81601
|
-
else if (obj1 !== null &&
|
|
81602
|
+
else if (obj1 !== null &&
|
|
81603
|
+
obj2 !== null &&
|
|
81604
|
+
typeof obj1 === "object" &&
|
|
81605
|
+
typeof obj2 === "object") {
|
|
81602
81606
|
const rec1 = obj1;
|
|
81603
81607
|
const rec2 = obj2;
|
|
81604
81608
|
const keys1 = Object.keys(rec1);
|
|
@@ -81633,7 +81637,10 @@ void main() {
|
|
|
81633
81637
|
gpuTracker.untrack("geometry", geometry);
|
|
81634
81638
|
geometry.dispose();
|
|
81635
81639
|
for (const attr of Object.values(geometry.attributes)) {
|
|
81636
|
-
if (attr &&
|
|
81640
|
+
if (attr &&
|
|
81641
|
+
typeof attr === "object" &&
|
|
81642
|
+
"dispose" in attr &&
|
|
81643
|
+
typeof attr.dispose === "function") {
|
|
81637
81644
|
attr.dispose();
|
|
81638
81645
|
}
|
|
81639
81646
|
}
|
|
@@ -81642,12 +81649,25 @@ void main() {
|
|
|
81642
81649
|
/** All texture map property names on MaterialLike (for iteration) */
|
|
81643
81650
|
const MATERIAL_TEXTURE_KEYS = [
|
|
81644
81651
|
// MeshStandardMaterial
|
|
81645
|
-
"map",
|
|
81646
|
-
"
|
|
81652
|
+
"map",
|
|
81653
|
+
"normalMap",
|
|
81654
|
+
"roughnessMap",
|
|
81655
|
+
"metalnessMap",
|
|
81656
|
+
"aoMap",
|
|
81657
|
+
"emissiveMap",
|
|
81658
|
+
"alphaMap",
|
|
81659
|
+
"bumpMap",
|
|
81647
81660
|
// MeshPhysicalMaterial
|
|
81648
|
-
"transmissionMap",
|
|
81649
|
-
"
|
|
81650
|
-
"
|
|
81661
|
+
"transmissionMap",
|
|
81662
|
+
"clearcoatMap",
|
|
81663
|
+
"clearcoatRoughnessMap",
|
|
81664
|
+
"clearcoatNormalMap",
|
|
81665
|
+
"thicknessMap",
|
|
81666
|
+
"specularIntensityMap",
|
|
81667
|
+
"specularColorMap",
|
|
81668
|
+
"sheenColorMap",
|
|
81669
|
+
"sheenRoughnessMap",
|
|
81670
|
+
"anisotropyMap",
|
|
81651
81671
|
];
|
|
81652
81672
|
/**
|
|
81653
81673
|
* Dispose a material and detach its texture references.
|
|
@@ -81776,14 +81796,16 @@ void main() {
|
|
|
81776
81796
|
* Accepts Object3D to allow use in controls where camera type is broader.
|
|
81777
81797
|
*/
|
|
81778
81798
|
function isOrthographicCamera(obj) {
|
|
81779
|
-
return "isOrthographicCamera" in obj &&
|
|
81799
|
+
return ("isOrthographicCamera" in obj &&
|
|
81800
|
+
obj.isOrthographicCamera === true);
|
|
81780
81801
|
}
|
|
81781
81802
|
/**
|
|
81782
81803
|
* Type guard to check if an object is a PerspectiveCamera.
|
|
81783
81804
|
* Accepts Object3D to allow use in controls where camera type is broader.
|
|
81784
81805
|
*/
|
|
81785
81806
|
function isPerspectiveCamera(obj) {
|
|
81786
|
-
return "isPerspectiveCamera" in obj &&
|
|
81807
|
+
return ("isPerspectiveCamera" in obj &&
|
|
81808
|
+
obj.isPerspectiveCamera === true);
|
|
81787
81809
|
}
|
|
81788
81810
|
/**
|
|
81789
81811
|
* Type guard to check if an Object3D is a LineSegments2 (fat line).
|
|
@@ -81801,7 +81823,8 @@ void main() {
|
|
|
81801
81823
|
* Type guard to check if a material is a MeshStandardMaterial.
|
|
81802
81824
|
*/
|
|
81803
81825
|
function isMeshStandardMaterial(material) {
|
|
81804
|
-
return "isMeshStandardMaterial" in material &&
|
|
81826
|
+
return ("isMeshStandardMaterial" in material &&
|
|
81827
|
+
material.isMeshStandardMaterial === true);
|
|
81805
81828
|
}
|
|
81806
81829
|
const KeyMapper = new _KeyMapper();
|
|
81807
81830
|
class EventListenerManager {
|
|
@@ -82027,7 +82050,9 @@ void main() {
|
|
|
82027
82050
|
if (mesh.userData.excludeFromZebra)
|
|
82028
82051
|
return;
|
|
82029
82052
|
// Store original material (handle array case by taking first)
|
|
82030
|
-
const currentMaterial = Array.isArray(mesh.material)
|
|
82053
|
+
const currentMaterial = Array.isArray(mesh.material)
|
|
82054
|
+
? mesh.material[0]
|
|
82055
|
+
: mesh.material;
|
|
82031
82056
|
if (!this.originalMaterials.has(mesh.uuid)) {
|
|
82032
82057
|
this.originalMaterials.set(mesh.uuid, currentMaterial);
|
|
82033
82058
|
}
|
|
@@ -82039,7 +82064,8 @@ void main() {
|
|
|
82039
82064
|
if (hasColor(currentMaterial)) {
|
|
82040
82065
|
baseColor = currentMaterial.color.clone();
|
|
82041
82066
|
}
|
|
82042
|
-
else if (isMeshStandardMaterial(currentMaterial) &&
|
|
82067
|
+
else if (isMeshStandardMaterial(currentMaterial) &&
|
|
82068
|
+
currentMaterial.map) {
|
|
82043
82069
|
// If there's a texture but no color, use white as base
|
|
82044
82070
|
baseColor = new Color(1, 1, 1);
|
|
82045
82071
|
}
|
|
@@ -82392,7 +82418,8 @@ void main() {
|
|
|
82392
82418
|
* Skips MeshBasicMaterial and other non-PBR materials.
|
|
82393
82419
|
*/
|
|
82394
82420
|
_forEachStandardMaterial(callback) {
|
|
82395
|
-
if (this.front &&
|
|
82421
|
+
if (this.front &&
|
|
82422
|
+
this.front.material instanceof MeshStandardMaterial) {
|
|
82396
82423
|
callback(this.front.material);
|
|
82397
82424
|
}
|
|
82398
82425
|
// back can also be MeshStandardMaterial (e.g., for polygon rendering)
|
|
@@ -82407,9 +82434,7 @@ void main() {
|
|
|
82407
82434
|
highlight(flag) {
|
|
82408
82435
|
const hColor = this._getHighlightColor();
|
|
82409
82436
|
// Find primary material (front face, vertices, or edges)
|
|
82410
|
-
const primaryMaterial = this.front?.material ||
|
|
82411
|
-
this.vertices?.material ||
|
|
82412
|
-
this.edgeMaterial;
|
|
82437
|
+
const primaryMaterial = this.front?.material || this.vertices?.material || this.edgeMaterial;
|
|
82413
82438
|
if (primaryMaterial) {
|
|
82414
82439
|
this.widen(flag);
|
|
82415
82440
|
this._applyColorToMaterial(primaryMaterial, flag ? hColor : this.originalColor);
|
|
@@ -82555,12 +82580,29 @@ void main() {
|
|
|
82555
82580
|
child1.material.visible = flag;
|
|
82556
82581
|
}
|
|
82557
82582
|
}
|
|
82558
|
-
if (this.back
|
|
82559
|
-
|
|
82560
|
-
|
|
82583
|
+
if (this.back) {
|
|
82584
|
+
// Hide-path: always hide the back when the shape is hidden, even
|
|
82585
|
+
// when !renderback. Otherwise a back face left visible by a prior
|
|
82586
|
+
// setBackVisible(true) (clip tab) would remain visible after the
|
|
82587
|
+
// front goes hidden, appearing as a ghost.
|
|
82588
|
+
// Show-path: only flip back to visible when renderback is set;
|
|
82589
|
+
// when !renderback, leave back.visible alone — the clip-tab's
|
|
82590
|
+
// setBackVisible() owns it.
|
|
82591
|
+
if (!flag) {
|
|
82592
|
+
if (this._isStudioMode) {
|
|
82593
|
+
this.back.visible = false;
|
|
82594
|
+
}
|
|
82595
|
+
else {
|
|
82596
|
+
this.back.material.visible = false;
|
|
82597
|
+
}
|
|
82561
82598
|
}
|
|
82562
|
-
else {
|
|
82563
|
-
this.
|
|
82599
|
+
else if (this.renderback) {
|
|
82600
|
+
if (this._isStudioMode) {
|
|
82601
|
+
this.back.visible = true;
|
|
82602
|
+
}
|
|
82603
|
+
else {
|
|
82604
|
+
this.back.material.visible = true;
|
|
82605
|
+
}
|
|
82564
82606
|
}
|
|
82565
82607
|
}
|
|
82566
82608
|
}
|
|
@@ -82774,10 +82816,16 @@ void main() {
|
|
|
82774
82816
|
this._cadBackMaterial = this.back.material;
|
|
82775
82817
|
}
|
|
82776
82818
|
// Save original colors used by highlight/unhighlight
|
|
82777
|
-
this._cadOriginalColor = this.originalColor
|
|
82778
|
-
|
|
82819
|
+
this._cadOriginalColor = this.originalColor
|
|
82820
|
+
? this.originalColor.clone()
|
|
82821
|
+
: null;
|
|
82822
|
+
this._cadOriginalBackColor = this.originalBackColor
|
|
82823
|
+
? this.originalBackColor.clone()
|
|
82824
|
+
: null;
|
|
82779
82825
|
// Save edge visibility state
|
|
82780
|
-
this._cadEdgesVisible = this.edgeMaterial
|
|
82826
|
+
this._cadEdgesVisible = this.edgeMaterial
|
|
82827
|
+
? this.edgeMaterial.visible
|
|
82828
|
+
: null;
|
|
82781
82829
|
// --- Swap front material ---
|
|
82782
82830
|
if (this.front && studioFront) {
|
|
82783
82831
|
// Transfer per-object visibility to mesh.visible (NOT material.visible)
|
|
@@ -83294,12 +83342,18 @@ void main() {
|
|
|
83294
83342
|
* @returns THREE.SRGBColorSpace or THREE.LinearSRGBColorSpace
|
|
83295
83343
|
*/
|
|
83296
83344
|
function getColorSpaceForMap(mapName) {
|
|
83297
|
-
return THREEJS_SRGB_MAPS.has(mapName)
|
|
83345
|
+
return THREEJS_SRGB_MAPS.has(mapName)
|
|
83346
|
+
? SRGBColorSpace
|
|
83347
|
+
: LinearSRGBColorSpace;
|
|
83298
83348
|
}
|
|
83299
83349
|
|
|
83300
83350
|
/** threejs-materials property keys that hold [r,g,b] color arrays (linear RGB). */
|
|
83301
83351
|
const COLOR_ARRAY_KEYS = new Set([
|
|
83302
|
-
"color",
|
|
83352
|
+
"color",
|
|
83353
|
+
"specularColor",
|
|
83354
|
+
"sheenColor",
|
|
83355
|
+
"emissive",
|
|
83356
|
+
"attenuationColor",
|
|
83303
83357
|
]);
|
|
83304
83358
|
/** Map from threejs-materials property names to Three.js texture map property names. */
|
|
83305
83359
|
const PROPERTY_TO_MAP = {
|
|
@@ -83374,7 +83428,7 @@ void main() {
|
|
|
83374
83428
|
* Create a standard material for back faces with PBR properties.
|
|
83375
83429
|
* Used for polygon rendering where back faces need full shading.
|
|
83376
83430
|
*/
|
|
83377
|
-
createBackFaceStandardMaterial({ color, alpha, polygonOffsetUnits = 2.0, visible = true }, label) {
|
|
83431
|
+
createBackFaceStandardMaterial({ color, alpha, polygonOffsetUnits = 2.0, visible = true, }, label) {
|
|
83378
83432
|
const material = new MeshStandardMaterial({
|
|
83379
83433
|
...this._createBaseProps(alpha),
|
|
83380
83434
|
color: color,
|
|
@@ -83392,7 +83446,7 @@ void main() {
|
|
|
83392
83446
|
* Create a basic material for back faces (no lighting/PBR).
|
|
83393
83447
|
* Used for shape rendering where back faces are simple fills.
|
|
83394
83448
|
*/
|
|
83395
|
-
createBackFaceBasicMaterial({ color, alpha, polygonOffsetUnits = 2.0, visible = true }, label) {
|
|
83449
|
+
createBackFaceBasicMaterial({ color, alpha, polygonOffsetUnits = 2.0, visible = true, }, label) {
|
|
83396
83450
|
const material = new MeshBasicMaterial({
|
|
83397
83451
|
...this._createBaseProps(alpha),
|
|
83398
83452
|
color: color,
|
|
@@ -83419,7 +83473,7 @@ void main() {
|
|
|
83419
83473
|
/**
|
|
83420
83474
|
* Create a fat line material (LineMaterial from Three.js examples).
|
|
83421
83475
|
*/
|
|
83422
|
-
createEdgeMaterial({ lineWidth, color, vertexColors = false, visible = true, resolution }, label) {
|
|
83476
|
+
createEdgeMaterial({ lineWidth, color, vertexColors = false, visible = true, resolution, }, label) {
|
|
83423
83477
|
const material = new LineMaterial({
|
|
83424
83478
|
linewidth: lineWidth,
|
|
83425
83479
|
transparent: true,
|
|
@@ -83493,7 +83547,7 @@ void main() {
|
|
|
83493
83547
|
* @param label - Optional label for GPU tracking
|
|
83494
83548
|
* @returns Configured MeshPhysicalMaterial (or MeshBasicMaterial if unlit)
|
|
83495
83549
|
*/
|
|
83496
|
-
async createStudioMaterial({ materialDef, fallbackColor, fallbackAlpha, textureCache }, label) {
|
|
83550
|
+
async createStudioMaterial({ materialDef, fallbackColor, fallbackAlpha, textureCache, }, label) {
|
|
83497
83551
|
const def = materialDef;
|
|
83498
83552
|
const side = def.doubleSided ? DoubleSide : FrontSide;
|
|
83499
83553
|
// --- Resolve base color and opacity ---
|
|
@@ -83641,11 +83695,13 @@ void main() {
|
|
|
83641
83695
|
* @param values - Scalar PBR values from threejs-materials
|
|
83642
83696
|
* @param textures - Texture map references from threejs-materials
|
|
83643
83697
|
* @param textureRepeat - Optional [u, v] texture tiling applied to all loaded textures
|
|
83698
|
+
* @param textureRotation - Optional texture rotation in radians (counterclockwise),
|
|
83699
|
+
* pivoting around the texture center (0.5, 0.5)
|
|
83644
83700
|
* @param textureCache - TextureCache for resolving data URI textures
|
|
83645
83701
|
* @param label - Optional label for GPU tracking
|
|
83646
83702
|
* @returns Configured MeshPhysicalMaterial
|
|
83647
83703
|
*/
|
|
83648
|
-
async createStudioMaterialFromMaterialX(values, textures, textureRepeat, textureCache, label) {
|
|
83704
|
+
async createStudioMaterialFromMaterialX(values, textures, textureRepeat, textureRotation, textureCache, label) {
|
|
83649
83705
|
// --- Build material options from scalar values ---
|
|
83650
83706
|
const matOptions = {
|
|
83651
83707
|
flatShading: false,
|
|
@@ -83661,14 +83717,17 @@ void main() {
|
|
|
83661
83717
|
}
|
|
83662
83718
|
for (const [key, value] of Object.entries(values)) {
|
|
83663
83719
|
// Skip displacement properties (not supported, would waste GPU memory)
|
|
83664
|
-
if (key === "displacement" ||
|
|
83720
|
+
if (key === "displacement" ||
|
|
83721
|
+
key === "displacementScale" ||
|
|
83722
|
+
key === "displacementBias")
|
|
83665
83723
|
continue;
|
|
83666
83724
|
// Color arrays → THREE.Color (already linear, no sRGB conversion)
|
|
83667
83725
|
if (COLOR_ARRAY_KEYS.has(key) && Array.isArray(value)) {
|
|
83668
83726
|
const [r, g, b] = value;
|
|
83669
83727
|
matOptions[key] = new Color(r, g, b);
|
|
83670
83728
|
}
|
|
83671
|
-
else if ((key === "normalScale" || key === "clearcoatNormalScale") &&
|
|
83729
|
+
else if ((key === "normalScale" || key === "clearcoatNormalScale") &&
|
|
83730
|
+
Array.isArray(value)) {
|
|
83672
83731
|
matOptions[key] = new Vector2(value[0], value[1]);
|
|
83673
83732
|
}
|
|
83674
83733
|
else if (key === "iridescenceThicknessRange" && Array.isArray(value)) {
|
|
@@ -83687,7 +83746,8 @@ void main() {
|
|
|
83687
83746
|
matOptions.opacity = 1.0;
|
|
83688
83747
|
matOptions.depthWrite = true;
|
|
83689
83748
|
}
|
|
83690
|
-
else if (transparentVal === true ||
|
|
83749
|
+
else if (transparentVal === true ||
|
|
83750
|
+
(typeof opacityVal === "number" && opacityVal < 1.0)) {
|
|
83691
83751
|
matOptions.transparent = true;
|
|
83692
83752
|
matOptions.depthWrite = false;
|
|
83693
83753
|
}
|
|
@@ -83712,10 +83772,19 @@ void main() {
|
|
|
83712
83772
|
: "normalTexture";
|
|
83713
83773
|
let tex = await textureCache.get(textureRef, roleForCache);
|
|
83714
83774
|
if (tex) {
|
|
83715
|
-
|
|
83775
|
+
const needsRepeat = textureRepeat &&
|
|
83776
|
+
(textureRepeat[0] !== 1 || textureRepeat[1] !== 1);
|
|
83777
|
+
const needsRotation = !!textureRotation;
|
|
83778
|
+
if (needsRepeat || needsRotation) {
|
|
83716
83779
|
// Clone to avoid mutating shared cached texture
|
|
83717
83780
|
tex = tex.clone();
|
|
83718
|
-
|
|
83781
|
+
if (needsRepeat) {
|
|
83782
|
+
tex.repeat.set(textureRepeat[0], textureRepeat[1]);
|
|
83783
|
+
}
|
|
83784
|
+
if (needsRotation) {
|
|
83785
|
+
tex.center.set(0.5, 0.5); // pivot around texture center
|
|
83786
|
+
tex.rotation = textureRotation;
|
|
83787
|
+
}
|
|
83719
83788
|
}
|
|
83720
83789
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
83721
83790
|
material[mapName] = tex;
|
|
@@ -83927,7 +83996,7 @@ void main() {
|
|
|
83927
83996
|
// ---------------------------------------------------------------------------
|
|
83928
83997
|
// Metals -- Polished
|
|
83929
83998
|
// ---------------------------------------------------------------------------
|
|
83930
|
-
|
|
83999
|
+
chrome: {
|
|
83931
84000
|
name: "Chrome",
|
|
83932
84001
|
color: [0.98, 0.98, 0.98, 1],
|
|
83933
84002
|
metalness: 1.0,
|
|
@@ -83945,19 +84014,19 @@ void main() {
|
|
|
83945
84014
|
metalness: 1.0,
|
|
83946
84015
|
roughness: 0.1,
|
|
83947
84016
|
},
|
|
83948
|
-
|
|
84017
|
+
gold: {
|
|
83949
84018
|
name: "Gold",
|
|
83950
84019
|
color: [1, 0.93, 0, 1],
|
|
83951
84020
|
metalness: 1.0,
|
|
83952
84021
|
roughness: 0.1,
|
|
83953
84022
|
},
|
|
83954
|
-
|
|
84023
|
+
copper: {
|
|
83955
84024
|
name: "Copper",
|
|
83956
84025
|
color: [0.98, 0.82, 0.76, 1],
|
|
83957
84026
|
metalness: 1.0,
|
|
83958
84027
|
roughness: 0.15,
|
|
83959
84028
|
},
|
|
83960
|
-
|
|
84029
|
+
brass: {
|
|
83961
84030
|
name: "Brass",
|
|
83962
84031
|
color: [0.95, 0.9, 0.7, 1],
|
|
83963
84032
|
metalness: 1.0,
|
|
@@ -83985,13 +84054,13 @@ void main() {
|
|
|
83985
84054
|
metalness: 0.9,
|
|
83986
84055
|
roughness: 0.7,
|
|
83987
84056
|
},
|
|
83988
|
-
|
|
84057
|
+
titanium: {
|
|
83989
84058
|
name: "Titanium",
|
|
83990
84059
|
color: [0.81, 0.79, 0.77, 1],
|
|
83991
84060
|
metalness: 1.0,
|
|
83992
84061
|
roughness: 0.45,
|
|
83993
84062
|
},
|
|
83994
|
-
|
|
84063
|
+
galvanized: {
|
|
83995
84064
|
name: "Galvanized",
|
|
83996
84065
|
color: [0.88, 0.88, 0.9, 1],
|
|
83997
84066
|
metalness: 0.8,
|
|
@@ -84018,7 +84087,7 @@ void main() {
|
|
|
84018
84087
|
metalness: 0.0,
|
|
84019
84088
|
roughness: 0.4,
|
|
84020
84089
|
},
|
|
84021
|
-
|
|
84090
|
+
nylon: {
|
|
84022
84091
|
name: "Nylon",
|
|
84023
84092
|
color: [0.95, 0.94, 0.92, 1],
|
|
84024
84093
|
metalness: 0.0,
|
|
@@ -84128,7 +84197,7 @@ void main() {
|
|
|
84128
84197
|
roughness: 0.35,
|
|
84129
84198
|
anisotropy: 0.3,
|
|
84130
84199
|
},
|
|
84131
|
-
|
|
84200
|
+
concrete: {
|
|
84132
84201
|
name: "Concrete",
|
|
84133
84202
|
color: [0.83, 0.82, 0.8, 1],
|
|
84134
84203
|
metalness: 0.0,
|
|
@@ -84181,6 +84250,7 @@ varying vec3 vTriplanarNormal;
|
|
|
84181
84250
|
uniform vec3 triplanarOffset;
|
|
84182
84251
|
uniform float triplanarScale;
|
|
84183
84252
|
uniform vec2 triplanarRepeat;
|
|
84253
|
+
uniform float triplanarRotation;
|
|
84184
84254
|
|
|
84185
84255
|
// normalMatrix is only declared in the fragment shader for object-space
|
|
84186
84256
|
// normal maps. We need it for triplanar tangent-space normal mapping too.
|
|
@@ -84203,6 +84273,20 @@ void initTriplanarUVs() {
|
|
|
84203
84273
|
tri_uvX = p.yz * r;
|
|
84204
84274
|
tri_uvY = p.xz * r;
|
|
84205
84275
|
tri_uvZ = p.xy * r;
|
|
84276
|
+
|
|
84277
|
+
// Apply rotation (counterclockwise, radians) if non-zero. The texture
|
|
84278
|
+
// coords are unbounded in triplanar (derived from world-space position),
|
|
84279
|
+
// so the pivot for rotation is the UV origin — for a tiled/repeating
|
|
84280
|
+
// texture this is visually equivalent to any other pivot modulo tile
|
|
84281
|
+
// wrapping, and is the simplest choice.
|
|
84282
|
+
if (triplanarRotation != 0.0) {
|
|
84283
|
+
float c = cos(triplanarRotation);
|
|
84284
|
+
float s = sin(triplanarRotation);
|
|
84285
|
+
mat2 rot = mat2(c, -s, s, c);
|
|
84286
|
+
tri_uvX = rot * tri_uvX;
|
|
84287
|
+
tri_uvY = rot * tri_uvY;
|
|
84288
|
+
tri_uvZ = rot * tri_uvZ;
|
|
84289
|
+
}
|
|
84206
84290
|
}
|
|
84207
84291
|
|
|
84208
84292
|
// Sample a texture using the global triplanar UVs and blend weights.
|
|
@@ -84403,14 +84487,24 @@ float metalnessFactor = metalness;
|
|
|
84403
84487
|
// Uniform values (captured by closure, per-object)
|
|
84404
84488
|
const offset = bb.min.clone();
|
|
84405
84489
|
const scale = 1.0 / maxDim;
|
|
84406
|
-
// Read texture repeat from the first available texture map
|
|
84407
|
-
|
|
84408
|
-
|
|
84490
|
+
// Read texture repeat and rotation from the first available texture map.
|
|
84491
|
+
// texture.rotation is set by the MaterialX path when textureRotation is
|
|
84492
|
+
// provided; for triplanar sampling we read it back and apply via uniform
|
|
84493
|
+
// since the triplanar shader bypasses three.js's texture-matrix transform.
|
|
84494
|
+
const firstMap = material.map ??
|
|
84495
|
+
material.roughnessMap ??
|
|
84496
|
+
material.normalMap ??
|
|
84497
|
+
material.metalnessMap ??
|
|
84498
|
+
material.emissiveMap ??
|
|
84499
|
+
material.aoMap;
|
|
84500
|
+
const repeat = firstMap?.repeat?.clone() ?? new Vector2(1, 1);
|
|
84501
|
+
const rotation = firstMap?.rotation ?? 0;
|
|
84409
84502
|
material.onBeforeCompile = (shader) => {
|
|
84410
84503
|
// Custom uniforms
|
|
84411
84504
|
shader.uniforms.triplanarOffset = { value: offset };
|
|
84412
84505
|
shader.uniforms.triplanarScale = { value: scale };
|
|
84413
84506
|
shader.uniforms.triplanarRepeat = { value: repeat };
|
|
84507
|
+
shader.uniforms.triplanarRotation = { value: rotation };
|
|
84414
84508
|
// --- Vertex shader ---
|
|
84415
84509
|
// Declare varyings
|
|
84416
84510
|
shader.vertexShader = shader.vertexShader.replace("#include <common>", `#include <common>\n${TRIPLANAR_VARYINGS}`);
|
|
@@ -84481,11 +84575,22 @@ float metalnessFactor = metalness;
|
|
|
84481
84575
|
*/
|
|
84482
84576
|
/** Texture field names on MaterialAppearance that require UV coordinates. */
|
|
84483
84577
|
const TEXTURE_FIELDS = [
|
|
84484
|
-
"map",
|
|
84485
|
-
"
|
|
84486
|
-
"
|
|
84487
|
-
"
|
|
84488
|
-
"
|
|
84578
|
+
"map",
|
|
84579
|
+
"normalMap",
|
|
84580
|
+
"aoMap",
|
|
84581
|
+
"metalnessMap",
|
|
84582
|
+
"roughnessMap",
|
|
84583
|
+
"emissiveMap",
|
|
84584
|
+
"transmissionMap",
|
|
84585
|
+
"clearcoatMap",
|
|
84586
|
+
"clearcoatRoughnessMap",
|
|
84587
|
+
"clearcoatNormalMap",
|
|
84588
|
+
"thicknessMap",
|
|
84589
|
+
"specularIntensityMap",
|
|
84590
|
+
"specularColorMap",
|
|
84591
|
+
"sheenColorMap",
|
|
84592
|
+
"sheenRoughnessMap",
|
|
84593
|
+
"anisotropyMap",
|
|
84489
84594
|
];
|
|
84490
84595
|
/** Check whether a resolved MaterialAppearance references any texture. */
|
|
84491
84596
|
function materialHasTexture(def) {
|
|
@@ -85217,7 +85322,7 @@ float metalnessFactor = metalness;
|
|
|
85217
85322
|
* 3. Clone BackSide variant for renderback objects
|
|
85218
85323
|
* 4. Auto-generate box-projected UVs when textured but geometry has no UVs
|
|
85219
85324
|
*/
|
|
85220
|
-
async enterStudioMode(textureMapping = "
|
|
85325
|
+
async enterStudioMode(textureMapping = "parametric") {
|
|
85221
85326
|
// Create TextureCache lazily
|
|
85222
85327
|
if (!this._textureCache) {
|
|
85223
85328
|
this._textureCache = new TextureCache();
|
|
@@ -85251,7 +85356,8 @@ float metalnessFactor = metalness;
|
|
|
85251
85356
|
try {
|
|
85252
85357
|
if (resolved && isMaterialXMaterial(resolved)) {
|
|
85253
85358
|
// --- threejs-materials path ---
|
|
85254
|
-
studioMaterial =
|
|
85359
|
+
studioMaterial =
|
|
85360
|
+
await this.materialFactory.createStudioMaterialFromMaterialX(resolved.values, resolved.textures, resolved.textureRepeat, resolved.textureRotation, this._textureCache);
|
|
85255
85361
|
if (materialXHasTextures(resolved)) {
|
|
85256
85362
|
this._texturedMaterialKeys.add(sharingKey);
|
|
85257
85363
|
}
|
|
@@ -85301,7 +85407,8 @@ float metalnessFactor = metalness;
|
|
|
85301
85407
|
if (textured) {
|
|
85302
85408
|
logger.debug(`Studio "${path}": ${needsTriplanar ? "using triplanar" : "using parametric UVs"}`);
|
|
85303
85409
|
}
|
|
85304
|
-
if (needsTriplanar &&
|
|
85410
|
+
if (needsTriplanar &&
|
|
85411
|
+
studioMaterial instanceof MeshPhysicalMaterial) {
|
|
85305
85412
|
const triKey = `${sharingKey}:tri:${path}`;
|
|
85306
85413
|
let triMat = this._studioMaterialCache.get(triKey);
|
|
85307
85414
|
if (!triMat) {
|
|
@@ -85313,7 +85420,8 @@ float metalnessFactor = metalness;
|
|
|
85313
85420
|
}
|
|
85314
85421
|
// Build back-face variant if needed
|
|
85315
85422
|
let studioBack = null;
|
|
85316
|
-
if (obj.renderback &&
|
|
85423
|
+
if (obj.renderback &&
|
|
85424
|
+
studioMaterial instanceof MeshPhysicalMaterial) {
|
|
85317
85425
|
const backKey = needsTriplanar
|
|
85318
85426
|
? `${sharingKey}:tri:${path}:back`
|
|
85319
85427
|
: `${sharingKey}:back`;
|
|
@@ -85341,7 +85449,9 @@ float metalnessFactor = metalness;
|
|
|
85341
85449
|
}
|
|
85342
85450
|
}
|
|
85343
85451
|
// Apply to ObjectGroup
|
|
85344
|
-
obj.enterStudioMode(studioMaterial instanceof MeshPhysicalMaterial
|
|
85452
|
+
obj.enterStudioMode(studioMaterial instanceof MeshPhysicalMaterial
|
|
85453
|
+
? studioMaterial
|
|
85454
|
+
: null, studioBack);
|
|
85345
85455
|
}
|
|
85346
85456
|
this._isStudioMode = true;
|
|
85347
85457
|
return [...unresolvedTags];
|
|
@@ -86604,7 +86714,10 @@ float metalnessFactor = metalness;
|
|
|
86604
86714
|
*/
|
|
86605
86715
|
_buildTreeStructure(data) {
|
|
86606
86716
|
const build = (data, path, level) => {
|
|
86607
|
-
const result = [
|
|
86717
|
+
const result = [
|
|
86718
|
+
States.unselected,
|
|
86719
|
+
States.unselected,
|
|
86720
|
+
];
|
|
86608
86721
|
const calcState = (states) => {
|
|
86609
86722
|
for (const s of [0, 1]) {
|
|
86610
86723
|
if (states[States.mixed][s] ||
|
|
@@ -87093,7 +87206,8 @@ float metalnessFactor = metalness;
|
|
|
87093
87206
|
this.update = (prefix = null) => {
|
|
87094
87207
|
if (!this.container || !this.model)
|
|
87095
87208
|
return;
|
|
87096
|
-
const visibleElements = this.getVisibleElements().filter((p) => p instanceof HTMLElement &&
|
|
87209
|
+
const visibleElements = this.getVisibleElements().filter((p) => p instanceof HTMLElement &&
|
|
87210
|
+
(prefix == null || (p.dataset.path || "").startsWith(prefix)));
|
|
87097
87211
|
for (const el of visibleElements) {
|
|
87098
87212
|
const path = el.dataset.path || "";
|
|
87099
87213
|
const node = this.findNodeByPath(path);
|
|
@@ -87729,7 +87843,8 @@ float metalnessFactor = metalness;
|
|
|
87729
87843
|
this.showChildContainer(node);
|
|
87730
87844
|
const el = this.getDomNode(path);
|
|
87731
87845
|
if (el != null) {
|
|
87732
|
-
this.scrollContainer.scrollTop =
|
|
87846
|
+
this.scrollContainer.scrollTop =
|
|
87847
|
+
el.offsetTop - this.scrollContainer.offsetTop;
|
|
87733
87848
|
}
|
|
87734
87849
|
if (this.debug) {
|
|
87735
87850
|
logger.debug("update => collapsePath");
|
|
@@ -87750,7 +87865,8 @@ float metalnessFactor = metalness;
|
|
|
87750
87865
|
this.model.setExpandedLevel(level);
|
|
87751
87866
|
const el = this.getDomNode(this.getNodePath(this.root));
|
|
87752
87867
|
if (el != null) {
|
|
87753
|
-
this.scrollContainer.scrollTop =
|
|
87868
|
+
this.scrollContainer.scrollTop =
|
|
87869
|
+
el.offsetTop - this.scrollContainer.offsetTop;
|
|
87754
87870
|
}
|
|
87755
87871
|
// Multiple updates to ensure all levels are rendered
|
|
87756
87872
|
const maxIterations = level === -1 ? this.maxLevel : level;
|
|
@@ -87983,7 +88099,9 @@ float metalnessFactor = metalness;
|
|
|
87983
88099
|
*/
|
|
87984
88100
|
// @ts-expect-error -- THREE.Plane.clone() returns `this`, but we need a concrete CenteredPlane
|
|
87985
88101
|
clone() {
|
|
87986
|
-
return new CenteredPlane(this.normal.clone(), this.centeredConstant, [
|
|
88102
|
+
return new CenteredPlane(this.normal.clone(), this.centeredConstant, [
|
|
88103
|
+
...this.center,
|
|
88104
|
+
]);
|
|
87987
88105
|
}
|
|
87988
88106
|
}
|
|
87989
88107
|
// ============================================================================
|
|
@@ -88193,7 +88311,9 @@ float metalnessFactor = metalness;
|
|
|
88193
88311
|
let j = 0;
|
|
88194
88312
|
for (const path in this.nestedGroup.groups) {
|
|
88195
88313
|
const group = this.nestedGroup.groups[path];
|
|
88196
|
-
if (group instanceof ObjectGroup &&
|
|
88314
|
+
if (group instanceof ObjectGroup &&
|
|
88315
|
+
group.subtype === "solid" &&
|
|
88316
|
+
group.front) {
|
|
88197
88317
|
// Store color for each plane-solid combination (mirrors _planeMeshGroup order)
|
|
88198
88318
|
const frontMesh = group.front;
|
|
88199
88319
|
const material = frontMesh.material;
|
|
@@ -88713,7 +88833,8 @@ float metalnessFactor = metalness;
|
|
|
88713
88833
|
}
|
|
88714
88834
|
else {
|
|
88715
88835
|
// Non-binary format: nested number[][] arrays
|
|
88716
|
-
if (!Array.isArray(shape.triangles) ||
|
|
88836
|
+
if (!Array.isArray(shape.triangles) ||
|
|
88837
|
+
!Array.isArray(shape.triangles[0])) {
|
|
88717
88838
|
throw new Error("Expected nested array for triangles in non-binary format");
|
|
88718
88839
|
}
|
|
88719
88840
|
// After validation, we know shape.triangles is number[][] (TypeScript can't infer this)
|
|
@@ -88841,7 +88962,8 @@ float metalnessFactor = metalness;
|
|
|
88841
88962
|
else {
|
|
88842
88963
|
// Non-binary format: nested number[][] arrays
|
|
88843
88964
|
const edgesRaw = shape.edges;
|
|
88844
|
-
if (!Array.isArray(edgesRaw) ||
|
|
88965
|
+
if (!Array.isArray(edgesRaw) ||
|
|
88966
|
+
(edgesRaw.length > 0 && !Array.isArray(edgesRaw[0]))) {
|
|
88845
88967
|
throw new Error("Expected nested array for edges in non-binary format");
|
|
88846
88968
|
}
|
|
88847
88969
|
// After validation, we know this is number[][] (TypeScript can't infer from the check)
|
|
@@ -92986,8 +93108,14 @@ float metalnessFactor = metalness;
|
|
|
92986
93108
|
rear: { pos: new Vector3(0, 1, 0), quat: null },
|
|
92987
93109
|
left: { pos: new Vector3(-1, 0, 0), quat: null },
|
|
92988
93110
|
right: { pos: new Vector3(1, 0, 0), quat: null },
|
|
92989
|
-
top: {
|
|
92990
|
-
|
|
93111
|
+
top: {
|
|
93112
|
+
pos: new Vector3(0, 0, 1),
|
|
93113
|
+
quat: new Quaternion(0, 0, 0, 1),
|
|
93114
|
+
},
|
|
93115
|
+
bottom: {
|
|
93116
|
+
pos: new Vector3(0, 0, -1),
|
|
93117
|
+
quat: new Quaternion(1, 0, 0, 0),
|
|
93118
|
+
},
|
|
92991
93119
|
},
|
|
92992
93120
|
legacy: {
|
|
92993
93121
|
// legacy Z up
|
|
@@ -93511,7 +93639,10 @@ float metalnessFactor = metalness;
|
|
|
93511
93639
|
const object = obj.object;
|
|
93512
93640
|
// Accept Mesh (faces), Points (vertices), and Line (edges)
|
|
93513
93641
|
const isValidType = isMesh(object) || isPoints(object) || isLine(object);
|
|
93514
|
-
if (isValidType &&
|
|
93642
|
+
if (isValidType &&
|
|
93643
|
+
object.visible &&
|
|
93644
|
+
!Array.isArray(object.material) &&
|
|
93645
|
+
object.material.visible) {
|
|
93515
93646
|
validObjs.push(obj);
|
|
93516
93647
|
}
|
|
93517
93648
|
}
|
|
@@ -93531,7 +93662,9 @@ float metalnessFactor = metalness;
|
|
|
93531
93662
|
const isValidType = isMesh(obj) || isPoints(obj) || isLine(obj);
|
|
93532
93663
|
if (!isValidType)
|
|
93533
93664
|
continue;
|
|
93534
|
-
if (!obj.visible ||
|
|
93665
|
+
if (!obj.visible ||
|
|
93666
|
+
Array.isArray(obj.material) ||
|
|
93667
|
+
!obj.material.visible)
|
|
93535
93668
|
continue;
|
|
93536
93669
|
const objectGroup = object.object.parent;
|
|
93537
93670
|
if (!isObjectGroup(objectGroup))
|
|
@@ -93560,7 +93693,9 @@ float metalnessFactor = metalness;
|
|
|
93560
93693
|
* Type guard to check if a value is a number array of specific length.
|
|
93561
93694
|
*/
|
|
93562
93695
|
function isNumberArray(value, length) {
|
|
93563
|
-
return Array.isArray(value) &&
|
|
93696
|
+
return (Array.isArray(value) &&
|
|
93697
|
+
value.length === length &&
|
|
93698
|
+
value.every((v) => typeof v === "number"));
|
|
93564
93699
|
}
|
|
93565
93700
|
/**
|
|
93566
93701
|
* Type guard to check if a value is a Record<string, number[]> (bounding box data).
|
|
@@ -93711,9 +93846,17 @@ float metalnessFactor = metalness;
|
|
|
93711
93846
|
* Skip list for technical fields that should not be rendered in panels.
|
|
93712
93847
|
*/
|
|
93713
93848
|
const SKIP_KEYS = [
|
|
93714
|
-
"type",
|
|
93715
|
-
"
|
|
93716
|
-
"
|
|
93849
|
+
"type",
|
|
93850
|
+
"tool_type",
|
|
93851
|
+
"subtype",
|
|
93852
|
+
"info",
|
|
93853
|
+
"refpoint",
|
|
93854
|
+
"refpoint1",
|
|
93855
|
+
"refpoint2",
|
|
93856
|
+
"shape_type",
|
|
93857
|
+
"geom_type",
|
|
93858
|
+
"groups",
|
|
93859
|
+
"result",
|
|
93717
93860
|
];
|
|
93718
93861
|
/**
|
|
93719
93862
|
* Render entries from a group object into a tbody.
|
|
@@ -94131,7 +94274,10 @@ float metalnessFactor = metalness;
|
|
|
94131
94274
|
e.stopPropagation();
|
|
94132
94275
|
};
|
|
94133
94276
|
this._movePanel = () => {
|
|
94134
|
-
if (!this.panel ||
|
|
94277
|
+
if (!this.panel ||
|
|
94278
|
+
!this.viewer ||
|
|
94279
|
+
!this.viewer.camera ||
|
|
94280
|
+
!this.panel.isVisible())
|
|
94135
94281
|
return;
|
|
94136
94282
|
const canvasRect = this.viewer.renderer.domElement.getBoundingClientRect();
|
|
94137
94283
|
const panelRect = this.panel.html.getBoundingClientRect();
|
|
@@ -94331,8 +94477,15 @@ float metalnessFactor = metalness;
|
|
|
94331
94477
|
responseData = {
|
|
94332
94478
|
groups: [
|
|
94333
94479
|
{ distance: 2.345, info: "center" },
|
|
94334
|
-
{
|
|
94335
|
-
|
|
94480
|
+
{
|
|
94481
|
+
"point 1": this.point1.toArray(),
|
|
94482
|
+
"point 2": this.point2.toArray(),
|
|
94483
|
+
},
|
|
94484
|
+
{
|
|
94485
|
+
angle: 43.21,
|
|
94486
|
+
"reference 1": "Plane (Face)",
|
|
94487
|
+
"reference 2": "Plane (Face)",
|
|
94488
|
+
},
|
|
94336
94489
|
],
|
|
94337
94490
|
type: "backend_response",
|
|
94338
94491
|
refpoint1: this.point1.toArray(),
|
|
@@ -94351,10 +94504,21 @@ float metalnessFactor = metalness;
|
|
|
94351
94504
|
geom_type: "EllipseArc",
|
|
94352
94505
|
refpoint: this.point1.toArray(),
|
|
94353
94506
|
groups: [
|
|
94354
|
-
{
|
|
94507
|
+
{
|
|
94508
|
+
center: this.point1.toArray(),
|
|
94509
|
+
"major radius": 0.4,
|
|
94510
|
+
"minor radius": 0.2,
|
|
94511
|
+
},
|
|
94355
94512
|
{ start: [2.4, -1, 0.0], end: [1.8, -0.8267949192431111, 0.0] },
|
|
94356
94513
|
{ length: 0.6868592404716374 },
|
|
94357
|
-
{
|
|
94514
|
+
{
|
|
94515
|
+
bb: {
|
|
94516
|
+
min: [1.8, -1, 0.0],
|
|
94517
|
+
center: [2.1, -0.9, 0.0],
|
|
94518
|
+
max: [2.4, -0.8, 0.0],
|
|
94519
|
+
size: [0.56, 0.2, 0.0],
|
|
94520
|
+
},
|
|
94521
|
+
},
|
|
94358
94522
|
],
|
|
94359
94523
|
};
|
|
94360
94524
|
}
|
|
@@ -94466,7 +94630,8 @@ float metalnessFactor = metalness;
|
|
|
94466
94630
|
this.debug = debug;
|
|
94467
94631
|
}
|
|
94468
94632
|
_createPanel() {
|
|
94469
|
-
if (isDistancePanel(this.panel) &&
|
|
94633
|
+
if (isDistancePanel(this.panel) &&
|
|
94634
|
+
isDistanceResponseData(this.responseData)) {
|
|
94470
94635
|
this.panel.createTable(this.responseData);
|
|
94471
94636
|
}
|
|
94472
94637
|
}
|
|
@@ -94526,7 +94691,8 @@ float metalnessFactor = metalness;
|
|
|
94526
94691
|
this.debug = debug;
|
|
94527
94692
|
}
|
|
94528
94693
|
_createPanel() {
|
|
94529
|
-
if (isPropertiesPanel(this.panel) &&
|
|
94694
|
+
if (isPropertiesPanel(this.panel) &&
|
|
94695
|
+
isPropertiesResponseData(this.responseData)) {
|
|
94530
94696
|
this.panel.createTable(this.responseData);
|
|
94531
94697
|
}
|
|
94532
94698
|
}
|
|
@@ -94534,7 +94700,8 @@ float metalnessFactor = metalness;
|
|
|
94534
94700
|
return 1;
|
|
94535
94701
|
}
|
|
94536
94702
|
_getPoint() {
|
|
94537
|
-
if (isPropertiesResponseData(this.responseData) &&
|
|
94703
|
+
if (isPropertiesResponseData(this.responseData) &&
|
|
94704
|
+
this.responseData.refpoint) {
|
|
94538
94705
|
this.point1 = new Vector3(...this.responseData.refpoint);
|
|
94539
94706
|
}
|
|
94540
94707
|
}
|
|
@@ -94801,7 +94968,7 @@ float metalnessFactor = metalness;
|
|
|
94801
94968
|
}
|
|
94802
94969
|
}
|
|
94803
94970
|
|
|
94804
|
-
const version = "4.3.
|
|
94971
|
+
const version = "4.3.9";
|
|
94805
94972
|
|
|
94806
94973
|
/**
|
|
94807
94974
|
* Clean room environment for Studio mode PMREM generation.
|
|
@@ -94906,7 +95073,7 @@ float metalnessFactor = metalness;
|
|
|
94906
95073
|
const indices = [];
|
|
94907
95074
|
const sign = -1 ;
|
|
94908
95075
|
for (let i = 0; i <= segments; i++) {
|
|
94909
|
-
const angle = (i / segments) * Math.PI / 2;
|
|
95076
|
+
const angle = ((i / segments) * Math.PI) / 2;
|
|
94910
95077
|
const h = sign * radius * Math.sin(angle); // horizontal offset toward wall
|
|
94911
95078
|
const y = radius * (1 - Math.cos(angle)); // vertical offset above floor
|
|
94912
95079
|
const nh = sign * Math.sin(angle); // normal toward wall
|
|
@@ -95455,7 +95622,7 @@ float metalnessFactor = metalness;
|
|
|
95455
95622
|
}
|
|
95456
95623
|
if (exponent === 31) {
|
|
95457
95624
|
// Infinity or NaN
|
|
95458
|
-
return mantissa ? NaN :
|
|
95625
|
+
return mantissa ? NaN : sign ? -Infinity : Infinity;
|
|
95459
95626
|
}
|
|
95460
95627
|
return (sign ? -1 : 1) * Math.pow(2, exponent - 15) * (1 + mantissa / 1024);
|
|
95461
95628
|
}
|
|
@@ -95486,7 +95653,7 @@ float metalnessFactor = metalness;
|
|
|
95486
95653
|
for (let sy = 0; sy < height; sy++) {
|
|
95487
95654
|
const gy = Math.min(Math.floor((sy / height) * GRID_H), GRID_H - 1);
|
|
95488
95655
|
// cos(latitude) weighting: equator has more area than poles
|
|
95489
|
-
const phi = (
|
|
95656
|
+
const phi = (0.5 - sy / height) * Math.PI;
|
|
95490
95657
|
const cosWeight = Math.cos(phi);
|
|
95491
95658
|
for (let sx = 0; sx < width; sx++) {
|
|
95492
95659
|
const gx = Math.min(Math.floor((sx / width) * GRID_W), GRID_W - 1);
|
|
@@ -95525,7 +95692,9 @@ float metalnessFactor = metalness;
|
|
|
95525
95692
|
}
|
|
95526
95693
|
}
|
|
95527
95694
|
// 2. Compute median luminance and threshold
|
|
95528
|
-
const sorted = Array.from(grid)
|
|
95695
|
+
const sorted = Array.from(grid)
|
|
95696
|
+
.filter((v) => v > 0)
|
|
95697
|
+
.sort((a, b) => a - b);
|
|
95529
95698
|
if (sorted.length === 0) {
|
|
95530
95699
|
return { lights: [], wasAnalyzed: true };
|
|
95531
95700
|
}
|
|
@@ -95621,9 +95790,9 @@ float metalnessFactor = metalness;
|
|
|
95621
95790
|
let color;
|
|
95622
95791
|
if (colorTotal > 0) {
|
|
95623
95792
|
color = [
|
|
95624
|
-
c.totalR / colorTotal * 3,
|
|
95625
|
-
c.totalG / colorTotal * 3,
|
|
95626
|
-
c.totalB / colorTotal * 3,
|
|
95793
|
+
(c.totalR / colorTotal) * 3,
|
|
95794
|
+
(c.totalG / colorTotal) * 3,
|
|
95795
|
+
(c.totalB / colorTotal) * 3,
|
|
95627
95796
|
];
|
|
95628
95797
|
// Clamp to 0-1
|
|
95629
95798
|
color = [
|
|
@@ -95784,14 +95953,6 @@ float metalnessFactor = metalness;
|
|
|
95784
95953
|
constructor(options = {}) {
|
|
95785
95954
|
/** Cached PMREM render targets keyed by environment name or URL */
|
|
95786
95955
|
this._cache = new Map();
|
|
95787
|
-
/**
|
|
95788
|
-
* Cached raw equirectangular HDR textures keyed by the same name/URL.
|
|
95789
|
-
* Preserved (not disposed after PMREM generation) so `scene.background`
|
|
95790
|
-
* can sample the original HDR at full source resolution instead of the
|
|
95791
|
-
* 256² PMREM cubemap. Only populated by `_loadHdr` — procedural
|
|
95792
|
-
* environments have no source HDR.
|
|
95793
|
-
*/
|
|
95794
|
-
this._hdrCache = new Map();
|
|
95795
95956
|
/** Cached light detection results keyed by environment name or URL */
|
|
95796
95957
|
this._lightDetectionCache = new Map();
|
|
95797
95958
|
/** In-flight load promises keyed by environment name or URL */
|
|
@@ -95806,13 +95967,6 @@ float metalnessFactor = metalness;
|
|
|
95806
95967
|
this._hdrLoader = null;
|
|
95807
95968
|
/** The last loaded PMREM texture (stateful — used by apply() for IBL) */
|
|
95808
95969
|
this._currentTexture = null;
|
|
95809
|
-
/**
|
|
95810
|
-
* Raw HDR texture corresponding to `_currentTexture`, used for
|
|
95811
|
-
* `scene.background` to keep the backdrop at source resolution. Null when
|
|
95812
|
-
* the current environment is procedural ("studio" RoomEnvironment) — in
|
|
95813
|
-
* that case the background falls back to `_currentTexture` (the PMREM).
|
|
95814
|
-
*/
|
|
95815
|
-
this._currentBackgroundTexture = null;
|
|
95816
95970
|
/** Whether this manager has been disposed */
|
|
95817
95971
|
this._disposed = false;
|
|
95818
95972
|
/**
|
|
@@ -95837,7 +95991,8 @@ float metalnessFactor = metalness;
|
|
|
95837
95991
|
* re-apply once the texture is ready.
|
|
95838
95992
|
*/
|
|
95839
95993
|
this._deferredApply = null;
|
|
95840
|
-
this._userOverrides =
|
|
95994
|
+
this._userOverrides =
|
|
95995
|
+
options.presetUrls ?? {};
|
|
95841
95996
|
this._presetUrls = {
|
|
95842
95997
|
..._buildPresetUrls(false),
|
|
95843
95998
|
...this._userOverrides,
|
|
@@ -95866,7 +96021,6 @@ float metalnessFactor = metalness;
|
|
|
95866
96021
|
}
|
|
95867
96022
|
if (name === "none") {
|
|
95868
96023
|
this._currentTexture = null;
|
|
95869
|
-
this._currentBackgroundTexture = null;
|
|
95870
96024
|
return null;
|
|
95871
96025
|
}
|
|
95872
96026
|
// Check cache first (name is the cache key for presets; URL string for custom)
|
|
@@ -95875,7 +96029,6 @@ float metalnessFactor = metalness;
|
|
|
95875
96029
|
if (cached) {
|
|
95876
96030
|
logger.debug(`Environment "${cacheKey}" loaded from cache`);
|
|
95877
96031
|
this._currentTexture = cached.texture;
|
|
95878
|
-
this._currentBackgroundTexture = this._hdrCache.get(cacheKey) ?? null;
|
|
95879
96032
|
return cached.texture;
|
|
95880
96033
|
}
|
|
95881
96034
|
// Check in-flight promise — await and set _currentTexture
|
|
@@ -95884,7 +96037,6 @@ float metalnessFactor = metalness;
|
|
|
95884
96037
|
logger.debug(`Environment "${cacheKey}" already loading, reusing promise`);
|
|
95885
96038
|
const texture = await inflight;
|
|
95886
96039
|
this._currentTexture = texture;
|
|
95887
|
-
this._currentBackgroundTexture = this._hdrCache.get(cacheKey) ?? null;
|
|
95888
96040
|
return texture;
|
|
95889
96041
|
}
|
|
95890
96042
|
// Start new load
|
|
@@ -95893,7 +96045,6 @@ float metalnessFactor = metalness;
|
|
|
95893
96045
|
try {
|
|
95894
96046
|
const texture = await promise;
|
|
95895
96047
|
this._currentTexture = texture;
|
|
95896
|
-
this._currentBackgroundTexture = this._hdrCache.get(cacheKey) ?? null;
|
|
95897
96048
|
// Self-healing: if apply() was called with "environment" background
|
|
95898
96049
|
// while texture was null, re-apply now that the texture is ready.
|
|
95899
96050
|
if (this._deferredApply) {
|
|
@@ -95933,8 +96084,13 @@ float metalnessFactor = metalness;
|
|
|
95933
96084
|
scene.environmentIntensity = envIntensity;
|
|
95934
96085
|
// HDR maps assume Y-up; rotate 90° around X to align with Z-up scenes.
|
|
95935
96086
|
// Additional rotation for user-controlled azimuthal rotation.
|
|
96087
|
+
// Note: Z-up branch uses "ZYX" Euler order so the matrix is
|
|
96088
|
+
// Rz(rotY) · Rx(PI/2). With the default "XYZ" order, the rotation
|
|
96089
|
+
// would land on the wrong axis (around World -Y, the depth axis,
|
|
96090
|
+
// instead of World Z, the vertical axis) because of how matrix
|
|
96091
|
+
// multiplication composes the X-tilt with the user rotation.
|
|
95936
96092
|
if (upIsZ) {
|
|
95937
|
-
scene.environmentRotation.set(Math.PI / 2, 0, rotY);
|
|
96093
|
+
scene.environmentRotation.set(Math.PI / 2, 0, rotY, "ZYX");
|
|
95938
96094
|
}
|
|
95939
96095
|
else {
|
|
95940
96096
|
scene.environmentRotation.set(0, rotY, 0);
|
|
@@ -95971,13 +96127,14 @@ float metalnessFactor = metalness;
|
|
|
95971
96127
|
break;
|
|
95972
96128
|
case "environment":
|
|
95973
96129
|
if (this._currentTexture) {
|
|
95974
|
-
//
|
|
95975
|
-
//
|
|
95976
|
-
//
|
|
95977
|
-
|
|
96130
|
+
// Use PMREM (CubeUVReflectionMapping) for the background. Raw
|
|
96131
|
+
// equirectangular HDR can't be used here: three.js's WebGLBackground
|
|
96132
|
+
// routes non-cubemap textures through a flat planeMesh path that
|
|
96133
|
+
// ignores scene.backgroundRotation, so env-rotation breaks. PMREM
|
|
96134
|
+
// takes the cubemap path and rotation works correctly.
|
|
95978
96135
|
// Always use render-to-texture with a fixed-FOV bgCamera so the
|
|
95979
96136
|
// background zoom level is identical in perspective and ortho modes.
|
|
95980
|
-
this._setupEnvBackground(scene,
|
|
96137
|
+
this._setupEnvBackground(scene, this._currentTexture, upIsZ, rotY);
|
|
95981
96138
|
this._deferredApply = null;
|
|
95982
96139
|
}
|
|
95983
96140
|
else {
|
|
@@ -96062,15 +96219,11 @@ float metalnessFactor = metalness;
|
|
|
96062
96219
|
this._lightDetectionCache.delete(slug);
|
|
96063
96220
|
logger.debug(`Evicted cached environment "${slug}" for resolution switch`);
|
|
96064
96221
|
}
|
|
96065
|
-
const cachedHdr = this._hdrCache.get(slug);
|
|
96066
|
-
if (cachedHdr) {
|
|
96067
|
-
gpuTracker.untrack("texture", cachedHdr);
|
|
96068
|
-
cachedHdr.dispose();
|
|
96069
|
-
this._hdrCache.delete(slug);
|
|
96070
|
-
}
|
|
96071
96222
|
}
|
|
96072
96223
|
// Reload the current environment at the new resolution
|
|
96073
|
-
if (currentEnvName &&
|
|
96224
|
+
if (currentEnvName &&
|
|
96225
|
+
currentEnvName !== "none" &&
|
|
96226
|
+
currentEnvName !== "studio") {
|
|
96074
96227
|
return this.loadEnvironment(currentEnvName, renderer);
|
|
96075
96228
|
}
|
|
96076
96229
|
return this._currentTexture;
|
|
@@ -96128,7 +96281,9 @@ float metalnessFactor = metalness;
|
|
|
96128
96281
|
const size = renderer.getDrawingBufferSize(_bgSizeVec);
|
|
96129
96282
|
const w = size.x;
|
|
96130
96283
|
const h = size.y;
|
|
96131
|
-
if (!this._bgRenderTarget ||
|
|
96284
|
+
if (!this._bgRenderTarget ||
|
|
96285
|
+
this._bgRenderTarget.width !== w ||
|
|
96286
|
+
this._bgRenderTarget.height !== h) {
|
|
96132
96287
|
this._bgRenderTarget?.dispose();
|
|
96133
96288
|
this._bgRenderTarget = new WebGLRenderTarget(w, h);
|
|
96134
96289
|
}
|
|
@@ -96166,7 +96321,6 @@ float metalnessFactor = metalness;
|
|
|
96166
96321
|
dispose() {
|
|
96167
96322
|
this._disposed = true;
|
|
96168
96323
|
this._currentTexture = null;
|
|
96169
|
-
this._currentBackgroundTexture = null;
|
|
96170
96324
|
this._deferredApply = null;
|
|
96171
96325
|
this._teardownEnvBackground();
|
|
96172
96326
|
this._bgScene = null;
|
|
@@ -96182,13 +96336,6 @@ float metalnessFactor = metalness;
|
|
|
96182
96336
|
logger.debug(`Disposed cached environment render target: ${key}`);
|
|
96183
96337
|
}
|
|
96184
96338
|
this._cache.clear();
|
|
96185
|
-
// Dispose all cached raw HDR textures
|
|
96186
|
-
for (const [key, hdrTexture] of this._hdrCache) {
|
|
96187
|
-
gpuTracker.untrack("texture", hdrTexture);
|
|
96188
|
-
hdrTexture.dispose();
|
|
96189
|
-
logger.debug(`Disposed cached HDR background: ${key}`);
|
|
96190
|
-
}
|
|
96191
|
-
this._hdrCache.clear();
|
|
96192
96339
|
this._lightDetectionCache.clear();
|
|
96193
96340
|
// Clear in-flight promises (they'll resolve but won't be cached)
|
|
96194
96341
|
this._inflight.clear();
|
|
@@ -96224,8 +96371,9 @@ float metalnessFactor = metalness;
|
|
|
96224
96371
|
this._bgScene.background = texture;
|
|
96225
96372
|
this._bgScene.backgroundIntensity = 1.0;
|
|
96226
96373
|
this._bgScene.backgroundBlurriness = 0;
|
|
96374
|
+
// See apply() for the "ZYX" rationale.
|
|
96227
96375
|
if (upIsZ) {
|
|
96228
|
-
this._bgScene.backgroundRotation.set(Math.PI / 2, 0, rotY);
|
|
96376
|
+
this._bgScene.backgroundRotation.set(Math.PI / 2, 0, rotY, "ZYX");
|
|
96229
96377
|
}
|
|
96230
96378
|
else {
|
|
96231
96379
|
this._bgScene.backgroundRotation.set(0, rotY, 0);
|
|
@@ -96343,10 +96491,9 @@ float metalnessFactor = metalness;
|
|
|
96343
96491
|
* Load an HDR file and generate a PMREM texture from it.
|
|
96344
96492
|
*
|
|
96345
96493
|
* Uses HDRLoader to fetch the .hdr file, then PMREMGenerator.fromEquirectangular()
|
|
96346
|
-
* to create the PMREM cubemap
|
|
96347
|
-
*
|
|
96348
|
-
*
|
|
96349
|
-
* instead of the 256² PMREM cubemap.
|
|
96494
|
+
* to create the PMREM cubemap. The source equirectangular HDR is disposed
|
|
96495
|
+
* after PMREM generation. The PMREM texture itself serves as both the IBL
|
|
96496
|
+
* environment and the background (in "environment" mode).
|
|
96350
96497
|
*
|
|
96351
96498
|
* @param url - URL of the .hdr file
|
|
96352
96499
|
* @param cacheKey - Cache key for the resulting PMREM render target
|
|
@@ -96372,19 +96519,21 @@ float metalnessFactor = metalness;
|
|
|
96372
96519
|
const renderTarget = pmremGenerator.fromEquirectangular(hdrTexture);
|
|
96373
96520
|
// Analyze HDR pixel data for dominant light sources BEFORE disposing.
|
|
96374
96521
|
// hdrTexture.image.data is Uint16Array (HalfFloatType) from HDRLoader.
|
|
96375
|
-
if (hdrTexture.image?.data &&
|
|
96522
|
+
if (hdrTexture.image?.data &&
|
|
96523
|
+
hdrTexture.image.width &&
|
|
96524
|
+
hdrTexture.image.height) {
|
|
96376
96525
|
const result = detectDominantLights(hdrTexture.image.data, hdrTexture.image.width, hdrTexture.image.height);
|
|
96377
96526
|
this._lightDetectionCache.set(cacheKey, result);
|
|
96378
96527
|
}
|
|
96379
|
-
//
|
|
96380
|
-
//
|
|
96381
|
-
//
|
|
96382
|
-
|
|
96383
|
-
//
|
|
96528
|
+
// Dispose the source equirectangular texture (PMREM is now in GPU memory).
|
|
96529
|
+
// Note: we cannot use the raw HDR for scene.background because three.js's
|
|
96530
|
+
// WebGLBackground routes non-cubemap textures through a flat planeMesh
|
|
96531
|
+
// path that ignores backgroundRotation; PMREM (CubeUVReflectionMapping)
|
|
96532
|
+
// takes the cubemap path which respects rotation.
|
|
96533
|
+
hdrTexture.dispose();
|
|
96534
|
+
// Cache render target and track its texture
|
|
96384
96535
|
this._cache.set(cacheKey, renderTarget);
|
|
96385
|
-
this._hdrCache.set(cacheKey, hdrTexture);
|
|
96386
96536
|
gpuTracker.trackTexture(renderTarget.texture, `PMREM environment: ${cacheKey}`);
|
|
96387
|
-
gpuTracker.trackTexture(hdrTexture, `HDR background: ${cacheKey}`);
|
|
96388
96537
|
logger.debug(`Loaded HDR environment from "${url}", cached as "${cacheKey}"`);
|
|
96389
96538
|
return renderTarget.texture;
|
|
96390
96539
|
}
|
|
@@ -96454,7 +96603,8 @@ float metalnessFactor = metalness;
|
|
|
96454
96603
|
*/
|
|
96455
96604
|
setShadowIntensity(intensity) {
|
|
96456
96605
|
if (this._shadowPlane) {
|
|
96457
|
-
this._shadowPlane.material.opacity =
|
|
96606
|
+
this._shadowPlane.material.opacity =
|
|
96607
|
+
intensity * 1.0;
|
|
96458
96608
|
}
|
|
96459
96609
|
}
|
|
96460
96610
|
/** Dispose all GPU resources. */
|
|
@@ -96470,7 +96620,10 @@ float metalnessFactor = metalness;
|
|
|
96470
96620
|
_createShadowPlane(zPosition, sceneSize) {
|
|
96471
96621
|
const floorSize = sceneSize * 6;
|
|
96472
96622
|
const geometry = new PlaneGeometry(floorSize, floorSize);
|
|
96473
|
-
const material = new ShadowMaterial({
|
|
96623
|
+
const material = new ShadowMaterial({
|
|
96624
|
+
opacity: 0.5,
|
|
96625
|
+
depthWrite: false,
|
|
96626
|
+
});
|
|
96474
96627
|
const plane = new Mesh(geometry, material);
|
|
96475
96628
|
plane.position.z = zPosition;
|
|
96476
96629
|
plane.receiveShadow = true;
|
|
@@ -103931,13 +104084,17 @@ void main() {
|
|
|
103931
104084
|
* Only instantiated when Studio mode is active. Non-Studio rendering
|
|
103932
104085
|
* bypasses this entirely and uses direct `renderer.render()`.
|
|
103933
104086
|
*/
|
|
103934
|
-
//
|
|
103935
|
-
//
|
|
103936
|
-
//
|
|
104087
|
+
// Tone mapping runs as a post-process effect in the EffectPass, before SMAA,
|
|
104088
|
+
// so SMAA's edge detection sees LDR luma in its calibrated [0,1] range.
|
|
104089
|
+
// Per-fragment tone mapping in the main RenderPass would be a no-op:
|
|
104090
|
+
// three.js forces NoToneMapping when rendering to a non-canvas render target
|
|
104091
|
+
// (see WebGLPrograms.getParameters), and the composer's input is a HalfFloat
|
|
104092
|
+
// HDR FBO. Exposure is read from renderer.toneMappingExposure by three.js's
|
|
104093
|
+
// shared <tonemapping_pars_fragment> chunk that ToneMappingEffect includes.
|
|
103937
104094
|
const TONE_MAP_MODE = {
|
|
103938
|
-
|
|
103939
|
-
|
|
103940
|
-
|
|
104095
|
+
neutral: ToneMappingMode.NEUTRAL,
|
|
104096
|
+
ACES: ToneMappingMode.ACES_FILMIC,
|
|
104097
|
+
none: ToneMappingMode.LINEAR,
|
|
103941
104098
|
};
|
|
103942
104099
|
// Scratch color to avoid per-frame allocation
|
|
103943
104100
|
const _savedClearColor = new Color();
|
|
@@ -103995,7 +104152,7 @@ void mainImage(const in vec4 inputColor, const in vec2 uv, const in float depth,
|
|
|
103995
104152
|
* @param width - Canvas width in pixels
|
|
103996
104153
|
* @param height - Canvas height in pixels
|
|
103997
104154
|
*/
|
|
103998
|
-
constructor(renderer, scene, camera, width, height) {
|
|
104155
|
+
constructor(renderer, scene, camera, width, height, onSmaaReady) {
|
|
103999
104156
|
/** Solid background color to protect from tone mapping, or null. */
|
|
104000
104157
|
this._bgProtectColor = null;
|
|
104001
104158
|
// Shadow mask pipeline — two separate masks to avoid depth-discontinuity halos
|
|
@@ -104016,15 +104173,17 @@ void mainImage(const in vec4 inputColor, const in vec2 uv, const in float depth,
|
|
|
104016
104173
|
this._camera = camera;
|
|
104017
104174
|
this._width = width;
|
|
104018
104175
|
this._height = height;
|
|
104019
|
-
//
|
|
104020
|
-
//
|
|
104176
|
+
// ToneMappingEffect handles the tone curve in the EffectPass. The
|
|
104177
|
+
// postprocessing library requires renderer.toneMapping = NoToneMapping
|
|
104178
|
+
// so the renderer doesn't try to apply it as well.
|
|
104021
104179
|
this._renderer.toneMapping = NoToneMapping;
|
|
104022
104180
|
// HDR pipeline with HalfFloat framebuffer.
|
|
104023
|
-
// multisampling =
|
|
104024
|
-
//
|
|
104181
|
+
// multisampling = 4: WebGL2 MSAA on the composer's input RT. Most GPUs
|
|
104182
|
+
// clamp half-float MSAA to 4 samples anyway, and Studio mode applies
|
|
104183
|
+
// additional supersampling via renderer.setPixelRatio to compensate.
|
|
104025
104184
|
this._composer = new EffectComposer(renderer, {
|
|
104026
104185
|
frameBufferType: HalfFloatType,
|
|
104027
|
-
multisampling:
|
|
104186
|
+
multisampling: 4,
|
|
104028
104187
|
});
|
|
104029
104188
|
// --- Pass 1: scene render ---
|
|
104030
104189
|
this._renderPass = new RenderPass(scene, camera);
|
|
@@ -104044,11 +104203,23 @@ void mainImage(const in vec4 inputColor, const in vec2 uv, const in float depth,
|
|
|
104044
104203
|
this._n8aoPass.enabled = false; // off by default
|
|
104045
104204
|
this._composer.addPass(this._n8aoPass);
|
|
104046
104205
|
// --- Pass 3: shadow mask compositing + tone mapping + SMAA ---
|
|
104206
|
+
// ShadowMask runs first in linear HDR (where shadow math is correct).
|
|
104207
|
+
// ToneMapping next so SMAA sees LDR luma in its calibrated [0,1] range.
|
|
104047
104208
|
this._toneMappingEffect = new ToneMappingEffect({
|
|
104048
104209
|
mode: ToneMappingMode.NEUTRAL,
|
|
104049
104210
|
});
|
|
104050
|
-
const smaaEffect = new SMAAEffect({ preset: SMAAPreset.
|
|
104051
|
-
//
|
|
104211
|
+
const smaaEffect = new SMAAEffect({ preset: SMAAPreset.ULTRA });
|
|
104212
|
+
// SMAA loads its lookup textures (search/area) asynchronously via
|
|
104213
|
+
// `new Image(); image.src = "data:..."`. Even though the source is a
|
|
104214
|
+
// data URL, the `load` event fires in a microtask — so the very first
|
|
104215
|
+
// render after composer construction has no SMAA textures attached and
|
|
104216
|
+
// produces aliased edges. We notify on `load` so the caller can trigger
|
|
104217
|
+
// another render and the user sees AA without having to interact.
|
|
104218
|
+
if (onSmaaReady) {
|
|
104219
|
+
// postprocessing's TS types only declare "change"; SMAA dispatches
|
|
104220
|
+
// "load" at runtime when the lookup textures finish decoding.
|
|
104221
|
+
smaaEffect.addEventListener("load", () => onSmaaReady());
|
|
104222
|
+
}
|
|
104052
104223
|
this._shadowMaskEffect = new ShadowMaskEffect();
|
|
104053
104224
|
this._effectPass = new EffectPass(camera, this._shadowMaskEffect, this._toneMappingEffect, smaaEffect);
|
|
104054
104225
|
this._composer.addPass(this._effectPass);
|
|
@@ -104116,6 +104287,9 @@ void mainImage(const in vec4 inputColor, const in vec2 uv, const in float depth,
|
|
|
104116
104287
|
else {
|
|
104117
104288
|
this._toneMappingEffect.mode = mapped;
|
|
104118
104289
|
}
|
|
104290
|
+
// ToneMappingEffect's GLSL reads toneMappingExposure from the
|
|
104291
|
+
// <tonemapping_pars_fragment> chunk, which three.js auto-populates
|
|
104292
|
+
// from this renderer property each frame.
|
|
104119
104293
|
this._renderer.toneMappingExposure = exposure;
|
|
104120
104294
|
}
|
|
104121
104295
|
// -----------------------------------------------------------------------
|
|
@@ -104215,7 +104389,9 @@ void mainImage(const in vec4 inputColor, const in vec2 uv, const in float depth,
|
|
|
104215
104389
|
this._composer.setSize(width, height, false);
|
|
104216
104390
|
this._n8aoPass.setSize(width, height);
|
|
104217
104391
|
// Resize shadow mask RTs at half resolution
|
|
104218
|
-
if (this._shadowMaskRT &&
|
|
104392
|
+
if (this._shadowMaskRT &&
|
|
104393
|
+
this._blurredObjectMaskRT &&
|
|
104394
|
+
this._blurredFloorMaskRT) {
|
|
104219
104395
|
const halfW = Math.max(1, Math.floor(width / 2));
|
|
104220
104396
|
const halfH = Math.max(1, Math.floor(height / 2));
|
|
104221
104397
|
this._shadowMaskRT.setSize(halfW, halfH);
|
|
@@ -104242,8 +104418,11 @@ void mainImage(const in vec4 inputColor, const in vec2 uv, const in float depth,
|
|
|
104242
104418
|
render(deltaTime) {
|
|
104243
104419
|
// Two-pass shadow mask: objects and floor are blurred separately to
|
|
104244
104420
|
// avoid depth-discontinuity halos at their boundary.
|
|
104245
|
-
if (this._shadowMaskEnabled &&
|
|
104246
|
-
|
|
104421
|
+
if (this._shadowMaskEnabled &&
|
|
104422
|
+
this._shadowMaskRT &&
|
|
104423
|
+
this._blurPass &&
|
|
104424
|
+
this._blurredObjectMaskRT &&
|
|
104425
|
+
this._blurredFloorMaskRT) {
|
|
104247
104426
|
this._renderer.shadowMap.autoUpdate = false;
|
|
104248
104427
|
this._renderer.shadowMap.needsUpdate = true;
|
|
104249
104428
|
// Pass 1: object shadow mask (floor hidden, generates shadow map)
|
|
@@ -104254,8 +104433,10 @@ void mainImage(const in vec4 inputColor, const in vec2 uv, const in float depth,
|
|
|
104254
104433
|
this._renderShadowMask("floor");
|
|
104255
104434
|
this._blurPass.render(this._renderer, this._shadowMaskRT, this._blurredFloorMaskRT);
|
|
104256
104435
|
// Feed both blurred masks to the compositing effect
|
|
104257
|
-
this._shadowMaskEffect.uniforms.get("shadowMaskObjects").value =
|
|
104258
|
-
|
|
104436
|
+
this._shadowMaskEffect.uniforms.get("shadowMaskObjects").value =
|
|
104437
|
+
this._blurredObjectMaskRT.texture;
|
|
104438
|
+
this._shadowMaskEffect.uniforms.get("shadowMaskFloor").value =
|
|
104439
|
+
this._blurredFloorMaskRT.texture;
|
|
104259
104440
|
this._renderer.shadowMap.autoUpdate = true;
|
|
104260
104441
|
}
|
|
104261
104442
|
// Hide floor during main render — blurred shadow mask provides floor shadow
|
|
@@ -104388,26 +104569,86 @@ void mainImage(const in vec4 inputColor, const in vec2 uv, const in float depth,
|
|
|
104388
104569
|
*/
|
|
104389
104570
|
const STATE_KEYS = new Set([
|
|
104390
104571
|
// Display
|
|
104391
|
-
"theme",
|
|
104392
|
-
"
|
|
104393
|
-
"
|
|
104572
|
+
"theme",
|
|
104573
|
+
"cadWidth",
|
|
104574
|
+
"treeWidth",
|
|
104575
|
+
"treeHeight",
|
|
104576
|
+
"height",
|
|
104577
|
+
"pinning",
|
|
104578
|
+
"glass",
|
|
104579
|
+
"tools",
|
|
104580
|
+
"keymap",
|
|
104581
|
+
"newTreeBehavior",
|
|
104582
|
+
"measureTools",
|
|
104583
|
+
"selectTool",
|
|
104584
|
+
"explodeTool",
|
|
104585
|
+
"zscaleTool",
|
|
104586
|
+
"zebraTool",
|
|
104587
|
+
"studioTool",
|
|
104588
|
+
"measurementDebug",
|
|
104394
104589
|
// Render
|
|
104395
|
-
"ambientIntensity",
|
|
104396
|
-
"
|
|
104590
|
+
"ambientIntensity",
|
|
104591
|
+
"directIntensity",
|
|
104592
|
+
"metalness",
|
|
104593
|
+
"roughness",
|
|
104594
|
+
"defaultOpacity",
|
|
104595
|
+
"edgeColor",
|
|
104596
|
+
"normalLen",
|
|
104397
104597
|
// Viewer
|
|
104398
|
-
"axes",
|
|
104399
|
-
"
|
|
104400
|
-
"
|
|
104401
|
-
"
|
|
104402
|
-
"
|
|
104598
|
+
"axes",
|
|
104599
|
+
"axes0",
|
|
104600
|
+
"grid",
|
|
104601
|
+
"ortho",
|
|
104602
|
+
"transparent",
|
|
104603
|
+
"blackEdges",
|
|
104604
|
+
"collapse",
|
|
104605
|
+
"clipIntersection",
|
|
104606
|
+
"clipPlaneHelpers",
|
|
104607
|
+
"clipObjectColors",
|
|
104608
|
+
"clipNormal0",
|
|
104609
|
+
"clipNormal1",
|
|
104610
|
+
"clipNormal2",
|
|
104611
|
+
"clipSlider0",
|
|
104612
|
+
"clipSlider1",
|
|
104613
|
+
"clipSlider2",
|
|
104614
|
+
"control",
|
|
104615
|
+
"holroyd",
|
|
104616
|
+
"up",
|
|
104617
|
+
"ticks",
|
|
104618
|
+
"gridFontSize",
|
|
104619
|
+
"centerGrid",
|
|
104620
|
+
"position",
|
|
104621
|
+
"quaternion",
|
|
104622
|
+
"target",
|
|
104623
|
+
"zoom",
|
|
104624
|
+
"panSpeed",
|
|
104625
|
+
"rotateSpeed",
|
|
104626
|
+
"zoomSpeed",
|
|
104627
|
+
"timeit",
|
|
104403
104628
|
// Zebra
|
|
104404
|
-
"zebraCount",
|
|
104629
|
+
"zebraCount",
|
|
104630
|
+
"zebraOpacity",
|
|
104631
|
+
"zebraDirection",
|
|
104632
|
+
"zebraColorScheme",
|
|
104633
|
+
"zebraMappingMode",
|
|
104405
104634
|
// Studio
|
|
104406
|
-
"studioEnvironment",
|
|
104407
|
-
"
|
|
104408
|
-
"
|
|
104635
|
+
"studioEnvironment",
|
|
104636
|
+
"studioEnvIntensity",
|
|
104637
|
+
"studioBackground",
|
|
104638
|
+
"studioToneMapping",
|
|
104639
|
+
"studioExposure",
|
|
104640
|
+
"studio4kEnvMaps",
|
|
104641
|
+
"studioTextureMapping",
|
|
104642
|
+
"studioEnvRotation",
|
|
104643
|
+
"studioShadowIntensity",
|
|
104644
|
+
"studioShadowSoftness",
|
|
104645
|
+
"studioAOIntensity",
|
|
104409
104646
|
// Runtime
|
|
104410
|
-
"activeTool",
|
|
104647
|
+
"activeTool",
|
|
104648
|
+
"animationMode",
|
|
104649
|
+
"animationSliderValue",
|
|
104650
|
+
"zscaleActive",
|
|
104651
|
+
"highlightedButton",
|
|
104411
104652
|
"activeTab",
|
|
104412
104653
|
]);
|
|
104413
104654
|
/**
|
|
@@ -104614,7 +104855,8 @@ void mainImage(const in vec4 inputColor, const in vec2 uv, const in float depth,
|
|
|
104614
104855
|
const value = updates[key];
|
|
104615
104856
|
// Skip undefined/null, except for keys where null is a valid value (reset to default)
|
|
104616
104857
|
const KEYS_WITH_VALID_NULL = ["position", "quaternion", "target"];
|
|
104617
|
-
if (value === undefined ||
|
|
104858
|
+
if (value === undefined ||
|
|
104859
|
+
(value === null && !KEYS_WITH_VALID_NULL.includes(key)))
|
|
104618
104860
|
continue;
|
|
104619
104861
|
const oldValue = this._state[key];
|
|
104620
104862
|
if (!valuesEqual(oldValue, value)) {
|
|
@@ -104640,8 +104882,13 @@ void mainImage(const in vec4 inputColor, const in vec2 uv, const in float depth,
|
|
|
104640
104882
|
* Converts Vector3Tuple/QuaternionTuple to THREE objects.
|
|
104641
104883
|
*/
|
|
104642
104884
|
updateViewerState(options, notify = true) {
|
|
104643
|
-
// Extract properties that need conversion to THREE objects
|
|
104644
|
-
|
|
104885
|
+
// Extract properties that need conversion to THREE objects.
|
|
104886
|
+
// `tab` is also extracted: it's not a state key directly (state uses
|
|
104887
|
+
// `activeTab`), and setting activeTab here would trigger switchToTab
|
|
104888
|
+
// before the scene is built. Viewer.render() applies it after
|
|
104889
|
+
// scene-building completes (suppressing the CAD-mode paint when a
|
|
104890
|
+
// non-default tab is the target).
|
|
104891
|
+
const { tab: _tab, clipNormal0, clipNormal1, clipNormal2, position, quaternion, target, ...rest } = options;
|
|
104645
104892
|
const converted = { ...rest };
|
|
104646
104893
|
// Convert tuple values to THREE objects
|
|
104647
104894
|
if (clipNormal0 !== undefined) {
|
|
@@ -104657,7 +104904,9 @@ void mainImage(const in vec4 inputColor, const in vec2 uv, const in float depth,
|
|
|
104657
104904
|
converted.position = position ? new Vector3(...position) : null;
|
|
104658
104905
|
}
|
|
104659
104906
|
if (quaternion !== undefined) {
|
|
104660
|
-
converted.quaternion = quaternion
|
|
104907
|
+
converted.quaternion = quaternion
|
|
104908
|
+
? new Quaternion(...quaternion)
|
|
104909
|
+
: null;
|
|
104661
104910
|
}
|
|
104662
104911
|
if (target !== undefined) {
|
|
104663
104912
|
converted.target = target ? new Vector3(...target) : null;
|
|
@@ -104781,9 +105030,15 @@ void mainImage(const in vec4 inputColor, const in vec2 uv, const in float depth,
|
|
|
104781
105030
|
// Apply transform if defined (e.g., slider 0-1000 → relative 0-1)
|
|
104782
105031
|
const transform = STATE_NOTIFICATION_TRANSFORM[key];
|
|
104783
105032
|
const notifyChange = transform
|
|
104784
|
-
? {
|
|
105033
|
+
? {
|
|
105034
|
+
old: change.old != null ? transform(change.old) : null,
|
|
105035
|
+
new: transform(change.new),
|
|
105036
|
+
}
|
|
104785
105037
|
: change;
|
|
104786
|
-
this._externalNotifyCallback({
|
|
105038
|
+
this._externalNotifyCallback({
|
|
105039
|
+
key: notificationKey,
|
|
105040
|
+
change: notifyChange,
|
|
105041
|
+
});
|
|
104787
105042
|
}
|
|
104788
105043
|
}
|
|
104789
105044
|
}
|
|
@@ -104861,12 +105116,39 @@ void mainImage(const in vec4 inputColor, const in vec2 uv, const in float depth,
|
|
|
104861
105116
|
glass: false,
|
|
104862
105117
|
tools: true,
|
|
104863
105118
|
keymap: {
|
|
104864
|
-
shift: "shiftKey",
|
|
104865
|
-
|
|
104866
|
-
|
|
104867
|
-
|
|
104868
|
-
|
|
104869
|
-
|
|
105119
|
+
shift: "shiftKey",
|
|
105120
|
+
ctrl: "ctrlKey",
|
|
105121
|
+
meta: "metaKey",
|
|
105122
|
+
alt: "altKey",
|
|
105123
|
+
axes: "a",
|
|
105124
|
+
axes0: "A",
|
|
105125
|
+
grid: "g",
|
|
105126
|
+
gridxy: "G",
|
|
105127
|
+
perspective: "p",
|
|
105128
|
+
transparent: "t",
|
|
105129
|
+
blackedges: "b",
|
|
105130
|
+
reset: "R",
|
|
105131
|
+
resize: "r",
|
|
105132
|
+
iso: "5",
|
|
105133
|
+
front: "1",
|
|
105134
|
+
rear: "3",
|
|
105135
|
+
top: "8",
|
|
105136
|
+
bottom: "2",
|
|
105137
|
+
left: "4",
|
|
105138
|
+
right: "6",
|
|
105139
|
+
explode: "x",
|
|
105140
|
+
zscale: "L",
|
|
105141
|
+
distance: "D",
|
|
105142
|
+
properties: "P",
|
|
105143
|
+
select: "S",
|
|
105144
|
+
help: "h",
|
|
105145
|
+
play: " ",
|
|
105146
|
+
stop: "Escape",
|
|
105147
|
+
tree: "T",
|
|
105148
|
+
clip: "C",
|
|
105149
|
+
material: "M",
|
|
105150
|
+
zebra: "Z",
|
|
105151
|
+
studio: "s",
|
|
104870
105152
|
},
|
|
104871
105153
|
newTreeBehavior: true,
|
|
104872
105154
|
measureTools: true,
|
|
@@ -104944,7 +105226,12 @@ void mainImage(const in vec4 inputColor, const in vec2 uv, const in float depth,
|
|
|
104944
105226
|
studioToneMapping: "neutral",
|
|
104945
105227
|
studioExposure: 1.0,
|
|
104946
105228
|
studio4kEnvMaps: false,
|
|
104947
|
-
|
|
105229
|
+
// "parametric" is the right default when the tessellator emits UVs:
|
|
105230
|
+
// each object's `nestedgroup.ts:applyTriplanarMapping` only kicks in
|
|
105231
|
+
// when the chosen mode is "triplanar" OR the geometry has no `uv`
|
|
105232
|
+
// attribute. So with the default "parametric", textured objects with
|
|
105233
|
+
// UVs use them; objects without UVs auto-fall back to triplanar.
|
|
105234
|
+
studioTextureMapping: "parametric",
|
|
104948
105235
|
studioEnvRotation: 0,
|
|
104949
105236
|
studioShadowIntensity: 0.5,
|
|
104950
105237
|
studioShadowSoftness: 0.2,
|
|
@@ -104981,6 +105268,13 @@ void mainImage(const in vec4 inputColor, const in vec2 uv, const in float depth,
|
|
|
104981
105268
|
this._active = false;
|
|
104982
105269
|
this._savedClippingState = null;
|
|
104983
105270
|
this._shadowLights = [];
|
|
105271
|
+
/**
|
|
105272
|
+
* Renderer pixel ratio saved on Studio entry, restored on leave.
|
|
105273
|
+
* Studio mode bumps the pixel ratio to apply supersampling, which
|
|
105274
|
+
* compensates for low DPR (e.g., VSCode webviews report DPR=1 even
|
|
105275
|
+
* on Retina displays) and improves AA on shallow-angle edges.
|
|
105276
|
+
*/
|
|
105277
|
+
this._savedPixelRatio = null;
|
|
104984
105278
|
// -------------------------------------------------------------------------
|
|
104985
105279
|
// Mode enter/leave
|
|
104986
105280
|
// -------------------------------------------------------------------------
|
|
@@ -105032,9 +105326,28 @@ void mainImage(const in vec4 inputColor, const in vec2 uv, const in float depth,
|
|
|
105032
105326
|
this._ctx.getDirectLight().intensity = 0;
|
|
105033
105327
|
// Floor
|
|
105034
105328
|
this._configureFloor();
|
|
105329
|
+
// Studio-only supersampling. Bump pixel ratio so the renderer draws to
|
|
105330
|
+
// a higher-resolution buffer; the browser downsamples to the canvas
|
|
105331
|
+
// display size, giving smooth AA on shallow-angle silhouettes that
|
|
105332
|
+
// MSAA alone leaves stair-stepped. Especially important in webview
|
|
105333
|
+
// hosts (e.g., VSCode) where window.devicePixelRatio is reported as 1
|
|
105334
|
+
// even on Retina displays. Restored in leaveStudioMode.
|
|
105335
|
+
this._savedPixelRatio = renderer.getPixelRatio();
|
|
105336
|
+
const targetPixelRatio = Math.max(2, window.devicePixelRatio);
|
|
105337
|
+
if (targetPixelRatio !== this._savedPixelRatio) {
|
|
105338
|
+
renderer.setPixelRatio(targetPixelRatio);
|
|
105339
|
+
renderer.setSize(state.get("cadWidth"), state.get("height"));
|
|
105340
|
+
}
|
|
105035
105341
|
// Create composer (must be before shadows)
|
|
105036
105342
|
if (!this._composer) {
|
|
105037
|
-
this._composer = new StudioComposer(renderer, scene, camera.getCamera(), state.get("cadWidth"), state.get("height"))
|
|
105343
|
+
this._composer = new StudioComposer(renderer, scene, camera.getCamera(), state.get("cadWidth"), state.get("height"), () => {
|
|
105344
|
+
// SMAA finished loading its async lookup textures. Re-render so
|
|
105345
|
+
// the first visible frame has anti-aliasing — without this the
|
|
105346
|
+
// user sees aliased edges until they interact with the scene.
|
|
105347
|
+
if (this._active && this._ctx.isRendered()) {
|
|
105348
|
+
this._ctx.update(true, false);
|
|
105349
|
+
}
|
|
105350
|
+
});
|
|
105038
105351
|
}
|
|
105039
105352
|
// Shadows (requires composer)
|
|
105040
105353
|
if (state.get("studioShadowIntensity") > 0) {
|
|
@@ -105058,6 +105371,12 @@ void mainImage(const in vec4 inputColor, const in vec2 uv, const in float depth,
|
|
|
105058
105371
|
this._composer.dispose();
|
|
105059
105372
|
this._composer = null;
|
|
105060
105373
|
}
|
|
105374
|
+
// Restore pixel ratio if we bumped it before failure
|
|
105375
|
+
if (this._savedPixelRatio !== null) {
|
|
105376
|
+
renderer.setPixelRatio(this._savedPixelRatio);
|
|
105377
|
+
renderer.setSize(state.get("cadWidth"), state.get("height"));
|
|
105378
|
+
this._savedPixelRatio = null;
|
|
105379
|
+
}
|
|
105061
105380
|
this._active = false;
|
|
105062
105381
|
logger.error("Unexpected error entering studio mode", err);
|
|
105063
105382
|
}
|
|
@@ -105074,6 +105393,12 @@ void mainImage(const in vec4 inputColor, const in vec2 uv, const in float depth,
|
|
|
105074
105393
|
this._composer.dispose();
|
|
105075
105394
|
this._composer = null;
|
|
105076
105395
|
}
|
|
105396
|
+
// Restore the renderer's pixel ratio that was bumped on Studio entry.
|
|
105397
|
+
if (this._savedPixelRatio !== null) {
|
|
105398
|
+
renderer.setPixelRatio(this._savedPixelRatio);
|
|
105399
|
+
renderer.setSize(state.get("cadWidth"), state.get("height"));
|
|
105400
|
+
this._savedPixelRatio = null;
|
|
105401
|
+
}
|
|
105077
105402
|
// 3. Remove environment, disable shadows
|
|
105078
105403
|
this.envManager.remove(this._ctx.getScene());
|
|
105079
105404
|
this._setShadowsEnabled(false);
|
|
@@ -105206,7 +105531,9 @@ void mainImage(const in vec4 inputColor, const in vec2 uv, const in float depth,
|
|
|
105206
105531
|
state.subscribe("studioEnvironment", (change) => {
|
|
105207
105532
|
if (!isActive())
|
|
105208
105533
|
return;
|
|
105209
|
-
this.envManager
|
|
105534
|
+
this.envManager
|
|
105535
|
+
.loadEnvironment(change.new, this._ctx.renderer)
|
|
105536
|
+
.then(() => {
|
|
105210
105537
|
if (!isActive())
|
|
105211
105538
|
return;
|
|
105212
105539
|
reapplyEnv();
|
|
@@ -105215,7 +105542,8 @@ void mainImage(const in vec4 inputColor, const in vec2 uv, const in float depth,
|
|
|
105215
105542
|
}
|
|
105216
105543
|
this._ctx.update(true, false);
|
|
105217
105544
|
this._ctx.dispatchEvent(new Event("tcv-studio-ready"));
|
|
105218
|
-
})
|
|
105545
|
+
})
|
|
105546
|
+
.catch((err) => {
|
|
105219
105547
|
logger.error("Unexpected error loading studio environment", err);
|
|
105220
105548
|
this._ctx.dispatchEvent(new Event("tcv-studio-ready"));
|
|
105221
105549
|
});
|
|
@@ -105306,7 +105634,9 @@ void mainImage(const in vec4 inputColor, const in vec2 uv, const in float depth,
|
|
|
105306
105634
|
if (!isActive())
|
|
105307
105635
|
return;
|
|
105308
105636
|
const envName = state.get("studioEnvironment");
|
|
105309
|
-
this.envManager
|
|
105637
|
+
this.envManager
|
|
105638
|
+
.setUse4kEnvMaps(change.new, envName, this._ctx.renderer)
|
|
105639
|
+
.then(() => {
|
|
105310
105640
|
if (!isActive())
|
|
105311
105641
|
return;
|
|
105312
105642
|
reapplyEnv();
|
|
@@ -105444,7 +105774,9 @@ void mainImage(const in vec4 inputColor, const in vec2 uv, const in float depth,
|
|
|
105444
105774
|
this.floor.setShadowsEnabled(true);
|
|
105445
105775
|
this._ctx.getScene().traverse((obj) => {
|
|
105446
105776
|
if (obj instanceof Mesh && obj.material) {
|
|
105447
|
-
const mats = Array.isArray(obj.material)
|
|
105777
|
+
const mats = Array.isArray(obj.material)
|
|
105778
|
+
? obj.material
|
|
105779
|
+
: [obj.material];
|
|
105448
105780
|
for (const m of mats) {
|
|
105449
105781
|
m.needsUpdate = true;
|
|
105450
105782
|
}
|
|
@@ -105705,7 +106037,9 @@ void mainImage(const in vec4 inputColor, const in vec2 uv, const in float depth,
|
|
|
105705
106037
|
return this._rendered;
|
|
105706
106038
|
}
|
|
105707
106039
|
/** Environment manager — proxied from StudioManager for display.ts access. */
|
|
105708
|
-
get envManager() {
|
|
106040
|
+
get envManager() {
|
|
106041
|
+
return this._studioManager.envManager;
|
|
106042
|
+
}
|
|
105709
106043
|
// ---------------------------------------------------------------------------
|
|
105710
106044
|
// Constructor & Initialization
|
|
105711
106045
|
// ---------------------------------------------------------------------------
|
|
@@ -105718,6 +106052,10 @@ void mainImage(const in vec4 inputColor, const in vec2 uv, const in float depth,
|
|
|
105718
106052
|
* @param updateMarker - enforce to redraw orientation marker after every ui activity
|
|
105719
106053
|
*/
|
|
105720
106054
|
constructor(display, options, notifyCallback, pinAsPngCallback = null, updateMarker = true) {
|
|
106055
|
+
// Grid size from the previous render, used to decide whether the new
|
|
106056
|
+
// geometry is "the same model" for clip-slider preservation.
|
|
106057
|
+
// Survives clear() so reused viewers remember the previous geometry.
|
|
106058
|
+
this._previousGridSize = 0;
|
|
105721
106059
|
// ---------------------------------------------------------------------------
|
|
105722
106060
|
// Render Loop & Scene Updates
|
|
105723
106061
|
// ---------------------------------------------------------------------------
|
|
@@ -105768,6 +106106,20 @@ void mainImage(const in vec4 inputColor, const in vec2 uv, const in float depth,
|
|
|
105768
106106
|
this.update = (updateMarker, notify = true) => {
|
|
105769
106107
|
if (!this.ready)
|
|
105770
106108
|
return;
|
|
106109
|
+
// Skip painting while Studio mode is mid-async-load: composer hasn't
|
|
106110
|
+
// been created yet, so a fall-through to renderer.render() would paint
|
|
106111
|
+
// the scene with CAD materials (Studio's material swap is also async).
|
|
106112
|
+
// Without this guard, any setter that calls update() — setCameraZoom,
|
|
106113
|
+
// setView, setExplode, setTool, etc. — would paint a CAD-materials
|
|
106114
|
+
// frame before Studio's first proper paint, which is visible as a
|
|
106115
|
+
// 0.5–1 sec CAD render before Studio takes over. Studio's tab
|
|
106116
|
+
// handler does its own update() at completion, which is when the
|
|
106117
|
+
// first painted frame should appear. State changes still propagate
|
|
106118
|
+
// synchronously and are picked up by that eventual paint.
|
|
106119
|
+
if (this.state.get("activeTab") === "studio" &&
|
|
106120
|
+
!this._studioManager.hasComposer) {
|
|
106121
|
+
return;
|
|
106122
|
+
}
|
|
105771
106123
|
if (this._externalGl) {
|
|
105772
106124
|
this.renderer.resetState();
|
|
105773
106125
|
}
|
|
@@ -105805,7 +106157,10 @@ void mainImage(const in vec4 inputColor, const in vec2 uv, const in float depth,
|
|
|
105805
106157
|
}
|
|
105806
106158
|
if (updateMarker) {
|
|
105807
106159
|
this.renderer.clearDepth(); // ensure orientation Marker is at the top
|
|
105808
|
-
this.rendered.orientationMarker.update(this.rendered.camera
|
|
106160
|
+
this.rendered.orientationMarker.update(this.rendered.camera
|
|
106161
|
+
.getPosition()
|
|
106162
|
+
.clone()
|
|
106163
|
+
.sub(this.rendered.controls.getTarget()), this.rendered.camera.getQuaternion());
|
|
105809
106164
|
this.rendered.orientationMarker.render(this.renderer);
|
|
105810
106165
|
}
|
|
105811
106166
|
if (this.animation) {
|
|
@@ -105872,6 +106227,11 @@ void mainImage(const in vec4 inputColor, const in vec2 uv, const in float depth,
|
|
|
105872
106227
|
if (!isObjectGroup(objectGroup))
|
|
105873
106228
|
continue;
|
|
105874
106229
|
objectGroup.setShapeVisible(compactTree[0] === 1);
|
|
106230
|
+
// Re-apply clip-mode back visibility when re-showing — see
|
|
106231
|
+
// matching comment in setObject().
|
|
106232
|
+
if (compactTree[0] === 1 && this.expandedNestedGroup.backVisible) {
|
|
106233
|
+
objectGroup.setBackVisible(true);
|
|
106234
|
+
}
|
|
105875
106235
|
objectGroup.setEdgesVisible(compactTree[1] === 1);
|
|
105876
106236
|
// Sync state (unless disabled = 3)
|
|
105877
106237
|
if (leafState[0] !== 3)
|
|
@@ -105903,6 +106263,11 @@ void mainImage(const in vec4 inputColor, const in vec2 uv, const in float depth,
|
|
|
105903
106263
|
}
|
|
105904
106264
|
}
|
|
105905
106265
|
objectGroup.setShapeVisible(shapeVisible);
|
|
106266
|
+
// Re-apply clip-mode back visibility when re-showing — see
|
|
106267
|
+
// matching comment in setObject().
|
|
106268
|
+
if (shapeVisible && this.compactNestedGroup.backVisible) {
|
|
106269
|
+
objectGroup.setBackVisible(true);
|
|
106270
|
+
}
|
|
105906
106271
|
objectGroup.setEdgesVisible(edgeVisible);
|
|
105907
106272
|
// Sync compact state (unless disabled = 3)
|
|
105908
106273
|
if (compactTree[0] !== 3)
|
|
@@ -106044,6 +106409,14 @@ void mainImage(const in vec4 inputColor, const in vec2 uv, const in float depth,
|
|
|
106044
106409
|
if (objectGroup != null && objectGroup instanceof ObjectGroup) {
|
|
106045
106410
|
if (iconNumber === 0) {
|
|
106046
106411
|
objectGroup.setShapeVisible(state === 1);
|
|
106412
|
+
// When re-showing while clip-tab is active, re-apply the clip-mode
|
|
106413
|
+
// back-visibility for this object. setShapeVisible's show-path
|
|
106414
|
+
// doesn't touch back when !renderback (clip-tab owns it), so a
|
|
106415
|
+
// previously-hidden object would otherwise come back with front
|
|
106416
|
+
// visible but back still hidden — looking hollow under clipping.
|
|
106417
|
+
if (state === 1 && this.rendered.nestedGroup.backVisible) {
|
|
106418
|
+
objectGroup.setBackVisible(true);
|
|
106419
|
+
}
|
|
106047
106420
|
}
|
|
106048
106421
|
else {
|
|
106049
106422
|
objectGroup.setEdgesVisible(state === 1);
|
|
@@ -107281,7 +107654,10 @@ void mainImage(const in vec4 inputColor, const in vec2 uv, const in float depth,
|
|
|
107281
107654
|
this.hasAnimationLoop = false;
|
|
107282
107655
|
this.display = display;
|
|
107283
107656
|
if (options.keymap) {
|
|
107284
|
-
this.setKeyMap({
|
|
107657
|
+
this.setKeyMap({
|
|
107658
|
+
...ViewerState.DISPLAY_DEFAULTS.keymap,
|
|
107659
|
+
...options.keymap,
|
|
107660
|
+
});
|
|
107285
107661
|
}
|
|
107286
107662
|
else {
|
|
107287
107663
|
this.setKeyMap(ViewerState.DISPLAY_DEFAULTS.keymap);
|
|
@@ -107602,7 +107978,8 @@ void mainImage(const in vec4 inputColor, const in vec2 uv, const in float depth,
|
|
|
107602
107978
|
this.renderer.renderLists.dispose();
|
|
107603
107979
|
this.renderer.dispose();
|
|
107604
107980
|
// Skip context loss for externally provided WebGL contexts
|
|
107605
|
-
if (!this._externalGl &&
|
|
107981
|
+
if (!this._externalGl &&
|
|
107982
|
+
typeof this.renderer.forceContextLoss === "function") {
|
|
107606
107983
|
this.renderer.forceContextLoss();
|
|
107607
107984
|
}
|
|
107608
107985
|
console.debug("three-cad-viewer: WebGL context disposed");
|
|
@@ -107705,6 +108082,19 @@ void mainImage(const in vec4 inputColor, const in vec2 uv, const in float depth,
|
|
|
107705
108082
|
deepDispose(this.compactNestedGroup);
|
|
107706
108083
|
this.compactNestedGroup = null;
|
|
107707
108084
|
}
|
|
108085
|
+
// Reset scene-derived fields so the next render() recomputes them
|
|
108086
|
+
// from the new geometry. Without this, reuse (clear() + render())
|
|
108087
|
+
// re-uses stale values from the previous scene, producing wrong
|
|
108088
|
+
// camera framing and stale bookkeeping.
|
|
108089
|
+
this.bbox = null;
|
|
108090
|
+
this.bb_max = 0;
|
|
108091
|
+
this.bb_radius = 0;
|
|
108092
|
+
this.lastBbox = null;
|
|
108093
|
+
this.materialSettings = null;
|
|
108094
|
+
this.renderOptions = null;
|
|
108095
|
+
this.tree = null;
|
|
108096
|
+
this.compactTree = null;
|
|
108097
|
+
this.expandedTree = null;
|
|
107708
108098
|
}
|
|
107709
108099
|
/**
|
|
107710
108100
|
* Build nestedGroup and treeview for initial render.
|
|
@@ -108003,16 +108393,28 @@ void mainImage(const in vec4 inputColor, const in vec2 uv, const in float depth,
|
|
|
108003
108393
|
this.setDirectLight(this.state.get("directIntensity"));
|
|
108004
108394
|
this.display.setSliderLimits(this.gridSize / 2);
|
|
108005
108395
|
this.display.syncClipSlidersFromState();
|
|
108006
|
-
// Compute clip slider values (used later after ready=true)
|
|
108007
|
-
|
|
108008
|
-
|
|
108009
|
-
|
|
108010
|
-
|
|
108011
|
-
|
|
108012
|
-
|
|
108013
|
-
|
|
108014
|
-
|
|
108015
|
-
|
|
108396
|
+
// Compute clip slider values (used later after ready=true).
|
|
108397
|
+
//
|
|
108398
|
+
// Three-tier policy:
|
|
108399
|
+
// 1. Caller passed a value (viewerOptions.clipSliderN != null) → use
|
|
108400
|
+
// it. Caller intent always wins.
|
|
108401
|
+
// 2. Same geometry as last render (gridSize unchanged) AND state has
|
|
108402
|
+
// a real value (≠ -1, the default sentinel) → reuse state. This
|
|
108403
|
+
// preserves the user's slider drag when re-rendering the same
|
|
108404
|
+
// model.
|
|
108405
|
+
// 3. New geometry (or first render) → default to gridSize/2.
|
|
108406
|
+
const gridSizeChanged = this._previousGridSize !== this.gridSize;
|
|
108407
|
+
this._previousGridSize = this.gridSize;
|
|
108408
|
+
const resolveSlider = (passed, stateValue) => {
|
|
108409
|
+
if (passed != null)
|
|
108410
|
+
return passed;
|
|
108411
|
+
if (!gridSizeChanged && stateValue !== -1)
|
|
108412
|
+
return stateValue;
|
|
108413
|
+
return this.gridSize / 2;
|
|
108414
|
+
};
|
|
108415
|
+
const clipSlider0 = resolveSlider(viewerOptions.clipSlider0, this.state.get("clipSlider0"));
|
|
108416
|
+
const clipSlider1 = resolveSlider(viewerOptions.clipSlider1, this.state.get("clipSlider1"));
|
|
108417
|
+
const clipSlider2 = resolveSlider(viewerOptions.clipSlider2, this.state.get("clipSlider2"));
|
|
108016
108418
|
nestedGroup.setClipPlanes(clipping.clipPlanes);
|
|
108017
108419
|
this.setLocalClipping(false); // only allow clipping when Clipping tab is selected
|
|
108018
108420
|
clipping.setVisible(false);
|
|
@@ -108033,15 +108435,29 @@ void mainImage(const in vec4 inputColor, const in vec2 uv, const in float depth,
|
|
|
108033
108435
|
this.display.showToolsPanel(false);
|
|
108034
108436
|
this.rendered.orientationMarker.setVisible(false);
|
|
108035
108437
|
}
|
|
108036
|
-
// Apply clip settings AFTER ready=true (clip setters check this.ready)
|
|
108037
|
-
//
|
|
108038
|
-
|
|
108039
|
-
|
|
108040
|
-
|
|
108041
|
-
//
|
|
108042
|
-
|
|
108043
|
-
|
|
108044
|
-
|
|
108438
|
+
// Apply clip settings AFTER ready=true (clip setters check this.ready).
|
|
108439
|
+
//
|
|
108440
|
+
// Same three-tier policy as clipSlider above (caller wins → reuse state
|
|
108441
|
+
// on same geometry → reset on new geometry). The default normals are
|
|
108442
|
+
// the axis-aligned planes that match the Clipping subsystem's own
|
|
108443
|
+
// DEFAULT_NORMALS.
|
|
108444
|
+
//
|
|
108445
|
+
// Always passing a non-null normal means setClipNormal also handles the
|
|
108446
|
+
// slider write (it calls setClipSlider internally), so no separate
|
|
108447
|
+
// setClipSlider follow-up is needed here.
|
|
108448
|
+
const resolveNormal = (passed, stateValue, defaultTuple) => {
|
|
108449
|
+
if (passed != null)
|
|
108450
|
+
return passed;
|
|
108451
|
+
if (!gridSizeChanged)
|
|
108452
|
+
return [stateValue.x, stateValue.y, stateValue.z];
|
|
108453
|
+
return defaultTuple;
|
|
108454
|
+
};
|
|
108455
|
+
const clipNormal0 = resolveNormal(viewerOptions.clipNormal0, this.state.get("clipNormal0"), [-1, 0, 0]);
|
|
108456
|
+
const clipNormal1 = resolveNormal(viewerOptions.clipNormal1, this.state.get("clipNormal1"), [0, -1, 0]);
|
|
108457
|
+
const clipNormal2 = resolveNormal(viewerOptions.clipNormal2, this.state.get("clipNormal2"), [0, 0, -1]);
|
|
108458
|
+
this.setClipNormal(0, clipNormal0, clipSlider0, true);
|
|
108459
|
+
this.setClipNormal(1, clipNormal1, clipSlider1, true);
|
|
108460
|
+
this.setClipNormal(2, clipNormal2, clipSlider2, true);
|
|
108045
108461
|
this.setClipIntersection(viewerOptions.clipIntersection ?? false, true);
|
|
108046
108462
|
this.setClipObjectColorCaps(viewerOptions.clipObjectColors ?? false, true);
|
|
108047
108463
|
this.setClipPlaneHelpers(viewerOptions.clipPlaneHelpers ?? false, true);
|
|
@@ -108053,15 +108469,38 @@ void mainImage(const in vec4 inputColor, const in vec2 uv, const in float depth,
|
|
|
108053
108469
|
// Computed values from controls/camera
|
|
108054
108470
|
target: { old: null, new: toVector3Tuple(controls.target.toArray()) },
|
|
108055
108471
|
target0: { old: null, new: toVector3Tuple(controls.target0.toArray()) },
|
|
108056
|
-
position: {
|
|
108057
|
-
|
|
108472
|
+
position: {
|
|
108473
|
+
old: null,
|
|
108474
|
+
new: this.rendered.camera.getPosition().toArray(),
|
|
108475
|
+
},
|
|
108476
|
+
quaternion: {
|
|
108477
|
+
old: null,
|
|
108478
|
+
new: this.rendered.camera.getQuaternion().toArray(),
|
|
108479
|
+
},
|
|
108058
108480
|
zoom: { old: null, new: this.rendered.camera.getZoom() },
|
|
108059
108481
|
// All config values from state
|
|
108060
108482
|
...this.state.getAllNotifiable(),
|
|
108061
108483
|
});
|
|
108062
108484
|
}
|
|
108063
108485
|
timer.split("notification done");
|
|
108064
|
-
|
|
108486
|
+
// Initial paint and tab-landing logic.
|
|
108487
|
+
//
|
|
108488
|
+
// viewerOptions.tab can request a non-tree tab as the landing
|
|
108489
|
+
// tab. To avoid a CAD-mode → target-tab flicker, we skip the default
|
|
108490
|
+
// CAD update() in that case and let the activeTab subscription's
|
|
108491
|
+
// switchToTab handler paint the right content (or, for studio, show
|
|
108492
|
+
// the spinner over a blank canvas while async setup runs).
|
|
108493
|
+
const targetTab = viewerOptions.tab ?? "tree";
|
|
108494
|
+
if (targetTab === "tree") {
|
|
108495
|
+
this.update(true, false);
|
|
108496
|
+
}
|
|
108497
|
+
else {
|
|
108498
|
+
// setActiveTab fires the subscription synchronously; switchToTab
|
|
108499
|
+
// either paints (clip / zebra / material) or initiates Studio's
|
|
108500
|
+
// async load (showing the spinner). The first painted frame the
|
|
108501
|
+
// user sees is the target tab, not CAD.
|
|
108502
|
+
this.setActiveTab(targetTab);
|
|
108503
|
+
}
|
|
108065
108504
|
treeview.update();
|
|
108066
108505
|
this.display.setTheme(this.state.get("theme"));
|
|
108067
108506
|
this.setZebraCount(this.state.get("zebraCount"));
|
|
@@ -108462,7 +108901,9 @@ void mainImage(const in vec4 inputColor, const in vec2 uv, const in float depth,
|
|
|
108462
108901
|
// Store current state
|
|
108463
108902
|
const camera = this.rendered.camera.getCamera();
|
|
108464
108903
|
const zoom = camera.zoom; // For orthographic cameras
|
|
108465
|
-
const offset = camera.position
|
|
108904
|
+
const offset = camera.position
|
|
108905
|
+
.clone()
|
|
108906
|
+
.sub(this.rendered.controls.getTarget());
|
|
108466
108907
|
// Update position and target
|
|
108467
108908
|
camera.position.copy(targetVec.clone().add(offset));
|
|
108468
108909
|
camera.updateWorldMatrix(true, false);
|
|
@@ -108697,7 +109138,10 @@ void mainImage(const in vec4 inputColor, const in vec2 uv, const in float depth,
|
|
|
108697
109138
|
version: partData.version,
|
|
108698
109139
|
id: wrapperId,
|
|
108699
109140
|
name: "__addPart_tmp__",
|
|
108700
|
-
loc: [
|
|
109141
|
+
loc: [
|
|
109142
|
+
[0, 0, 0],
|
|
109143
|
+
[0, 0, 0, 1],
|
|
109144
|
+
],
|
|
108701
109145
|
parts: [partData],
|
|
108702
109146
|
};
|
|
108703
109147
|
const wrapperGroup = nestedGroup.renderLoop(wrapper);
|
|
@@ -108877,8 +109321,7 @@ void mainImage(const in vec4 inputColor, const in vec2 uv, const in float depth,
|
|
|
108877
109321
|
else {
|
|
108878
109322
|
const edgePosAttr = group.edges.geometry.getAttribute("position");
|
|
108879
109323
|
sameEdges =
|
|
108880
|
-
edgePosAttr != null &&
|
|
108881
|
-
edgePosAttr.count === flatLen(shape.edges) / 3;
|
|
109324
|
+
edgePosAttr != null && edgePosAttr.count === flatLen(shape.edges) / 3;
|
|
108882
109325
|
}
|
|
108883
109326
|
}
|
|
108884
109327
|
if (!sameVertices || !sameTriangles || !sameEdges) {
|
|
@@ -109009,7 +109452,8 @@ void mainImage(const in vec4 inputColor, const in vec2 uv, const in float depth,
|
|
|
109009
109452
|
// Only rebuild stencils if geometry grew beyond the region that stencils
|
|
109010
109453
|
// were last built for. Shrinking geometry still fits within existing
|
|
109011
109454
|
// stencils, so skip the expensive rebuild in that case.
|
|
109012
|
-
const newCSize = 1.1 *
|
|
109455
|
+
const newCSize = 1.1 *
|
|
109456
|
+
Math.max(Math.abs(this.bbox.min.length()), Math.abs(this.bbox.max.length()));
|
|
109013
109457
|
if (newCSize > this._stencilCSize + 1e-6) {
|
|
109014
109458
|
this._stencilCSize = newCSize;
|
|
109015
109459
|
const clipping = this.rendered.clipping;
|
|
@@ -109055,9 +109499,7 @@ void mainImage(const in vec4 inputColor, const in vec2 uv, const in float depth,
|
|
|
109055
109499
|
}
|
|
109056
109500
|
const min = new Vector3(bb.xmin, bb.ymin, bb.zmin);
|
|
109057
109501
|
const max = new Vector3(bb.xmax, bb.ymax, bb.zmax);
|
|
109058
|
-
const center = new Vector3()
|
|
109059
|
-
.addVectors(min, max)
|
|
109060
|
-
.multiplyScalar(0.5);
|
|
109502
|
+
const center = new Vector3().addVectors(min, max).multiplyScalar(0.5);
|
|
109061
109503
|
const requiredCSize = 1.1 * Math.max(Math.abs(min.length()), Math.abs(max.length()));
|
|
109062
109504
|
if (requiredCSize > this._stencilCSize + 1e-6) {
|
|
109063
109505
|
this._stencilCSize = requiredCSize;
|
|
@@ -109284,7 +109726,8 @@ void mainImage(const in vec4 inputColor, const in vec2 uv, const in float depth,
|
|
|
109284
109726
|
if (value === undefined)
|
|
109285
109727
|
continue;
|
|
109286
109728
|
if (modifierKeys.has(key)) {
|
|
109287
|
-
modifiers[key] =
|
|
109729
|
+
modifiers[key] =
|
|
109730
|
+
value;
|
|
109288
109731
|
}
|
|
109289
109732
|
else {
|
|
109290
109733
|
actions[key] = value;
|
|
@@ -109340,6 +109783,7 @@ void mainImage(const in vec4 inputColor, const in vec2 uv, const in float depth,
|
|
|
109340
109783
|
*/
|
|
109341
109784
|
resizeCadView(cadWidth, treeWidth, height, glass = false) {
|
|
109342
109785
|
this.state.set("cadWidth", cadWidth);
|
|
109786
|
+
this.state.set("treeWidth", treeWidth);
|
|
109343
109787
|
this.state.set("height", height);
|
|
109344
109788
|
// Adapt renderer dimensions
|
|
109345
109789
|
this.renderer.setSize(cadWidth, height);
|
|
@@ -109397,7 +109841,9 @@ void mainImage(const in vec4 inputColor, const in vec2 uv, const in float depth,
|
|
|
109397
109841
|
* @param notify - Whether to trigger the notification
|
|
109398
109842
|
*/
|
|
109399
109843
|
this._notify = (value, notify = true) => {
|
|
109400
|
-
if (this.type == "plane" &&
|
|
109844
|
+
if (this.type == "plane" &&
|
|
109845
|
+
this.notifyCallback &&
|
|
109846
|
+
this.index !== undefined) {
|
|
109401
109847
|
const change = {};
|
|
109402
109848
|
change[`clip_slider_${this.index - 1}`] = parseFloat(String(value));
|
|
109403
109849
|
this.notifyCallback(change, notify);
|
|
@@ -109441,7 +109887,8 @@ void mainImage(const in vec4 inputColor, const in vec2 uv, const in float depth,
|
|
|
109441
109887
|
this.onSetSlider = options.onSetSlider || null;
|
|
109442
109888
|
const sliderEl = container.getElementsByClassName(`tcv_sld_value_${index}`)[0];
|
|
109443
109889
|
const inputEl = container.getElementsByClassName(`tcv_inp_value_${index}`)[0];
|
|
109444
|
-
if (!(sliderEl instanceof HTMLInputElement) ||
|
|
109890
|
+
if (!(sliderEl instanceof HTMLInputElement) ||
|
|
109891
|
+
!(inputEl instanceof HTMLInputElement)) {
|
|
109445
109892
|
throw new Error(`Slider elements not found for index "${index}" in container`);
|
|
109446
109893
|
}
|
|
109447
109894
|
this.slider = sliderEl;
|
|
@@ -109949,20 +110396,112 @@ void mainImage(const in vec4 inputColor, const in vec2 uv, const in float depth,
|
|
|
109949
110396
|
return `${val}px`;
|
|
109950
110397
|
}
|
|
109951
110398
|
const MAT_EDITOR_PARAMS = [
|
|
109952
|
-
{
|
|
109953
|
-
|
|
109954
|
-
|
|
109955
|
-
|
|
109956
|
-
|
|
109957
|
-
|
|
109958
|
-
|
|
109959
|
-
|
|
110399
|
+
{
|
|
110400
|
+
key: "metalness",
|
|
110401
|
+
label: "Metallic",
|
|
110402
|
+
min: 0,
|
|
110403
|
+
max: 1,
|
|
110404
|
+
step: 0.01,
|
|
110405
|
+
group: "PBR Core",
|
|
110406
|
+
},
|
|
110407
|
+
{
|
|
110408
|
+
key: "roughness",
|
|
110409
|
+
label: "Roughness",
|
|
110410
|
+
min: 0,
|
|
110411
|
+
max: 1,
|
|
110412
|
+
step: 0.01,
|
|
110413
|
+
group: "PBR Core",
|
|
110414
|
+
},
|
|
110415
|
+
{
|
|
110416
|
+
key: "clearcoat",
|
|
110417
|
+
label: "Clearcoat",
|
|
110418
|
+
min: 0,
|
|
110419
|
+
max: 1,
|
|
110420
|
+
step: 0.01,
|
|
110421
|
+
group: "Clearcoat",
|
|
110422
|
+
},
|
|
110423
|
+
{
|
|
110424
|
+
key: "clearcoatRoughness",
|
|
110425
|
+
label: "Clearcoat Rough.",
|
|
110426
|
+
min: 0,
|
|
110427
|
+
max: 1,
|
|
110428
|
+
step: 0.01,
|
|
110429
|
+
group: "Clearcoat",
|
|
110430
|
+
},
|
|
110431
|
+
{
|
|
110432
|
+
key: "transmission",
|
|
110433
|
+
label: "Transmission",
|
|
110434
|
+
min: 0,
|
|
110435
|
+
max: 1,
|
|
110436
|
+
step: 0.01,
|
|
110437
|
+
group: "Transmission",
|
|
110438
|
+
},
|
|
110439
|
+
{
|
|
110440
|
+
key: "ior",
|
|
110441
|
+
label: "IOR",
|
|
110442
|
+
min: 1.0,
|
|
110443
|
+
max: 2.5,
|
|
110444
|
+
step: 0.01,
|
|
110445
|
+
group: "Transmission",
|
|
110446
|
+
},
|
|
110447
|
+
{
|
|
110448
|
+
key: "thickness",
|
|
110449
|
+
label: "Thickness",
|
|
110450
|
+
min: 0,
|
|
110451
|
+
max: 10,
|
|
110452
|
+
step: 0.1,
|
|
110453
|
+
group: "Transmission",
|
|
110454
|
+
},
|
|
110455
|
+
{
|
|
110456
|
+
key: "attenuationDistance",
|
|
110457
|
+
label: "Atten. Distance",
|
|
110458
|
+
min: 0,
|
|
110459
|
+
max: 100,
|
|
110460
|
+
step: 0.5,
|
|
110461
|
+
group: "Transmission",
|
|
110462
|
+
infinity: true,
|
|
110463
|
+
},
|
|
109960
110464
|
{ key: "sheen", label: "Sheen", min: 0, max: 1, step: 0.01, group: "Sheen" },
|
|
109961
|
-
{
|
|
109962
|
-
|
|
109963
|
-
|
|
109964
|
-
|
|
109965
|
-
|
|
110465
|
+
{
|
|
110466
|
+
key: "sheenRoughness",
|
|
110467
|
+
label: "Sheen Roughness",
|
|
110468
|
+
min: 0,
|
|
110469
|
+
max: 1,
|
|
110470
|
+
step: 0.01,
|
|
110471
|
+
group: "Sheen",
|
|
110472
|
+
},
|
|
110473
|
+
{
|
|
110474
|
+
key: "specularIntensity",
|
|
110475
|
+
label: "Specular Intensity",
|
|
110476
|
+
min: 0,
|
|
110477
|
+
max: 2,
|
|
110478
|
+
step: 0.01,
|
|
110479
|
+
group: "Specular",
|
|
110480
|
+
},
|
|
110481
|
+
{
|
|
110482
|
+
key: "anisotropy",
|
|
110483
|
+
label: "Anisotropy",
|
|
110484
|
+
min: 0,
|
|
110485
|
+
max: 1,
|
|
110486
|
+
step: 0.01,
|
|
110487
|
+
group: "Anisotropy",
|
|
110488
|
+
},
|
|
110489
|
+
{
|
|
110490
|
+
key: "anisotropyRotation",
|
|
110491
|
+
label: "Anisotropy Rotation",
|
|
110492
|
+
min: 0,
|
|
110493
|
+
max: 6.28,
|
|
110494
|
+
step: 0.01,
|
|
110495
|
+
group: "Anisotropy",
|
|
110496
|
+
},
|
|
110497
|
+
{
|
|
110498
|
+
key: "emissiveIntensity",
|
|
110499
|
+
label: "Emissive Intensity",
|
|
110500
|
+
min: 0,
|
|
110501
|
+
max: 5,
|
|
110502
|
+
step: 0.1,
|
|
110503
|
+
group: "Emissive",
|
|
110504
|
+
},
|
|
109966
110505
|
];
|
|
109967
110506
|
function _formatMatValue(value, step) {
|
|
109968
110507
|
const decimals = step < 0.1 ? 2 : 1;
|
|
@@ -110411,7 +110950,13 @@ void mainImage(const in vec4 inputColor, const in vec2 uv, const in float depth,
|
|
|
110411
110950
|
if (!(e.target instanceof HTMLSelectElement))
|
|
110412
110951
|
return;
|
|
110413
110952
|
const value = e.target.value;
|
|
110414
|
-
if (value === "grey" ||
|
|
110953
|
+
if (value === "grey" ||
|
|
110954
|
+
value === "darkgrey" ||
|
|
110955
|
+
value === "white" ||
|
|
110956
|
+
value === "gradient" ||
|
|
110957
|
+
value === "gradient-dark" ||
|
|
110958
|
+
value === "environment" ||
|
|
110959
|
+
value === "transparent") {
|
|
110415
110960
|
this.state.set("studioBackground", value);
|
|
110416
110961
|
}
|
|
110417
110962
|
};
|
|
@@ -110629,7 +111174,8 @@ void mainImage(const in vec4 inputColor, const in vec2 uv, const in float depth,
|
|
|
110629
111174
|
// Skip if target is a text-entry input element (but allow buttons/checkboxes)
|
|
110630
111175
|
const target = e.target;
|
|
110631
111176
|
if ((target instanceof HTMLInputElement &&
|
|
110632
|
-
target.type !== "button" &&
|
|
111177
|
+
target.type !== "button" &&
|
|
111178
|
+
target.type !== "checkbox") ||
|
|
110633
111179
|
target instanceof HTMLTextAreaElement ||
|
|
110634
111180
|
target instanceof HTMLSelectElement) {
|
|
110635
111181
|
return;
|
|
@@ -110773,7 +111319,7 @@ void mainImage(const in vec4 inputColor, const in vec2 uv, const in float depth,
|
|
|
110773
111319
|
this._spinnerEl = this.container.querySelector(".tcv_studio_spinner");
|
|
110774
111320
|
this._warningBannerEl = this.container.querySelector(".tcv_warning_banner");
|
|
110775
111321
|
this.container.addEventListener("tcv-material-warnings", ((e) => {
|
|
110776
|
-
this._showWarningBanner(`Unresolved material tag(s): ${e.detail.map(t => `"${t}"`).join(", ")}`);
|
|
111322
|
+
this._showWarningBanner(`Unresolved material tag(s): ${e.detail.map((t) => `"${t}"`).join(", ")}`);
|
|
110777
111323
|
}));
|
|
110778
111324
|
this.tabTree = this.getElement("tcv_tab_tree");
|
|
110779
111325
|
this.tabClip = this.getElement("tcv_tab_clip");
|
|
@@ -111591,7 +112137,8 @@ void mainImage(const in vec4 inputColor, const in vec2 uv, const in float depth,
|
|
|
111591
112137
|
attachCanvas(canvasElement) {
|
|
111592
112138
|
// If the canvas is already attached elsewhere
|
|
111593
112139
|
// do not re-parent it into this display.
|
|
111594
|
-
if (canvasElement.parentElement &&
|
|
112140
|
+
if (canvasElement.parentElement &&
|
|
112141
|
+
canvasElement.parentElement !== this.cadView) {
|
|
111595
112142
|
listeners.add(canvasElement, "click", () => {
|
|
111596
112143
|
if (this.help_shown) {
|
|
111597
112144
|
this.showHelp(false);
|
|
@@ -111642,7 +112189,8 @@ void mainImage(const in vec4 inputColor, const in vec2 uv, const in float depth,
|
|
|
111642
112189
|
_deactivateToolsForStudio() {
|
|
111643
112190
|
// If a tool is currently active, deactivate it cleanly
|
|
111644
112191
|
const activeTool = this.state.get("activeTool");
|
|
111645
|
-
if (activeTool &&
|
|
112192
|
+
if (activeTool &&
|
|
112193
|
+
["distance", "properties", "angle", "select"].includes(activeTool)) {
|
|
111646
112194
|
this.clickButtons[activeTool]?.set(false);
|
|
111647
112195
|
this.setTool(activeTool, false);
|
|
111648
112196
|
// setTool→toggleTab(false) silently sets activeTab to "tree" (no notification).
|
|
@@ -111750,7 +112298,13 @@ void mainImage(const in vec4 inputColor, const in vec2 uv, const in float depth,
|
|
|
111750
112298
|
});
|
|
111751
112299
|
}
|
|
111752
112300
|
// Update tab styling
|
|
111753
|
-
[
|
|
112301
|
+
[
|
|
112302
|
+
this.tabTree,
|
|
112303
|
+
this.tabClip,
|
|
112304
|
+
this.tabZebra,
|
|
112305
|
+
this.tabMaterial,
|
|
112306
|
+
this.tabStudio,
|
|
112307
|
+
].forEach((tabEl) => {
|
|
111754
112308
|
tabEl.classList.add("tcv_tab-unselected");
|
|
111755
112309
|
tabEl.classList.remove("tcv_tab-selected");
|
|
111756
112310
|
});
|
|
@@ -111847,11 +112401,15 @@ void mainImage(const in vec4 inputColor, const in vec2 uv, const in float depth,
|
|
|
111847
112401
|
originalMat = currentMat;
|
|
111848
112402
|
mat = currentMat.clone();
|
|
111849
112403
|
// Preserve triplanar mapping if the original material uses it
|
|
111850
|
-
if (currentMat.customProgramCacheKey() === "triplanar" &&
|
|
112404
|
+
if (currentMat.customProgramCacheKey() === "triplanar" &&
|
|
112405
|
+
object.shapeGeometry) {
|
|
111851
112406
|
applyTriplanarMapping(mat, object.shapeGeometry);
|
|
111852
112407
|
}
|
|
111853
112408
|
object.front.material = mat;
|
|
111854
|
-
this._matEditorClones.set(objectPath, {
|
|
112409
|
+
this._matEditorClones.set(objectPath, {
|
|
112410
|
+
original: originalMat,
|
|
112411
|
+
clone: mat,
|
|
112412
|
+
});
|
|
111855
112413
|
}
|
|
111856
112414
|
// Restore elements that _showMatEditorHint may have hidden
|
|
111857
112415
|
const resetBtn = dialog.querySelector(".tcv_mat_editor_reset");
|
|
@@ -111902,7 +112460,9 @@ void mainImage(const in vec4 inputColor, const in vec2 uv, const in float depth,
|
|
|
111902
112460
|
try {
|
|
111903
112461
|
groups = this.viewer.rendered.nestedGroup.groups;
|
|
111904
112462
|
}
|
|
111905
|
-
catch {
|
|
112463
|
+
catch {
|
|
112464
|
+
/* not rendered */
|
|
112465
|
+
}
|
|
111906
112466
|
for (const [path, { original, clone }] of this._matEditorClones.entries()) {
|
|
111907
112467
|
// Restore original material on mesh before disposing the clone
|
|
111908
112468
|
if (groups) {
|
|
@@ -111964,7 +112524,8 @@ void mainImage(const in vec4 inputColor, const in vec2 uv, const in float depth,
|
|
|
111964
112524
|
if (!(currentMat instanceof MeshPhysicalMaterial))
|
|
111965
112525
|
continue;
|
|
111966
112526
|
const clone = currentMat.clone();
|
|
111967
|
-
if (currentMat.customProgramCacheKey() === "triplanar" &&
|
|
112527
|
+
if (currentMat.customProgramCacheKey() === "triplanar" &&
|
|
112528
|
+
group.shapeGeometry) {
|
|
111968
112529
|
applyTriplanarMapping(clone, group.shapeGeometry);
|
|
111969
112530
|
}
|
|
111970
112531
|
for (const [key, value] of Object.entries(changes)) {
|
|
@@ -111972,7 +112533,10 @@ void mainImage(const in vec4 inputColor, const in vec2 uv, const in float depth,
|
|
|
111972
112533
|
clone[key] = value;
|
|
111973
112534
|
}
|
|
111974
112535
|
group.front.material = clone;
|
|
111975
|
-
this._matEditorClones.set(path, {
|
|
112536
|
+
this._matEditorClones.set(path, {
|
|
112537
|
+
original: currentMat,
|
|
112538
|
+
clone,
|
|
112539
|
+
});
|
|
111976
112540
|
}
|
|
111977
112541
|
this._savedMatEditorChanges.clear();
|
|
111978
112542
|
}
|
|
@@ -112015,7 +112579,10 @@ void mainImage(const in vec4 inputColor, const in vec2 uv, const in float depth,
|
|
|
112015
112579
|
startX = e.clientX;
|
|
112016
112580
|
startY = e.clientY;
|
|
112017
112581
|
const rect = dialog.getBoundingClientRect();
|
|
112018
|
-
const parentRect = dialog.offsetParent?.getBoundingClientRect() ?? {
|
|
112582
|
+
const parentRect = dialog.offsetParent?.getBoundingClientRect() ?? {
|
|
112583
|
+
left: 0,
|
|
112584
|
+
top: 0,
|
|
112585
|
+
};
|
|
112019
112586
|
origLeft = rect.left - parentRect.left;
|
|
112020
112587
|
origTop = rect.top - parentRect.top;
|
|
112021
112588
|
// Switch from right-positioning to left-positioning for drag
|
|
@@ -112029,8 +112596,8 @@ void mainImage(const in vec4 inputColor, const in vec2 uv, const in float depth,
|
|
|
112029
112596
|
return;
|
|
112030
112597
|
const dx = e.clientX - startX;
|
|
112031
112598
|
const dy = e.clientY - startY;
|
|
112032
|
-
dialog.style.left =
|
|
112033
|
-
dialog.style.top =
|
|
112599
|
+
dialog.style.left = origLeft + dx + "px";
|
|
112600
|
+
dialog.style.top = origTop + dy + "px";
|
|
112034
112601
|
}, { signal });
|
|
112035
112602
|
document.addEventListener("mouseup", () => {
|
|
112036
112603
|
dragging = false;
|
|
@@ -112048,7 +112615,8 @@ void mainImage(const in vec4 inputColor, const in vec2 uv, const in float depth,
|
|
|
112048
112615
|
}
|
|
112049
112616
|
let currentValue = // eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
112050
112617
|
material[param.key];
|
|
112051
|
-
const isInfinity = param.infinity === true &&
|
|
112618
|
+
const isInfinity = param.infinity === true &&
|
|
112619
|
+
(currentValue === Infinity || currentValue == null);
|
|
112052
112620
|
if (isInfinity)
|
|
112053
112621
|
currentValue = param.max;
|
|
112054
112622
|
this._buildMatEditorRow(content, param, currentValue ?? 0, isInfinity, originalMat);
|
|
@@ -112060,8 +112628,8 @@ void mainImage(const in vec4 inputColor, const in vec2 uv, const in float depth,
|
|
|
112060
112628
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
112061
112629
|
const origValue = originalMat[param.key];
|
|
112062
112630
|
const isChanged = (v) => param.infinity
|
|
112063
|
-
?
|
|
112064
|
-
|
|
112631
|
+
? v >= param.max !== (origValue === Infinity || origValue == null) ||
|
|
112632
|
+
(v < param.max && Math.abs(v - origValue) > param.step * 0.5)
|
|
112065
112633
|
: Math.abs(v - origValue) > param.step * 0.5;
|
|
112066
112634
|
const label = document.createElement("label");
|
|
112067
112635
|
label.className = "tcv_mat_editor_label";
|
|
@@ -112081,7 +112649,9 @@ void mainImage(const in vec4 inputColor, const in vec2 uv, const in float depth,
|
|
|
112081
112649
|
const valueDisplay = document.createElement("input");
|
|
112082
112650
|
valueDisplay.className = "tcv_clip_input";
|
|
112083
112651
|
valueDisplay.readOnly = true;
|
|
112084
|
-
valueDisplay.value = isInfinity
|
|
112652
|
+
valueDisplay.value = isInfinity
|
|
112653
|
+
? "\u221E"
|
|
112654
|
+
: _formatMatValue(value, param.step);
|
|
112085
112655
|
slider.addEventListener("input", () => {
|
|
112086
112656
|
const newValue = parseFloat(slider.value);
|
|
112087
112657
|
const result = this.viewer.getSelectedObjectGroup();
|
|
@@ -112170,7 +112740,8 @@ void mainImage(const in vec4 inputColor, const in vec2 uv, const in float depth,
|
|
|
112170
112740
|
this.studioShadowIntensitySlider?.setValueFromState(state.get("studioShadowIntensity") * 100);
|
|
112171
112741
|
this.studioShadowSoftnessSlider?.setValueFromState(state.get("studioShadowSoftness") * 100);
|
|
112172
112742
|
this.studioAOIntensitySlider?.setValueFromState(state.get("studioAOIntensity") * 10);
|
|
112173
|
-
this.getInputElement("tcv_studio_4k_env_maps").checked =
|
|
112743
|
+
this.getInputElement("tcv_studio_4k_env_maps").checked =
|
|
112744
|
+
state.get("studio4kEnvMaps");
|
|
112174
112745
|
this._syncEnvDropdown(state.get("studioEnvironment"));
|
|
112175
112746
|
const bgEl = this.container.querySelector(".tcv_studio_background");
|
|
112176
112747
|
if (bgEl instanceof HTMLSelectElement)
|
|
@@ -112199,7 +112770,10 @@ void mainImage(const in vec4 inputColor, const in vec2 uv, const in float depth,
|
|
|
112199
112770
|
}
|
|
112200
112771
|
else {
|
|
112201
112772
|
// Add or update a "Custom" optgroup with the custom HDR entry
|
|
112202
|
-
const label = envName
|
|
112773
|
+
const label = envName
|
|
112774
|
+
.split("/")
|
|
112775
|
+
.pop()
|
|
112776
|
+
?.replace(/\.hdr$/i, "") || "Custom HDR";
|
|
112203
112777
|
let customGroup = el.querySelector("optgroup[data-custom]");
|
|
112204
112778
|
if (customGroup) {
|
|
112205
112779
|
const opt = customGroup.querySelector("option");
|
|
@@ -112230,7 +112804,8 @@ void mainImage(const in vec4 inputColor, const in vec2 uv, const in float depth,
|
|
|
112230
112804
|
const isPreset = this.viewer.envManager.isPreset(envName);
|
|
112231
112805
|
cb.disabled = !isPreset;
|
|
112232
112806
|
if (!isPreset) {
|
|
112233
|
-
cb.title =
|
|
112807
|
+
cb.title =
|
|
112808
|
+
"4K switching is only available for built-in Poly Haven presets";
|
|
112234
112809
|
}
|
|
112235
112810
|
else {
|
|
112236
112811
|
cb.title = "";
|
|
@@ -112263,8 +112838,17 @@ void mainImage(const in vec4 inputColor, const in vec2 uv, const in float depth,
|
|
|
112263
112838
|
_dispatchAction(action) {
|
|
112264
112839
|
// Toggle buttons
|
|
112265
112840
|
const toggleActions = [
|
|
112266
|
-
"axes",
|
|
112267
|
-
"
|
|
112841
|
+
"axes",
|
|
112842
|
+
"axes0",
|
|
112843
|
+
"grid",
|
|
112844
|
+
"perspective",
|
|
112845
|
+
"transparent",
|
|
112846
|
+
"blackedges",
|
|
112847
|
+
"explode",
|
|
112848
|
+
"zscale",
|
|
112849
|
+
"distance",
|
|
112850
|
+
"properties",
|
|
112851
|
+
"select",
|
|
112268
112852
|
];
|
|
112269
112853
|
if (toggleActions.includes(action)) {
|
|
112270
112854
|
this._toggleClickButton(action);
|
|
@@ -112474,6 +113058,7 @@ void mainImage(const in vec4 inputColor, const in vec2 uv, const in float depth,
|
|
|
112474
113058
|
* @public
|
|
112475
113059
|
*/
|
|
112476
113060
|
setTheme(theme) {
|
|
113061
|
+
let resolved;
|
|
112477
113062
|
if (theme === "dark" ||
|
|
112478
113063
|
(theme === "browser" &&
|
|
112479
113064
|
window.matchMedia("(prefers-color-scheme: dark)").matches)) {
|
|
@@ -112485,7 +113070,7 @@ void mainImage(const in vec4 inputColor, const in vec2 uv, const in float depth,
|
|
|
112485
113070
|
this.viewer.gridHelper.update(this.viewer.getCameraZoom(), true, "dark");
|
|
112486
113071
|
}
|
|
112487
113072
|
this.viewer.update(true);
|
|
112488
|
-
|
|
113073
|
+
resolved = "dark";
|
|
112489
113074
|
}
|
|
112490
113075
|
else {
|
|
112491
113076
|
this.container.setAttribute("data-theme", "light");
|
|
@@ -112496,8 +113081,14 @@ void mainImage(const in vec4 inputColor, const in vec2 uv, const in float depth,
|
|
|
112496
113081
|
this.viewer.gridHelper.update(this.viewer.getCameraZoom(), true, "light");
|
|
112497
113082
|
}
|
|
112498
113083
|
this.viewer.update(true);
|
|
112499
|
-
|
|
112500
|
-
}
|
|
113084
|
+
resolved = "light";
|
|
113085
|
+
}
|
|
113086
|
+
// Keep state.theme in sync with the DOM. Without this, paths that call
|
|
113087
|
+
// setTheme directly (matchMedia listener, viewer.setTheme, MutationObserver
|
|
113088
|
+
// bridges) would update the DOM while leaving state.theme stale, and the
|
|
113089
|
+
// next viewer.render() would re-apply the stale state value
|
|
113090
|
+
this.viewer.state.set("theme", resolved, false);
|
|
113091
|
+
return resolved;
|
|
112501
113092
|
}
|
|
112502
113093
|
}
|
|
112503
113094
|
|