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.
@@ -493,26 +493,25 @@ export interface MaterialAppearance {
493
493
  *
494
494
  * This format is produced by the threejs-materials Python library, which catalogs
495
495
  * PBR materials from ambientCG, GPUOpen, PolyHaven, and PhysicallyBased.
496
- * The `properties` dict uses simplified property names (e.g., "color", "roughness",
497
- * "normal") where each entry has an optional `value` (scalar or color array in
498
- * linear RGB) and/or `texture` (inline data URI).
496
+ * `values` contains scalar properties (e.g., color as linear RGB array,
497
+ * roughness as float). `textures` contains texture references (inline data URIs
498
+ * or file paths) keyed by property name.
499
499
  *
500
- * Detected by the presence of the `properties` key.
500
+ * Detected by the presence of the `values` key.
501
501
  * Extra keys from threejs-materials (id, name, source, url, license) pass through
502
502
  * harmlessly and are not part of this interface.
503
503
  */
504
504
  export interface MaterialXMaterial {
505
- /** Material properties from threejs-materials. Each key maps to { value?, texture? } */
506
- properties: Record<string, {
507
- value?: unknown;
508
- texture?: string;
509
- }>;
505
+ /** Scalar PBR property values (e.g., color, metalness, roughness). */
506
+ values: Record<string, unknown>;
507
+ /** Texture map references keyed by property name (e.g., color, normal). */
508
+ textures: Record<string, string>;
510
509
  /** Optional texture tiling [u, v], default [1, 1]. Applied to all textures. */
511
510
  textureRepeat?: [number, number];
512
511
  }
513
512
  /**
514
513
  * Type guard to check if a material entry is a threejs-materials format dict.
515
- * Detected by the presence of the `properties` key.
514
+ * Detected by the presence of the `values` key.
516
515
  */
517
516
  export declare function isMaterialXMaterial(m: unknown): m is MaterialXMaterial;
518
517
  /**
@@ -697,7 +696,7 @@ export interface Shapes {
697
696
  /** User-defined material library (root node).
698
697
  * Values can be:
699
698
  * - string: builtin preset reference (e.g., "builtin:car-paint")
700
- * - MaterialXMaterial: threejs-materials format (detected by `properties` key)
699
+ * - MaterialXMaterial: threejs-materials format (detected by `values` key)
701
700
  * - MaterialAppearance: preset with overrides (e.g., { builtin: "acrylic-clear", color: "#55a0e3" })
702
701
  */
703
702
  materials?: Record<string, string | MaterialXMaterial | MaterialAppearance> | undefined;
@@ -164,16 +164,14 @@ declare class MaterialFactory {
164
164
  * "roughness", "normal") where each entry has an optional `value` (scalar or
165
165
  * [r,g,b] array in **linear RGB**) and/or `texture` (inline data URI).
166
166
  *
167
- * @param properties - Material properties from threejs-materials
167
+ * @param values - Scalar PBR values from threejs-materials
168
+ * @param textures - Texture map references from threejs-materials
168
169
  * @param textureRepeat - Optional [u, v] texture tiling applied to all loaded textures
169
170
  * @param textureCache - TextureCache for resolving data URI textures
170
171
  * @param label - Optional label for GPU tracking
171
172
  * @returns Configured MeshPhysicalMaterial
172
173
  */
173
- createStudioMaterialFromMaterialX(properties: Record<string, {
174
- value?: unknown;
175
- texture?: string;
176
- }>, textureRepeat: [number, number] | undefined, textureCache: TextureCacheInterface | null, label?: string): Promise<THREE.MeshPhysicalMaterial>;
174
+ createStudioMaterialFromMaterialX(values: Record<string, unknown>, textures: Record<string, string>, textureRepeat: [number, number] | undefined, textureCache: TextureCacheInterface | null, label?: string): Promise<THREE.MeshPhysicalMaterial>;
177
175
  /**
178
176
  * Apply alpha mode settings to a material.
179
177
  *
@@ -20,6 +20,12 @@ declare class CenteredPlane extends THREE.Plane {
20
20
  * @param value - The centered constant value.
21
21
  */
22
22
  setConstant(value: number): void;
23
+ /**
24
+ * Clone this CenteredPlane.
25
+ * Overrides THREE.Plane.clone() which calls `new this.constructor()` without
26
+ * arguments, causing `center` to be undefined during shadow map generation.
27
+ */
28
+ clone(): CenteredPlane;
23
29
  }
24
30
  /**
25
31
  * A THREE.Group that only contains PlaneMesh children.
@@ -83121,10 +83121,12 @@ class MaterialFactory {
83121
83121
  }
83122
83122
  }
83123
83123
  // --- Anisotropy (brushed metal) ---
83124
- // Skipped: anisotropic reflections require tangent vectors on the mesh.
83125
- // CAD tessellation never provides tangents, so Three.js falls back to
83126
- // screen-space derivative tangents which produce visible diamond-shaped
83127
- // facet artifacts on coarse meshes.
83124
+ if (def.anisotropy !== undefined && def.anisotropy > 0) {
83125
+ material.anisotropy = def.anisotropy;
83126
+ if (def.anisotropyRotation !== undefined) {
83127
+ material.anisotropyRotation = def.anisotropyRotation;
83128
+ }
83129
+ }
83128
83130
  // --- Textures ---
83129
83131
  // Resolve all texture references via TextureCache.
83130
83132
  // The TextureCache determines colorSpace internally from the texture role name.
@@ -83141,13 +83143,14 @@ class MaterialFactory {
83141
83143
  * "roughness", "normal") where each entry has an optional `value` (scalar or
83142
83144
  * [r,g,b] array in **linear RGB**) and/or `texture` (inline data URI).
83143
83145
  *
83144
- * @param properties - Material properties from threejs-materials
83146
+ * @param values - Scalar PBR values from threejs-materials
83147
+ * @param textures - Texture map references from threejs-materials
83145
83148
  * @param textureRepeat - Optional [u, v] texture tiling applied to all loaded textures
83146
83149
  * @param textureCache - TextureCache for resolving data URI textures
83147
83150
  * @param label - Optional label for GPU tracking
83148
83151
  * @returns Configured MeshPhysicalMaterial
83149
83152
  */
83150
- async createStudioMaterialFromMaterialX(properties, textureRepeat, textureCache, label) {
83153
+ async createStudioMaterialFromMaterialX(values, textures, textureRepeat, textureCache, label) {
83151
83154
  // --- Build material options from scalar values ---
83152
83155
  const matOptions = {
83153
83156
  flatShading: false,
@@ -83158,37 +83161,32 @@ class MaterialFactory {
83158
83161
  depthTest: true,
83159
83162
  };
83160
83163
  // Warn once if displacement data is present (not supported in Studio)
83161
- if (properties.displacement?.texture || properties.displacementScale?.value !== undefined) {
83164
+ if (textures.displacement || values.displacementScale !== undefined) {
83162
83165
  logger.warn("Displacement not supported by the Studio");
83163
83166
  }
83164
- for (const [key, prop] of Object.entries(properties)) {
83165
- if (prop.value === undefined)
83166
- continue;
83167
+ for (const [key, value] of Object.entries(values)) {
83167
83168
  // Skip displacement properties (not supported, would waste GPU memory)
83168
83169
  if (key === "displacement" || key === "displacementScale" || key === "displacementBias")
83169
83170
  continue;
83170
- // Skip anisotropy — requires tangent vectors that CAD meshes don't have
83171
- if (key === "anisotropy" || key === "anisotropyRotation")
83172
- continue;
83173
83171
  // Color arrays → THREE.Color (already linear, no sRGB conversion)
83174
- if (COLOR_ARRAY_KEYS.has(key) && Array.isArray(prop.value)) {
83175
- const [r, g, b] = prop.value;
83172
+ if (COLOR_ARRAY_KEYS.has(key) && Array.isArray(value)) {
83173
+ const [r, g, b] = value;
83176
83174
  matOptions[key] = new Color(r, g, b);
83177
83175
  }
83178
- else if ((key === "normalScale" || key === "clearcoatNormalScale") && Array.isArray(prop.value)) {
83179
- matOptions[key] = new Vector2(prop.value[0], prop.value[1]);
83176
+ else if ((key === "normalScale" || key === "clearcoatNormalScale") && Array.isArray(value)) {
83177
+ matOptions[key] = new Vector2(value[0], value[1]);
83180
83178
  }
83181
- else if (key === "iridescenceThicknessRange" && Array.isArray(prop.value)) {
83182
- matOptions[key] = prop.value;
83179
+ else if (key === "iridescenceThicknessRange" && Array.isArray(value)) {
83180
+ matOptions[key] = value;
83183
83181
  }
83184
83182
  else {
83185
- matOptions[key] = prop.value;
83183
+ matOptions[key] = value;
83186
83184
  }
83187
83185
  }
83188
83186
  // --- Handle transmission ---
83189
- const transmissionVal = properties.transmission?.value;
83190
- const opacityVal = properties.opacity?.value;
83191
- const transparentVal = properties.transparent?.value;
83187
+ const transmissionVal = values.transmission;
83188
+ const opacityVal = values.opacity;
83189
+ const transparentVal = values.transparent;
83192
83190
  if (typeof transmissionVal === "number" && transmissionVal > 0) {
83193
83191
  matOptions.transparent = false;
83194
83192
  matOptions.opacity = 1.0;
@@ -83206,9 +83204,7 @@ class MaterialFactory {
83206
83204
  // --- Resolve textures ---
83207
83205
  let hasTextures = false;
83208
83206
  if (textureCache) {
83209
- for (const [key, prop] of Object.entries(properties)) {
83210
- if (!prop.texture)
83211
- continue;
83207
+ for (const [key, textureRef] of Object.entries(textures)) {
83212
83208
  const mapName = PROPERTY_TO_MAP[key];
83213
83209
  if (!mapName)
83214
83210
  continue;
@@ -83219,7 +83215,7 @@ class MaterialFactory {
83219
83215
  const roleForCache = colorSpace === SRGBColorSpace
83220
83216
  ? "baseColorTexture"
83221
83217
  : "normalTexture";
83222
- const tex = await textureCache.get(prop.texture, roleForCache);
83218
+ const tex = await textureCache.get(textureRef, roleForCache);
83223
83219
  if (tex) {
83224
83220
  if (textureRepeat) {
83225
83221
  tex.repeat.set(textureRepeat[0], textureRepeat[1]);
@@ -83340,9 +83336,9 @@ class MaterialFactory {
83340
83336
  const sheenRoughnessTex = await resolve(def.sheenRoughnessMap, "sheenRoughnessTexture");
83341
83337
  if (sheenRoughnessTex)
83342
83338
  material.sheenRoughnessMap = sheenRoughnessTex;
83343
- // Anisotropy texture skipped CAD meshes lack tangent vectors.
83344
- // const anisotropyTex = await resolve(def.anisotropyMap, "anisotropyTexture");
83345
- // if (anisotropyTex) material.anisotropyMap = anisotropyTex;
83339
+ const anisotropyTex = await resolve(def.anisotropyMap, "anisotropyTexture");
83340
+ if (anisotropyTex)
83341
+ material.anisotropyMap = anisotropyTex;
83346
83342
  }
83347
83343
  /**
83348
83344
  * Update global settings.
@@ -83380,10 +83376,10 @@ var CollapseState;
83380
83376
  })(CollapseState || (CollapseState = {}));
83381
83377
  /**
83382
83378
  * Type guard to check if a material entry is a threejs-materials format dict.
83383
- * Detected by the presence of the `properties` key.
83379
+ * Detected by the presence of the `values` key.
83384
83380
  */
83385
83381
  function isMaterialXMaterial(m) {
83386
- return typeof m === "object" && m !== null && "properties" in m;
83382
+ return typeof m === "object" && m !== null && "values" in m;
83387
83383
  }
83388
83384
  /**
83389
83385
  * Check if shape uses binary format (has triangles_per_face).
@@ -84001,13 +83997,9 @@ function materialHasTexture(def) {
84001
83997
  }
84002
83998
  return false;
84003
83999
  }
84004
- /** Check whether a threejs-materials entry has texture references in its properties. */
84000
+ /** Check whether a threejs-materials entry has texture references. */
84005
84001
  function materialXHasTextures(entry) {
84006
- for (const [, prop] of Object.entries(entry.properties)) {
84007
- if (prop.texture)
84008
- return true;
84009
- }
84010
- return false;
84002
+ return Object.keys(entry.textures).length > 0;
84011
84003
  }
84012
84004
  class NestedGroup {
84013
84005
  /**
@@ -84125,7 +84117,7 @@ class NestedGroup {
84125
84117
  logger.warn(`Invalid material string '${entry}' for tag '${tag}' (expected "builtin:" prefix)`);
84126
84118
  return null;
84127
84119
  }
84128
- // MaterialXMaterial entry: object with `properties` key
84120
+ // MaterialXMaterial entry: object with `values` key
84129
84121
  if (isMaterialXMaterial(entry)) {
84130
84122
  this.resolvedMaterialX.set(tag, entry);
84131
84123
  return entry;
@@ -84139,11 +84131,15 @@ class NestedGroup {
84139
84131
  logger.warn(`Unknown builtin preset '${presetName}' referenced by '${tag}' on '${objectPath}'`);
84140
84132
  return null;
84141
84133
  }
84142
- const resolved = { ...preset, ...appearance };
84134
+ // Strip preset color unless the user explicitly provides one,
84135
+ // so the leaf node's CAD color is used as fallback.
84136
+ const { color: presetColor, ...presetRest } = preset;
84137
+ const resolved = "color" in appearance
84138
+ ? { ...preset, ...appearance }
84139
+ : { ...presetRest, ...appearance };
84143
84140
  this.resolvedMaterials.set(tag, resolved);
84144
84141
  return resolved;
84145
84142
  }
84146
- // Should not happen with current type, but guard anyway
84147
84143
  logger.warn(`Unrecognised material entry for tag '${tag}' on '${objectPath}'`);
84148
84144
  return null;
84149
84145
  }
@@ -84757,7 +84753,7 @@ class NestedGroup {
84757
84753
  try {
84758
84754
  if (resolved && isMaterialXMaterial(resolved)) {
84759
84755
  // --- threejs-materials path ---
84760
- studioMaterial = await this.materialFactory.createStudioMaterialFromMaterialX(resolved.properties, resolved.textureRepeat, this._textureCache);
84756
+ studioMaterial = await this.materialFactory.createStudioMaterialFromMaterialX(resolved.values, resolved.textures, resolved.textureRepeat, this._textureCache);
84761
84757
  if (materialXHasTextures(resolved)) {
84762
84758
  this._texturedMaterialKeys.add(sharingKey);
84763
84759
  }
@@ -84834,6 +84830,18 @@ class NestedGroup {
84834
84830
  }
84835
84831
  studioBack = cachedBack;
84836
84832
  }
84833
+ // Compute tangents for anisotropic materials (required by Three.js)
84834
+ if (studioMaterial instanceof MeshPhysicalMaterial &&
84835
+ studioMaterial.anisotropy > 0 &&
84836
+ obj.shapeGeometry?.getAttribute("uv") != null &&
84837
+ obj.shapeGeometry.getAttribute("tangent") == null) {
84838
+ try {
84839
+ obj.shapeGeometry.computeTangents();
84840
+ }
84841
+ catch {
84842
+ logger.debug(`Studio "${path}": tangent computation failed, anisotropy may have artifacts`);
84843
+ }
84844
+ }
84837
84845
  // Apply to ObjectGroup
84838
84846
  obj.enterStudioMode(studioMaterial instanceof MeshPhysicalMaterial ? studioMaterial : null, studioBack);
84839
84847
  }
@@ -87470,6 +87478,15 @@ class CenteredPlane extends Plane {
87470
87478
  const z = this.distanceToPoint(new Vector3(0, 0, 0));
87471
87479
  this.constant = z - c + value;
87472
87480
  }
87481
+ /**
87482
+ * Clone this CenteredPlane.
87483
+ * Overrides THREE.Plane.clone() which calls `new this.constructor()` without
87484
+ * arguments, causing `center` to be undefined during shadow map generation.
87485
+ */
87486
+ // @ts-expect-error -- THREE.Plane.clone() returns `this`, but we need a concrete CenteredPlane
87487
+ clone() {
87488
+ return new CenteredPlane(this.normal.clone(), this.centeredConstant, [...this.center]);
87489
+ }
87473
87490
  }
87474
87491
  // ============================================================================
87475
87492
  // PlaneMesh - Visual representation of a clipping plane
@@ -94284,7 +94301,7 @@ class Tools {
94284
94301
  }
94285
94302
  }
94286
94303
 
94287
- const version = "4.3.2";
94304
+ const version = "4.3.5";
94288
94305
 
94289
94306
  /**
94290
94307
  * Clean room environment for Studio mode PMREM generation.
@@ -95909,7 +95926,7 @@ class StudioFloor {
95909
95926
  * Create a shadow-receiving plane at the floor position.
95910
95927
  */
95911
95928
  _createShadowPlane(zPosition, sceneSize) {
95912
- const floorSize = sceneSize * 4;
95929
+ const floorSize = sceneSize * 6;
95913
95930
  const geometry = new PlaneGeometry(floorSize, floorSize);
95914
95931
  const material = new ShadowMaterial({ opacity: 0.5, depthWrite: false });
95915
95932
  const plane = new Mesh(geometry, material);
@@ -104756,7 +104773,7 @@ class StudioManager {
104756
104773
  light.position.copy(bboxCenter).addScaledVector(dir, maxExtent * 3);
104757
104774
  light.target.position.copy(bboxCenter);
104758
104775
  light.castShadow = true;
104759
- const frustumSize = maxExtent * 4.0;
104776
+ const frustumSize = maxExtent * 6.0;
104760
104777
  light.shadow.camera.left = -frustumSize;
104761
104778
  light.shadow.camera.right = frustumSize;
104762
104779
  light.shadow.camera.top = frustumSize;