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.
@@ -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 && obj1.every((v, i) => isEqual(v, obj2[i], tol)));
81599
+ return (obj1.length === obj2.length &&
81600
+ obj1.every((v, i) => isEqual(v, obj2[i], tol)));
81600
81601
  }
81601
- else if (obj1 !== null && obj2 !== null && typeof obj1 === "object" && typeof obj2 === "object") {
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 && typeof attr === "object" && "dispose" in attr && typeof attr.dispose === "function") {
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", "normalMap", "roughnessMap", "metalnessMap",
81646
- "aoMap", "emissiveMap", "alphaMap", "bumpMap",
81652
+ "map",
81653
+ "normalMap",
81654
+ "roughnessMap",
81655
+ "metalnessMap",
81656
+ "aoMap",
81657
+ "emissiveMap",
81658
+ "alphaMap",
81659
+ "bumpMap",
81647
81660
  // MeshPhysicalMaterial
81648
- "transmissionMap", "clearcoatMap", "clearcoatRoughnessMap", "clearcoatNormalMap",
81649
- "thicknessMap", "specularIntensityMap", "specularColorMap",
81650
- "sheenColorMap", "sheenRoughnessMap", "anisotropyMap",
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 && obj.isOrthographicCamera === true;
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 && obj.isPerspectiveCamera === true;
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 && material.isMeshStandardMaterial === true;
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) ? mesh.material[0] : 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) && currentMaterial.map) {
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 && this.front.material instanceof MeshStandardMaterial) {
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 && this.renderback) {
82559
- if (this._isStudioMode) {
82560
- this.back.visible = flag;
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.back.material.visible = flag;
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 ? this.originalColor.clone() : null;
82778
- this._cadOriginalBackColor = this.originalBackColor ? this.originalBackColor.clone() : null;
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 ? this.edgeMaterial.visible : null;
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) ? SRGBColorSpace : LinearSRGBColorSpace;
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", "specularColor", "sheenColor", "emissive", "attenuationColor",
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" || key === "displacementScale" || key === "displacementBias")
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") && Array.isArray(value)) {
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 || (typeof opacityVal === "number" && opacityVal < 1.0)) {
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
- if (textureRepeat && (textureRepeat[0] !== 1 || textureRepeat[1] !== 1)) {
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
- tex.repeat.set(textureRepeat[0], textureRepeat[1]);
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
- "chrome": {
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
- "gold": {
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
- "copper": {
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
- "brass": {
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
- "titanium": {
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
- "galvanized": {
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
- "nylon": {
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
- "concrete": {
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
- const repeat = (material.map ?? material.roughnessMap ?? material.normalMap ??
84408
- material.metalnessMap ?? material.emissiveMap ?? material.aoMap)?.repeat?.clone() ?? new Vector2(1, 1);
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", "normalMap", "aoMap",
84485
- "metalnessMap", "roughnessMap", "emissiveMap", "transmissionMap",
84486
- "clearcoatMap", "clearcoatRoughnessMap", "clearcoatNormalMap",
84487
- "thicknessMap", "specularIntensityMap", "specularColorMap",
84488
- "sheenColorMap", "sheenRoughnessMap", "anisotropyMap",
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 = "triplanar") {
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 = await this.materialFactory.createStudioMaterialFromMaterialX(resolved.values, resolved.textures, resolved.textureRepeat, this._textureCache);
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 && studioMaterial instanceof MeshPhysicalMaterial) {
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 && studioMaterial instanceof MeshPhysicalMaterial) {
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 ? studioMaterial : null, studioBack);
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 = [States.unselected, States.unselected];
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 && (prefix == null || (p.dataset.path || "").startsWith(prefix)));
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 = el.offsetTop - this.scrollContainer.offsetTop;
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 = el.offsetTop - this.scrollContainer.offsetTop;
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, [...this.center]);
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 && group.subtype === "solid" && group.front) {
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) || !Array.isArray(shape.triangles[0])) {
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) || (edgesRaw.length > 0 && !Array.isArray(edgesRaw[0]))) {
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: { pos: new Vector3(0, 0, 1), quat: new Quaternion(0, 0, 0, 1) },
92990
- bottom: { pos: new Vector3(0, 0, -1), quat: new Quaternion(1, 0, 0, 0) },
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 && object.visible && !Array.isArray(object.material) && object.material.visible) {
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 || Array.isArray(obj.material) || !obj.material.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) && value.length === length && value.every((v) => typeof v === "number");
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", "tool_type", "subtype", "info",
93715
- "refpoint", "refpoint1", "refpoint2",
93716
- "shape_type", "geom_type", "groups", "result",
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 || !this.viewer || !this.viewer.camera || !this.panel.isVisible())
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
- { "point 1": this.point1.toArray(), "point 2": this.point2.toArray() },
94335
- { angle: 43.21, "reference 1": "Plane (Face)", "reference 2": "Plane (Face)" },
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
- { center: this.point1.toArray(), "major radius": 0.4, "minor radius": 0.2 },
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
- { bb: { min: [1.8, -1, 0.0], center: [2.1, -0.9, 0.0], max: [2.4, -0.8, 0.0], size: [0.56, 0.2, 0.0] } },
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) && isDistanceResponseData(this.responseData)) {
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) && isPropertiesResponseData(this.responseData)) {
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) && this.responseData.refpoint) {
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.8";
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 : (sign ? -Infinity : Infinity);
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 = ((0.5 - sy / height)) * Math.PI;
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).filter(v => v > 0).sort((a, b) => a - b);
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 = options.presetUrls ?? {};
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
- // Prefer the raw HDR for the background so the backdrop samples
95975
- // at source resolution (2K/4K) rather than the 256² PMREM cubemap.
95976
- // Falls back to PMREM for procedural "studio" (no source HDR).
95977
- const bgTex = this._currentBackgroundTexture ?? this._currentTexture;
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, bgTex, upIsZ, rotY);
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 && currentEnvName !== "none" && currentEnvName !== "studio") {
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 || this._bgRenderTarget.width !== w || this._bgRenderTarget.height !== h) {
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 for IBL. The source equirectangular HDR is
96347
- * preserved and cached separately (in `_hdrCache`) so that "environment"
96348
- * background mode can sample the full-resolution equirectangular texture
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 && hdrTexture.image.width && hdrTexture.image.height) {
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
- // Preserve the equirectangular HDR for use as `scene.background` at
96380
- // source resolution. PMREM's base mip is a 256² cubemap good for IBL
96381
- // (roughness-weighted prefilter) but visibly soft as a backdrop.
96382
- hdrTexture.mapping = EquirectangularReflectionMapping;
96383
- // Cache render target and HDR; track both.
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 = intensity * 1.0;
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({ opacity: 0.5, depthWrite: false });
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
- // Tone-mapping: maps viewer strings to postprocessing ToneMappingMode
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
- "neutral": ToneMappingMode.NEUTRAL,
103939
- "ACES": ToneMappingMode.ACES_FILMIC,
103940
- "none": ToneMappingMode.LINEAR,
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
- // Postprocessing library requires renderer.toneMapping = NoToneMapping.
104020
- // Tone mapping is handled by ToneMappingEffect in the pipeline.
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 = 0: MSAA conflicts with depth-based AO passes;
104024
- // antialiasing is handled by SMAAEffect instead.
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: 0,
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.HIGH });
104051
- // ShadowMaskEffect is first so it runs in linear space before tone mapping
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 && this._blurredObjectMaskRT && this._blurredFloorMaskRT) {
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 && this._shadowMaskRT && this._blurPass
104246
- && this._blurredObjectMaskRT && this._blurredFloorMaskRT) {
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 = this._blurredObjectMaskRT.texture;
104258
- this._shadowMaskEffect.uniforms.get("shadowMaskFloor").value = this._blurredFloorMaskRT.texture;
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", "cadWidth", "treeWidth", "treeHeight", "height", "pinning", "glass", "tools",
104392
- "keymap", "newTreeBehavior", "measureTools", "selectTool", "explodeTool", "zscaleTool",
104393
- "zebraTool", "studioTool", "measurementDebug",
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", "directIntensity", "metalness", "roughness", "defaultOpacity",
104396
- "edgeColor", "normalLen",
104590
+ "ambientIntensity",
104591
+ "directIntensity",
104592
+ "metalness",
104593
+ "roughness",
104594
+ "defaultOpacity",
104595
+ "edgeColor",
104596
+ "normalLen",
104397
104597
  // Viewer
104398
- "axes", "axes0", "grid", "ortho", "transparent", "blackEdges", "collapse",
104399
- "clipIntersection", "clipPlaneHelpers", "clipObjectColors", "clipNormal0", "clipNormal1",
104400
- "clipNormal2", "clipSlider0", "clipSlider1", "clipSlider2", "control", "holroyd", "up",
104401
- "ticks", "gridFontSize", "centerGrid", "position", "quaternion", "target", "zoom",
104402
- "panSpeed", "rotateSpeed", "zoomSpeed", "timeit",
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", "zebraOpacity", "zebraDirection", "zebraColorScheme", "zebraMappingMode",
104629
+ "zebraCount",
104630
+ "zebraOpacity",
104631
+ "zebraDirection",
104632
+ "zebraColorScheme",
104633
+ "zebraMappingMode",
104405
104634
  // Studio
104406
- "studioEnvironment", "studioEnvIntensity", "studioBackground",
104407
- "studioToneMapping", "studioExposure", "studio4kEnvMaps", "studioTextureMapping",
104408
- "studioEnvRotation", "studioShadowIntensity", "studioShadowSoftness", "studioAOIntensity",
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", "animationMode", "animationSliderValue", "zscaleActive", "highlightedButton",
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 || (value === null && !KEYS_WITH_VALID_NULL.includes(key)))
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
- const { clipNormal0, clipNormal1, clipNormal2, position, quaternion, target, ...rest } = options;
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 ? new Quaternion(...quaternion) : null;
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
- ? { old: change.old != null ? transform(change.old) : null, new: transform(change.new) }
105033
+ ? {
105034
+ old: change.old != null ? transform(change.old) : null,
105035
+ new: transform(change.new),
105036
+ }
104785
105037
  : change;
104786
- this._externalNotifyCallback({ key: notificationKey, change: notifyChange });
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", ctrl: "ctrlKey", meta: "metaKey", alt: "altKey",
104865
- axes: "a", axes0: "A", grid: "g", gridxy: "G", perspective: "p", transparent: "t", blackedges: "b",
104866
- reset: "R", resize: "r",
104867
- iso: "5", front: "1", rear: "3", top: "8", bottom: "2", left: "4", right: "6",
104868
- explode: "x", zscale: "L", distance: "D", properties: "P", select: "S", help: "h", play: " ", stop: "Escape",
104869
- tree: "T", clip: "C", material: "M", zebra: "Z", studio: "s",
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
- studioTextureMapping: "triplanar",
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.loadEnvironment(change.new, this._ctx.renderer).then(() => {
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
- }).catch((err) => {
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.setUse4kEnvMaps(change.new, envName, this._ctx.renderer).then(() => {
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) ? obj.material : [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() { return this._studioManager.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.getPosition().clone().sub(this.rendered.controls.getTarget()), this.rendered.camera.getQuaternion());
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({ ...ViewerState.DISPLAY_DEFAULTS.keymap, ...options.keymap });
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 && typeof this.renderer.forceContextLoss === "function") {
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
- const clipSlider0 = viewerOptions.clipSlider0 != null
108008
- ? viewerOptions.clipSlider0
108009
- : this.gridSize / 2;
108010
- const clipSlider1 = viewerOptions.clipSlider1 != null
108011
- ? viewerOptions.clipSlider1
108012
- : this.gridSize / 2;
108013
- const clipSlider2 = viewerOptions.clipSlider2 != null
108014
- ? viewerOptions.clipSlider2
108015
- : this.gridSize / 2;
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
- // Set normals first (if provided), passing slider values to avoid reset to gridSize/2
108038
- this.setClipNormal(0, viewerOptions.clipNormal0 ?? null, clipSlider0, true);
108039
- this.setClipNormal(1, viewerOptions.clipNormal1 ?? null, clipSlider1, true);
108040
- this.setClipNormal(2, viewerOptions.clipNormal2 ?? null, clipSlider2, true);
108041
- // Set sliders for any planes without custom normals (setClipNormal returns early if normal is null)
108042
- this.setClipSlider(0, clipSlider0, true);
108043
- this.setClipSlider(1, clipSlider1, true);
108044
- this.setClipSlider(2, clipSlider2, true);
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: { old: null, new: this.rendered.camera.getPosition().toArray() },
108057
- quaternion: { old: null, new: this.rendered.camera.getQuaternion().toArray() },
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
- this.update(true, false);
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.clone().sub(this.rendered.controls.getTarget());
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: [[0, 0, 0], [0, 0, 0, 1]],
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 * Math.max(Math.abs(this.bbox.min.length()), Math.abs(this.bbox.max.length()));
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] = value;
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" && this.notifyCallback && this.index !== undefined) {
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) || !(inputEl 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
- { key: "metalness", label: "Metallic", min: 0, max: 1, step: 0.01, group: "PBR Core" },
109953
- { key: "roughness", label: "Roughness", min: 0, max: 1, step: 0.01, group: "PBR Core" },
109954
- { key: "clearcoat", label: "Clearcoat", min: 0, max: 1, step: 0.01, group: "Clearcoat" },
109955
- { key: "clearcoatRoughness", label: "Clearcoat Rough.", min: 0, max: 1, step: 0.01, group: "Clearcoat" },
109956
- { key: "transmission", label: "Transmission", min: 0, max: 1, step: 0.01, group: "Transmission" },
109957
- { key: "ior", label: "IOR", min: 1.0, max: 2.5, step: 0.01, group: "Transmission" },
109958
- { key: "thickness", label: "Thickness", min: 0, max: 10, step: 0.1, group: "Transmission" },
109959
- { key: "attenuationDistance", label: "Atten. Distance", min: 0, max: 100, step: 0.5, group: "Transmission", infinity: true },
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
- { key: "sheenRoughness", label: "Sheen Roughness", min: 0, max: 1, step: 0.01, group: "Sheen" },
109962
- { key: "specularIntensity", label: "Specular Intensity", min: 0, max: 2, step: 0.01, group: "Specular" },
109963
- { key: "anisotropy", label: "Anisotropy", min: 0, max: 1, step: 0.01, group: "Anisotropy" },
109964
- { key: "anisotropyRotation", label: "Anisotropy Rotation", min: 0, max: 6.28, step: 0.01, group: "Anisotropy" },
109965
- { key: "emissiveIntensity", label: "Emissive Intensity", min: 0, max: 5, step: 0.1, group: "Emissive" },
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" || value === "darkgrey" || value === "white" || value === "gradient" || value === "gradient-dark" || value === "environment" || value === "transparent") {
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" && target.type !== "checkbox") ||
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 && canvasElement.parentElement !== this.cadView) {
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 && ["distance", "properties", "angle", "select"].includes(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
- [this.tabTree, this.tabClip, this.tabZebra, this.tabMaterial, this.tabStudio].forEach((tabEl) => {
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" && object.shapeGeometry) {
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, { original: originalMat, clone: mat });
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 { /* not rendered */ }
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" && group.shapeGeometry) {
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, { original: currentMat, clone });
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() ?? { left: 0, top: 0 };
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 = (origLeft + dx) + "px";
112033
- dialog.style.top = (origTop + dy) + "px";
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 && (currentValue === Infinity || currentValue == null);
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
- ? (v >= param.max) !== (origValue === Infinity || origValue == null)
112064
- || (v < param.max && Math.abs(v - origValue) > param.step * 0.5)
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 ? "\u221E" : _formatMatValue(value, param.step);
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 = state.get("studio4kEnvMaps");
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.split("/").pop()?.replace(/\.hdr$/i, "") || "Custom HDR";
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 = "4K switching is only available for built-in Poly Haven presets";
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", "axes0", "grid", "perspective", "transparent", "blackedges",
112267
- "explode", "zscale", "distance", "properties", "select",
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
- return "dark";
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
- return "light";
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