three-cad-viewer 4.3.2 → 4.3.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "three-cad-viewer",
3
- "version": "4.3.2",
3
+ "version": "4.3.5",
4
4
  "type": "module",
5
5
  "description": "WebGL-based CAD viewer built on Three.js with clipping planes, measurement tools, and tree navigation",
6
6
  "repository": {
@@ -8,6 +8,7 @@
8
8
  "url": "https://github.com/bernhard-42/three-cad-viewer"
9
9
  },
10
10
  "files": [
11
+ "README.md",
11
12
  "dist/**/*.d.ts",
12
13
  "dist/three-cad-viewer.css",
13
14
  "dist/three-cad-viewer.esm.js",
package/src/_version.ts CHANGED
@@ -1 +1 @@
1
- export const version: string = "4.3.2";
1
+ export const version: string = "4.3.5";
@@ -540,7 +540,7 @@ class StudioManager {
540
540
  light.target.position.copy(bboxCenter);
541
541
 
542
542
  light.castShadow = true;
543
- const frustumSize = maxExtent * 4.0;
543
+ const frustumSize = maxExtent * 6.0;
544
544
  light.shadow.camera.left = -frustumSize;
545
545
  light.shadow.camera.right = frustumSize;
546
546
  light.shadow.camera.top = frustumSize;
package/src/core/types.ts CHANGED
@@ -625,27 +625,29 @@ export interface MaterialAppearance {
625
625
  *
626
626
  * This format is produced by the threejs-materials Python library, which catalogs
627
627
  * PBR materials from ambientCG, GPUOpen, PolyHaven, and PhysicallyBased.
628
- * The `properties` dict uses simplified property names (e.g., "color", "roughness",
629
- * "normal") where each entry has an optional `value` (scalar or color array in
630
- * linear RGB) and/or `texture` (inline data URI).
628
+ * `values` contains scalar properties (e.g., color as linear RGB array,
629
+ * roughness as float). `textures` contains texture references (inline data URIs
630
+ * or file paths) keyed by property name.
631
631
  *
632
- * Detected by the presence of the `properties` key.
632
+ * Detected by the presence of the `values` key.
633
633
  * Extra keys from threejs-materials (id, name, source, url, license) pass through
634
634
  * harmlessly and are not part of this interface.
635
635
  */
636
636
  export interface MaterialXMaterial {
637
- /** Material properties from threejs-materials. Each key maps to { value?, texture? } */
638
- properties: Record<string, { value?: unknown; texture?: string }>;
637
+ /** Scalar PBR property values (e.g., color, metalness, roughness). */
638
+ values: Record<string, unknown>;
639
+ /** Texture map references keyed by property name (e.g., color, normal). */
640
+ textures: Record<string, string>;
639
641
  /** Optional texture tiling [u, v], default [1, 1]. Applied to all textures. */
640
642
  textureRepeat?: [number, number];
641
643
  }
642
644
 
643
645
  /**
644
646
  * Type guard to check if a material entry is a threejs-materials format dict.
645
- * Detected by the presence of the `properties` key.
647
+ * Detected by the presence of the `values` key.
646
648
  */
647
649
  export function isMaterialXMaterial(m: unknown): m is MaterialXMaterial {
648
- return typeof m === "object" && m !== null && "properties" in m;
650
+ return typeof m === "object" && m !== null && "values" in m;
649
651
  }
650
652
 
651
653
  // =============================================================================
@@ -854,7 +856,7 @@ export interface Shapes {
854
856
  /** User-defined material library (root node).
855
857
  * Values can be:
856
858
  * - string: builtin preset reference (e.g., "builtin:car-paint")
857
- * - MaterialXMaterial: threejs-materials format (detected by `properties` key)
859
+ * - MaterialXMaterial: threejs-materials format (detected by `values` key)
858
860
  * - MaterialAppearance: preset with overrides (e.g., { builtin: "acrylic-clear", color: "#55a0e3" })
859
861
  */
860
862
  materials?: Record<string, string | MaterialXMaterial | MaterialAppearance> | undefined;
@@ -478,10 +478,12 @@ class MaterialFactory {
478
478
  }
479
479
 
480
480
  // --- Anisotropy (brushed metal) ---
481
- // Skipped: anisotropic reflections require tangent vectors on the mesh.
482
- // CAD tessellation never provides tangents, so Three.js falls back to
483
- // screen-space derivative tangents which produce visible diamond-shaped
484
- // facet artifacts on coarse meshes.
481
+ if (def.anisotropy !== undefined && def.anisotropy > 0) {
482
+ material.anisotropy = def.anisotropy;
483
+ if (def.anisotropyRotation !== undefined) {
484
+ material.anisotropyRotation = def.anisotropyRotation;
485
+ }
486
+ }
485
487
 
486
488
  // --- Textures ---
487
489
  // Resolve all texture references via TextureCache.
@@ -501,14 +503,16 @@ class MaterialFactory {
501
503
  * "roughness", "normal") where each entry has an optional `value` (scalar or
502
504
  * [r,g,b] array in **linear RGB**) and/or `texture` (inline data URI).
503
505
  *
504
- * @param properties - Material properties from threejs-materials
506
+ * @param values - Scalar PBR values from threejs-materials
507
+ * @param textures - Texture map references from threejs-materials
505
508
  * @param textureRepeat - Optional [u, v] texture tiling applied to all loaded textures
506
509
  * @param textureCache - TextureCache for resolving data URI textures
507
510
  * @param label - Optional label for GPU tracking
508
511
  * @returns Configured MeshPhysicalMaterial
509
512
  */
510
513
  async createStudioMaterialFromMaterialX(
511
- properties: Record<string, { value?: unknown; texture?: string }>,
514
+ values: Record<string, unknown>,
515
+ textures: Record<string, string>,
512
516
  textureRepeat: [number, number] | undefined,
513
517
  textureCache: TextureCacheInterface | null,
514
518
  label?: string,
@@ -524,36 +528,31 @@ class MaterialFactory {
524
528
  };
525
529
 
526
530
  // Warn once if displacement data is present (not supported in Studio)
527
- if (properties.displacement?.texture || properties.displacementScale?.value !== undefined) {
531
+ if (textures.displacement || values.displacementScale !== undefined) {
528
532
  logger.warn("Displacement not supported by the Studio");
529
533
  }
530
534
 
531
- for (const [key, prop] of Object.entries(properties)) {
532
- if (prop.value === undefined) continue;
533
-
535
+ for (const [key, value] of Object.entries(values)) {
534
536
  // Skip displacement properties (not supported, would waste GPU memory)
535
537
  if (key === "displacement" || key === "displacementScale" || key === "displacementBias") continue;
536
538
 
537
- // Skip anisotropy — requires tangent vectors that CAD meshes don't have
538
- if (key === "anisotropy" || key === "anisotropyRotation") continue;
539
-
540
539
  // Color arrays → THREE.Color (already linear, no sRGB conversion)
541
- if (COLOR_ARRAY_KEYS.has(key) && Array.isArray(prop.value)) {
542
- const [r, g, b] = prop.value as number[];
540
+ if (COLOR_ARRAY_KEYS.has(key) && Array.isArray(value)) {
541
+ const [r, g, b] = value as number[];
543
542
  matOptions[key] = new THREE.Color(r, g, b);
544
- } else if ((key === "normalScale" || key === "clearcoatNormalScale") && Array.isArray(prop.value)) {
545
- matOptions[key] = new THREE.Vector2(prop.value[0], prop.value[1]);
546
- } else if (key === "iridescenceThicknessRange" && Array.isArray(prop.value)) {
547
- matOptions[key] = prop.value;
543
+ } else if ((key === "normalScale" || key === "clearcoatNormalScale") && Array.isArray(value)) {
544
+ matOptions[key] = new THREE.Vector2(value[0], value[1]);
545
+ } else if (key === "iridescenceThicknessRange" && Array.isArray(value)) {
546
+ matOptions[key] = value;
548
547
  } else {
549
- matOptions[key] = prop.value;
548
+ matOptions[key] = value;
550
549
  }
551
550
  }
552
551
 
553
552
  // --- Handle transmission ---
554
- const transmissionVal = properties.transmission?.value;
555
- const opacityVal = properties.opacity?.value;
556
- const transparentVal = properties.transparent?.value;
553
+ const transmissionVal = values.transmission;
554
+ const opacityVal = values.opacity;
555
+ const transparentVal = values.transparent;
557
556
  if (typeof transmissionVal === "number" && transmissionVal > 0) {
558
557
  matOptions.transparent = false;
559
558
  matOptions.opacity = 1.0;
@@ -571,9 +570,7 @@ class MaterialFactory {
571
570
  // --- Resolve textures ---
572
571
  let hasTextures = false;
573
572
  if (textureCache) {
574
- for (const [key, prop] of Object.entries(properties)) {
575
- if (!prop.texture) continue;
576
-
573
+ for (const [key, textureRef] of Object.entries(textures)) {
577
574
  const mapName = PROPERTY_TO_MAP[key];
578
575
  if (!mapName) continue;
579
576
 
@@ -584,7 +581,7 @@ class MaterialFactory {
584
581
  const roleForCache = colorSpace === THREE.SRGBColorSpace
585
582
  ? "baseColorTexture"
586
583
  : "normalTexture";
587
- const tex = await textureCache.get(prop.texture, roleForCache);
584
+ const tex = await textureCache.get(textureRef, roleForCache);
588
585
  if (tex) {
589
586
  if (textureRepeat) {
590
587
  tex.repeat.set(textureRepeat[0], textureRepeat[1]);
@@ -594,7 +591,6 @@ class MaterialFactory {
594
591
  hasTextures = true;
595
592
  }
596
593
  }
597
-
598
594
  }
599
595
 
600
596
  // Enable alpha cutout when an alphaMap is present
@@ -719,9 +715,8 @@ class MaterialFactory {
719
715
  const sheenRoughnessTex = await resolve(def.sheenRoughnessMap, "sheenRoughnessTexture");
720
716
  if (sheenRoughnessTex) material.sheenRoughnessMap = sheenRoughnessTex;
721
717
 
722
- // Anisotropy texture skipped CAD meshes lack tangent vectors.
723
- // const anisotropyTex = await resolve(def.anisotropyMap, "anisotropyTexture");
724
- // if (anisotropyTex) material.anisotropyMap = anisotropyTex;
718
+ const anisotropyTex = await resolve(def.anisotropyMap, "anisotropyTexture");
719
+ if (anisotropyTex) material.anisotropyMap = anisotropyTex;
725
720
  }
726
721
 
727
722
  /**
@@ -77,7 +77,7 @@ class StudioFloor {
77
77
  * Create a shadow-receiving plane at the floor position.
78
78
  */
79
79
  private _createShadowPlane(zPosition: number, sceneSize: number): void {
80
- const floorSize = sceneSize * 4;
80
+ const floorSize = sceneSize * 6;
81
81
 
82
82
  const geometry = new THREE.PlaneGeometry(floorSize, floorSize);
83
83
  const material = new THREE.ShadowMaterial({ opacity: 0.5, depthWrite: false });
@@ -154,6 +154,16 @@ class CenteredPlane extends THREE.Plane {
154
154
  const z = this.distanceToPoint(new THREE.Vector3(0, 0, 0));
155
155
  this.constant = z - c + value;
156
156
  }
157
+
158
+ /**
159
+ * Clone this CenteredPlane.
160
+ * Overrides THREE.Plane.clone() which calls `new this.constructor()` without
161
+ * arguments, causing `center` to be undefined during shadow map generation.
162
+ */
163
+ // @ts-expect-error -- THREE.Plane.clone() returns `this`, but we need a concrete CenteredPlane
164
+ clone(): CenteredPlane {
165
+ return new CenteredPlane(this.normal.clone(), this.centeredConstant, [...this.center]);
166
+ }
157
167
  }
158
168
 
159
169
  // ============================================================================
@@ -142,12 +142,9 @@ function materialHasTexture(def: MaterialAppearance): boolean {
142
142
  return false;
143
143
  }
144
144
 
145
- /** Check whether a threejs-materials entry has texture references in its properties. */
145
+ /** Check whether a threejs-materials entry has texture references. */
146
146
  function materialXHasTextures(entry: MaterialXMaterial): boolean {
147
- for (const [, prop] of Object.entries(entry.properties)) {
148
- if (prop.texture) return true;
149
- }
150
- return false;
147
+ return Object.keys(entry.textures).length > 0;
151
148
  }
152
149
 
153
150
  class NestedGroup {
@@ -322,7 +319,7 @@ class NestedGroup {
322
319
  return null;
323
320
  }
324
321
 
325
- // MaterialXMaterial entry: object with `properties` key
322
+ // MaterialXMaterial entry: object with `values` key
326
323
  if (isMaterialXMaterial(entry)) {
327
324
  this.resolvedMaterialX.set(tag, entry);
328
325
  return entry;
@@ -339,12 +336,16 @@ class NestedGroup {
339
336
  );
340
337
  return null;
341
338
  }
342
- const resolved: MaterialAppearance = { ...preset, ...appearance };
339
+ // Strip preset color unless the user explicitly provides one,
340
+ // so the leaf node's CAD color is used as fallback.
341
+ const { color: presetColor, ...presetRest } = preset;
342
+ const resolved: MaterialAppearance = "color" in appearance
343
+ ? { ...preset, ...appearance }
344
+ : { ...presetRest, ...appearance };
343
345
  this.resolvedMaterials.set(tag, resolved);
344
346
  return resolved;
345
347
  }
346
348
 
347
- // Should not happen with current type, but guard anyway
348
349
  logger.warn(`Unrecognised material entry for tag '${tag}' on '${objectPath}'`);
349
350
  return null;
350
351
  }
@@ -1253,7 +1254,8 @@ class NestedGroup {
1253
1254
  if (resolved && isMaterialXMaterial(resolved)) {
1254
1255
  // --- threejs-materials path ---
1255
1256
  studioMaterial = await this.materialFactory.createStudioMaterialFromMaterialX(
1256
- resolved.properties,
1257
+ resolved.values,
1258
+ resolved.textures,
1257
1259
  resolved.textureRepeat,
1258
1260
  this._textureCache as TextureCacheInterface,
1259
1261
  );
@@ -1339,6 +1341,20 @@ class NestedGroup {
1339
1341
  studioBack = cachedBack as THREE.MeshPhysicalMaterial;
1340
1342
  }
1341
1343
 
1344
+ // Compute tangents for anisotropic materials (required by Three.js)
1345
+ if (
1346
+ studioMaterial instanceof THREE.MeshPhysicalMaterial &&
1347
+ studioMaterial.anisotropy > 0 &&
1348
+ obj.shapeGeometry?.getAttribute("uv") != null &&
1349
+ obj.shapeGeometry.getAttribute("tangent") == null
1350
+ ) {
1351
+ try {
1352
+ obj.shapeGeometry.computeTangents();
1353
+ } catch {
1354
+ logger.debug(`Studio "${path}": tangent computation failed, anisotropy may have artifacts`);
1355
+ }
1356
+ }
1357
+
1342
1358
  // Apply to ObjectGroup
1343
1359
  obj.enterStudioMode(
1344
1360
  studioMaterial instanceof THREE.MeshPhysicalMaterial ? studioMaterial : null,