three-cad-viewer 4.2.0 → 4.3.0

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.
@@ -82545,19 +82545,6 @@ const _sphere = new Sphere();
82545
82545
  // =============================================================================
82546
82546
  // Constants
82547
82547
  // =============================================================================
82548
- /** Size of procedurally generated builtin textures (pixels) */
82549
- const BUILTIN_SIZE = 256;
82550
- /** Names of all supported builtin procedural textures */
82551
- const BUILTIN_NAMES = [
82552
- "brushed",
82553
- "knurled",
82554
- "sandblasted",
82555
- "hammered",
82556
- "checker",
82557
- "wood-dark",
82558
- "leather",
82559
- "fabric-weave",
82560
- ];
82561
82548
  /**
82562
82549
  * Texture fields that carry sRGB color data.
82563
82550
  *
@@ -82586,431 +82573,6 @@ const THREEJS_SRGB_MAPS = new Set([
82586
82573
  "specularColorMap",
82587
82574
  ]);
82588
82575
  // =============================================================================
82589
- // Builtin Procedural Texture Generators
82590
- // =============================================================================
82591
- /**
82592
- * Create a 2D canvas context of the given size.
82593
- */
82594
- function createCanvas(size) {
82595
- // Prefer OffscreenCanvas when available (Web Workers, modern browsers)
82596
- if (typeof OffscreenCanvas !== "undefined") {
82597
- const canvas = new OffscreenCanvas(size, size);
82598
- const ctx = canvas.getContext("2d");
82599
- return { canvas, ctx };
82600
- }
82601
- const canvas = document.createElement("canvas");
82602
- canvas.width = size;
82603
- canvas.height = size;
82604
- const ctx = canvas.getContext("2d");
82605
- return { canvas, ctx };
82606
- }
82607
- /**
82608
- * Simple pseudo-random number generator (mulberry32) for deterministic output.
82609
- * Ensures builtin textures look identical across sessions.
82610
- */
82611
- function mulberry32(seed) {
82612
- return () => {
82613
- seed |= 0;
82614
- seed = (seed + 0x6d2b79f5) | 0;
82615
- let t = Math.imul(seed ^ (seed >>> 15), 1 | seed);
82616
- t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
82617
- return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
82618
- };
82619
- }
82620
- /**
82621
- * Generate a brushed-metal normal map.
82622
- *
82623
- * Creates horizontal noise streaks simulating directional brushing.
82624
- * The streaks run along the X axis with slight vertical variation.
82625
- */
82626
- function generateBrushed(size) {
82627
- const { canvas, ctx } = createCanvas(size);
82628
- const imageData = ctx.createImageData(size, size);
82629
- const data = imageData.data;
82630
- const rng = mulberry32(42);
82631
- // Generate per-row intensity variation (horizontal streaks)
82632
- const rowIntensity = new Float32Array(size);
82633
- for (let y = 0; y < size; y++) {
82634
- rowIntensity[y] = 0.3 + rng() * 0.7;
82635
- }
82636
- for (let y = 0; y < size; y++) {
82637
- for (let x = 0; x < size; x++) {
82638
- const idx = (y * size + x) * 4;
82639
- // High-frequency horizontal noise for brush lines
82640
- const noise = (rng() - 0.5) * 0.15 * rowIntensity[y];
82641
- // Slight vertical gradient perturbation
82642
- const yNoise = (rng() - 0.5) * 0.05;
82643
- // Normal map encoding: (0.5, 0.5, 1.0) = flat
82644
- data[idx] = Math.round((0.5 + noise) * 255); // R: tangent X
82645
- data[idx + 1] = Math.round((0.5 + yNoise) * 255); // G: tangent Y
82646
- data[idx + 2] = 255; // B: tangent Z (flat)
82647
- data[idx + 3] = 255; // A: opaque
82648
- }
82649
- }
82650
- ctx.putImageData(imageData, 0, 0);
82651
- return canvas;
82652
- }
82653
- /**
82654
- * Generate a diamond knurl pattern normal map.
82655
- *
82656
- * Creates a repeating diamond grid pattern typical of knurled metal surfaces.
82657
- */
82658
- function generateKnurled(size) {
82659
- const { canvas, ctx } = createCanvas(size);
82660
- const imageData = ctx.createImageData(size, size);
82661
- const data = imageData.data;
82662
- const diamonds = 16; // Number of diamond repeats
82663
- const step = size / diamonds;
82664
- for (let y = 0; y < size; y++) {
82665
- for (let x = 0; x < size; x++) {
82666
- const idx = (y * size + x) * 4;
82667
- // Diamond pattern using modular arithmetic
82668
- const dx = ((x % step) / step) * 2 - 1; // -1 to 1 within cell
82669
- const dy = ((y % step) / step) * 2 - 1;
82670
- // Diamond distance (L1 norm)
82671
- const dist = Math.abs(dx) + Math.abs(dy);
82672
- // Gradient direction for normal
82673
- const sx = dx > 0 ? 1 : -1;
82674
- const sy = dy > 0 ? 1 : -1;
82675
- // Smooth pyramid shape
82676
- const strength = 0.3;
82677
- const nx = dist < 1 ? sx * strength * (1 - dist) : 0;
82678
- const ny = dist < 1 ? sy * strength * (1 - dist) : 0;
82679
- data[idx] = Math.round((0.5 + nx) * 255);
82680
- data[idx + 1] = Math.round((0.5 + ny) * 255);
82681
- data[idx + 2] = 255;
82682
- data[idx + 3] = 255;
82683
- }
82684
- }
82685
- ctx.putImageData(imageData, 0, 0);
82686
- return canvas;
82687
- }
82688
- /**
82689
- * Generate a sandblasted surface normal map.
82690
- *
82691
- * Creates fine random grain using layered noise at different frequencies,
82692
- * simulating a sandblasted or bead-blasted metal surface.
82693
- */
82694
- function generateSandblasted(size) {
82695
- const { canvas, ctx } = createCanvas(size);
82696
- const imageData = ctx.createImageData(size, size);
82697
- const data = imageData.data;
82698
- const rng = mulberry32(137);
82699
- // Generate a height field with multi-octave noise
82700
- const heights = new Float32Array(size * size);
82701
- for (let i = 0; i < heights.length; i++) {
82702
- heights[i] = rng() * 0.5 + rng() * 0.3 + rng() * 0.2;
82703
- }
82704
- // Derive normals from height field via finite differences
82705
- const strength = 0.2;
82706
- for (let y = 0; y < size; y++) {
82707
- for (let x = 0; x < size; x++) {
82708
- const idx = (y * size + x) * 4;
82709
- const xp = (x + 1) % size;
82710
- const xm = (x - 1 + size) % size;
82711
- const yp = (y + 1) % size;
82712
- const ym = (y - 1 + size) % size;
82713
- const dhdx = (heights[y * size + xp] - heights[y * size + xm]) * 0.5;
82714
- const dhdy = (heights[yp * size + x] - heights[ym * size + x]) * 0.5;
82715
- data[idx] = Math.round((0.5 - dhdx * strength) * 255);
82716
- data[idx + 1] = Math.round((0.5 - dhdy * strength) * 255);
82717
- data[idx + 2] = 255;
82718
- data[idx + 3] = 255;
82719
- }
82720
- }
82721
- ctx.putImageData(imageData, 0, 0);
82722
- return canvas;
82723
- }
82724
- /**
82725
- * Generate a hammered/peened surface normal map.
82726
- *
82727
- * Creates random crater bumps simulating a hand-hammered metal surface.
82728
- */
82729
- function generateHammered(size) {
82730
- const { canvas, ctx } = createCanvas(size);
82731
- const imageData = ctx.createImageData(size, size);
82732
- const data = imageData.data;
82733
- const rng = mulberry32(314);
82734
- // Generate a height field with random circular craters
82735
- const heights = new Float32Array(size * size);
82736
- const craterCount = 80;
82737
- for (let c = 0; c < craterCount; c++) {
82738
- const cx = rng() * size;
82739
- const cy = rng() * size;
82740
- const radius = 8 + rng() * 16;
82741
- const depth = 0.3 + rng() * 0.7;
82742
- // Stamp crater (with wrapping for tileable texture)
82743
- const r2 = radius * radius;
82744
- for (let dy = -radius; dy <= radius; dy++) {
82745
- for (let dx = -radius; dx <= radius; dx++) {
82746
- const d2 = dx * dx + dy * dy;
82747
- if (d2 < r2) {
82748
- const px = ((Math.round(cx + dx) % size) + size) % size;
82749
- const py = ((Math.round(cy + dy) % size) + size) % size;
82750
- // Smooth hemisphere shape
82751
- const t = 1 - d2 / r2;
82752
- heights[py * size + px] += depth * t * t;
82753
- }
82754
- }
82755
- }
82756
- }
82757
- // Derive normals from height field
82758
- const strength = 0.25;
82759
- for (let y = 0; y < size; y++) {
82760
- for (let x = 0; x < size; x++) {
82761
- const idx = (y * size + x) * 4;
82762
- const xp = (x + 1) % size;
82763
- const xm = (x - 1 + size) % size;
82764
- const yp = (y + 1) % size;
82765
- const ym = (y - 1 + size) % size;
82766
- const dhdx = (heights[y * size + xp] - heights[y * size + xm]) * 0.5;
82767
- const dhdy = (heights[yp * size + x] - heights[ym * size + x]) * 0.5;
82768
- data[idx] = Math.round(Math.max(0, Math.min(255, (0.5 - dhdx * strength) * 255)));
82769
- data[idx + 1] = Math.round(Math.max(0, Math.min(255, (0.5 - dhdy * strength) * 255)));
82770
- data[idx + 2] = 255;
82771
- data[idx + 3] = 255;
82772
- }
82773
- }
82774
- ctx.putImageData(imageData, 0, 0);
82775
- return canvas;
82776
- }
82777
- /**
82778
- * Generate a black/white checkerboard texture.
82779
- *
82780
- * Useful for UV debugging and as a base color texture for testing.
82781
- */
82782
- function generateChecker(size) {
82783
- const { canvas, ctx } = createCanvas(size);
82784
- const squares = 8; // 8x8 checkerboard
82785
- const step = size / squares;
82786
- ctx.fillStyle = "#ffffff";
82787
- ctx.fillRect(0, 0, size, size);
82788
- ctx.fillStyle = "#000000";
82789
- for (let row = 0; row < squares; row++) {
82790
- for (let col = 0; col < squares; col++) {
82791
- if ((row + col) % 2 === 0) {
82792
- ctx.fillRect(col * step, row * step, step, step);
82793
- }
82794
- }
82795
- }
82796
- return canvas;
82797
- }
82798
- /**
82799
- * Generate a dark wood grain base color texture.
82800
- *
82801
- * Creates concentric growth rings with noise perturbation,
82802
- * in warm walnut/mahogany tones. Intended as a baseColorTexture (sRGB).
82803
- */
82804
- function generateWoodDark(size) {
82805
- const { canvas, ctx } = createCanvas(size);
82806
- const imageData = ctx.createImageData(size, size);
82807
- const data = imageData.data;
82808
- const rng = mulberry32(73);
82809
- // Pre-generate a noise field for grain perturbation
82810
- const noise = new Float32Array(size * size);
82811
- for (let i = 0; i < noise.length; i++) {
82812
- noise[i] = rng();
82813
- }
82814
- // Smooth the noise (simple box blur, 2 passes)
82815
- const tmp = new Float32Array(size * size);
82816
- for (let pass = 0; pass < 2; pass++) {
82817
- const src = pass === 0 ? noise : tmp;
82818
- const dst = pass === 0 ? tmp : noise;
82819
- const k = 3;
82820
- for (let y = 0; y < size; y++) {
82821
- for (let x = 0; x < size; x++) {
82822
- let sum = 0;
82823
- let count = 0;
82824
- for (let dy = -k; dy <= k; dy++) {
82825
- for (let dx = -k; dx <= k; dx++) {
82826
- const px = (x + dx + size) % size;
82827
- const py = (y + dy + size) % size;
82828
- sum += src[py * size + px];
82829
- count++;
82830
- }
82831
- }
82832
- dst[y * size + x] = sum / count;
82833
- }
82834
- }
82835
- }
82836
- // Wood color palette (sRGB, will be decoded by Three.js)
82837
- // Dark grain lines: ~[80, 45, 22] Light wood body: ~[145, 90, 48]
82838
- const darkR = 80, darkG = 45, darkB = 22;
82839
- const lightR = 145, lightG = 90, lightB = 48;
82840
- // Ring center (offset from image center for asymmetry)
82841
- const cx = size * 0.45;
82842
- const cy = size * 0.52;
82843
- const ringScale = 0.08; // Controls ring spacing
82844
- for (let y = 0; y < size; y++) {
82845
- for (let x = 0; x < size; x++) {
82846
- const idx = (y * size + x) * 4;
82847
- // Distance from center with noise distortion
82848
- const n = noise[y * size + x];
82849
- const dx = x - cx + (n - 0.5) * 30;
82850
- const dy = y - cy + (n - 0.5) * 15;
82851
- const dist = Math.sqrt(dx * dx + dy * dy);
82852
- // Growth ring pattern (sinusoidal)
82853
- const ring = Math.sin(dist * ringScale * Math.PI * 2);
82854
- // Remap from [-1,1] to [0,1]
82855
- const t = ring * 0.5 + 0.5;
82856
- // sqrt biases toward light — dark lines stay thin, brown dominates
82857
- const ringFactor = Math.sqrt(t);
82858
- // Add fine-grain noise for fiber texture
82859
- const fineNoise = (rng() - 0.5) * 12;
82860
- // Interpolate between dark and light
82861
- const r = Math.round(darkR + (lightR - darkR) * ringFactor + fineNoise);
82862
- const g = Math.round(darkG + (lightG - darkG) * ringFactor + fineNoise * 0.6);
82863
- const b = Math.round(darkB + (lightB - darkB) * ringFactor + fineNoise * 0.3);
82864
- data[idx] = Math.max(0, Math.min(255, r));
82865
- data[idx + 1] = Math.max(0, Math.min(255, g));
82866
- data[idx + 2] = Math.max(0, Math.min(255, b));
82867
- data[idx + 3] = 255;
82868
- }
82869
- }
82870
- ctx.putImageData(imageData, 0, 0);
82871
- return canvas;
82872
- }
82873
- /**
82874
- * Generate a leather pebble-grain normal map.
82875
- *
82876
- * Creates irregular rounded bumps (Voronoi-like cells) typical of
82877
- * top-grain or pebbled leather. Each cell has a smooth dome shape
82878
- * with slight creases between cells.
82879
- */
82880
- function generateLeather(size) {
82881
- const { canvas, ctx } = createCanvas(size);
82882
- const imageData = ctx.createImageData(size, size);
82883
- const data = imageData.data;
82884
- const rng = mulberry32(217);
82885
- // Scatter seed points for Voronoi cells (pebbles)
82886
- const cellCount = 180;
82887
- const seeds = [];
82888
- for (let i = 0; i < cellCount; i++) {
82889
- seeds.push({ x: rng() * size, y: rng() * size });
82890
- }
82891
- // Build a height field from Voronoi distance
82892
- const heights = new Float32Array(size * size);
82893
- for (let y = 0; y < size; y++) {
82894
- for (let x = 0; x < size; x++) {
82895
- // Find distance to nearest seed (with wrapping for tileability)
82896
- let minDist = Infinity;
82897
- for (const s of seeds) {
82898
- let dx = Math.abs(x - s.x);
82899
- let dy = Math.abs(y - s.y);
82900
- if (dx > size / 2)
82901
- dx = size - dx;
82902
- if (dy > size / 2)
82903
- dy = size - dy;
82904
- const d = Math.sqrt(dx * dx + dy * dy);
82905
- if (d < minDist)
82906
- minDist = d;
82907
- }
82908
- // Invert: closer to seed = higher (dome center)
82909
- // Normalize roughly by expected average cell radius
82910
- const avgRadius = size / Math.sqrt(cellCount);
82911
- const t = Math.min(minDist / avgRadius, 1.0);
82912
- // Smooth dome falloff: 1 at center, 0 at edge
82913
- heights[y * size + x] = (1 - t * t) * 0.8 + rng() * 0.05;
82914
- }
82915
- }
82916
- // Derive normals from height field via finite differences
82917
- const strength = 0.35;
82918
- for (let y = 0; y < size; y++) {
82919
- for (let x = 0; x < size; x++) {
82920
- const idx = (y * size + x) * 4;
82921
- const xp = (x + 1) % size;
82922
- const xm = (x - 1 + size) % size;
82923
- const yp = (y + 1) % size;
82924
- const ym = (y - 1 + size) % size;
82925
- const dhdx = (heights[y * size + xp] - heights[y * size + xm]) * 0.5;
82926
- const dhdy = (heights[yp * size + x] - heights[ym * size + x]) * 0.5;
82927
- data[idx] = Math.round(Math.max(0, Math.min(255, (0.5 - dhdx * strength) * 255)));
82928
- data[idx + 1] = Math.round(Math.max(0, Math.min(255, (0.5 - dhdy * strength) * 255)));
82929
- data[idx + 2] = 255;
82930
- data[idx + 3] = 255;
82931
- }
82932
- }
82933
- ctx.putImageData(imageData, 0, 0);
82934
- return canvas;
82935
- }
82936
- /**
82937
- * Generate a fabric twill-weave normal map.
82938
- *
82939
- * Creates a repeating over-under weave pattern with slightly raised
82940
- * warp/weft threads and recessed gaps, typical of upholstery fabric.
82941
- */
82942
- function generateFabricWeave(size) {
82943
- const { canvas, ctx } = createCanvas(size);
82944
- const imageData = ctx.createImageData(size, size);
82945
- const data = imageData.data;
82946
- const rng = mulberry32(159);
82947
- // Thread parameters
82948
- const threadCount = 32; // threads per axis
82949
- const cellSize = size / threadCount;
82950
- // Build height field: each cell is either warp-over or weft-over
82951
- const heights = new Float32Array(size * size);
82952
- for (let y = 0; y < size; y++) {
82953
- for (let x = 0; x < size; x++) {
82954
- const cx = Math.floor(x / cellSize);
82955
- const cy = Math.floor(y / cellSize);
82956
- // Position within the cell [0, 1]
82957
- const lx = (x % cellSize) / cellSize;
82958
- const ly = (y % cellSize) / cellSize;
82959
- // Twill pattern: diagonal shift (2/1 twill)
82960
- const isWarpOver = ((cx + cy) % 3) < 2;
82961
- // Thread profile: rounded bump across the thread width
82962
- // Warp threads run vertically (bump shape across x)
82963
- // Weft threads run horizontally (bump shape across y)
82964
- const warpProfile = Math.sin(lx * Math.PI);
82965
- const weftProfile = Math.sin(ly * Math.PI);
82966
- // Gap between threads (edges of cells are lower)
82967
- const edgeFalloff = Math.min(Math.sin(lx * Math.PI), Math.sin(ly * Math.PI));
82968
- let h;
82969
- if (isWarpOver) {
82970
- // Warp on top: height from warp profile
82971
- h = 0.5 + warpProfile * 0.4 * edgeFalloff;
82972
- }
82973
- else {
82974
- // Weft on top: height from weft profile
82975
- h = 0.5 + weftProfile * 0.4 * edgeFalloff;
82976
- }
82977
- // Add subtle noise for fabric irregularity
82978
- h += (rng() - 0.5) * 0.04;
82979
- heights[y * size + x] = h;
82980
- }
82981
- }
82982
- // Derive normals from height field
82983
- const strength = 0.3;
82984
- for (let y = 0; y < size; y++) {
82985
- for (let x = 0; x < size; x++) {
82986
- const idx = (y * size + x) * 4;
82987
- const xp = (x + 1) % size;
82988
- const xm = (x - 1 + size) % size;
82989
- const yp = (y + 1) % size;
82990
- const ym = (y - 1 + size) % size;
82991
- const dhdx = (heights[y * size + xp] - heights[y * size + xm]) * 0.5;
82992
- const dhdy = (heights[yp * size + x] - heights[ym * size + x]) * 0.5;
82993
- data[idx] = Math.round(Math.max(0, Math.min(255, (0.5 - dhdx * strength) * 255)));
82994
- data[idx + 1] = Math.round(Math.max(0, Math.min(255, (0.5 - dhdy * strength) * 255)));
82995
- data[idx + 2] = 255;
82996
- data[idx + 3] = 255;
82997
- }
82998
- }
82999
- ctx.putImageData(imageData, 0, 0);
83000
- return canvas;
83001
- }
83002
- /** Map of builtin texture names to their generator functions */
83003
- const BUILTIN_GENERATORS = {
83004
- brushed: generateBrushed,
83005
- knurled: generateKnurled,
83006
- sandblasted: generateSandblasted,
83007
- hammered: generateHammered,
83008
- checker: generateChecker,
83009
- "wood-dark": generateWoodDark,
83010
- leather: generateLeather,
83011
- "fabric-weave": generateFabricWeave,
83012
- };
83013
- // =============================================================================
83014
82576
  // TextureCache
83015
82577
  // =============================================================================
83016
82578
  /**
@@ -83021,28 +82583,20 @@ const BUILTIN_GENERATORS = {
83021
82583
  * Only TextureCache.dispose() / disposeFull() disposes GPU texture resources.
83022
82584
  *
83023
82585
  * Resolution order for texture reference strings:
83024
- * 1. `builtin:` prefix -- procedural texture from the persistent builtin cache
83025
- * 2. Key in the root-level `textures` table -- embedded data or URL
83026
- * 3. `data:` prefix -- treat as data URI, load directly
83027
- * 4. Otherwise -- treat as URL, resolve relative to HTML page
82586
+ * 1. `data:` prefix -- treat as data URI, load directly
82587
+ * 2. Otherwise -- treat as URL, resolve relative to HTML page
83028
82588
  *
83029
82589
  * Features:
83030
- * - Two-tier caching: user textures (disposed on clear) + builtin textures
83031
- * (persistent, only disposed on viewer teardown)
83032
82590
  * - In-flight promise deduplication (no duplicate loads for the same key)
83033
82591
  * - Correct colorSpace assignment per texture semantic role
83034
82592
  * - GPU resource tracking via gpuTracker
83035
82593
  */
83036
82594
  class TextureCache {
83037
82595
  constructor() {
83038
- /** User textures cache (disposed on clear/dispose, rebuilt per shape data) */
82596
+ /** Textures cache (disposed on clear/dispose, rebuilt per shape data) */
83039
82597
  this._cache = new Map();
83040
- /** Built-in procedural textures (persistent, only disposed via disposeFull) */
83041
- this._builtinCache = new Map();
83042
82598
  /** In-flight load promises keyed by cache key */
83043
82599
  this._inflight = new Map();
83044
- /** Root-level textures table from shape data */
83045
- this._texturesTable = {};
83046
82600
  /** THREE.TextureLoader instance (created lazily) */
83047
82601
  this._textureLoader = null;
83048
82602
  /** Whether this cache has been fully disposed */
@@ -83051,22 +82605,11 @@ class TextureCache {
83051
82605
  // ---------------------------------------------------------------------------
83052
82606
  // Public API
83053
82607
  // ---------------------------------------------------------------------------
83054
- /**
83055
- * Set or update the root-level textures table.
83056
- *
83057
- * Called when new shape data is loaded. The table maps string keys to
83058
- * TextureEntry objects (embedded base64 data or URL references).
83059
- *
83060
- * @param table - The textures table from root Shapes node, or undefined to clear
83061
- */
83062
- setTexturesTable(table) {
83063
- this._texturesTable = table ?? {};
83064
- }
83065
82608
  /**
83066
82609
  * Resolve a texture reference string and return a cached or newly loaded
83067
82610
  * THREE.Texture with the correct colorSpace set.
83068
82611
  *
83069
- * @param ref - Texture reference string (builtin name, table key, data URI, or URL)
82612
+ * @param ref - Texture reference string (table key, data URI, or URL)
83070
82613
  * @param textureRole - The texture role name (MaterialAppearance field name or proxy role)
83071
82614
  * (e.g. "baseColorTexture", "normalTexture"). Used to determine colorSpace.
83072
82615
  * @returns The resolved THREE.Texture, or null if the reference is invalid
@@ -83084,148 +82627,41 @@ class TextureCache {
83084
82627
  const colorSpace = SRGB_TEXTURE_ROLES.has(textureRole)
83085
82628
  ? SRGBColorSpace
83086
82629
  : LinearSRGBColorSpace;
83087
- // 1. builtin: prefix
83088
- if (ref.startsWith("builtin:")) {
83089
- return this._getBuiltin(ref, colorSpace);
83090
- }
83091
- // 2. Look up in textures table
83092
- const tableEntry = this._texturesTable[ref];
83093
- if (tableEntry) {
83094
- return this._getFromTable(ref, tableEntry, colorSpace);
83095
- }
83096
- // 3. data: prefix (data URI)
82630
+ // 1. data: prefix (data URI)
83097
82631
  if (ref.startsWith("data:")) {
83098
82632
  return this._getFromDataUri(ref, colorSpace);
83099
82633
  }
83100
- // 4. URL (relative to HTML page)
82634
+ // 2. URL (relative to HTML page)
83101
82635
  return this._getFromUrl(ref, colorSpace);
83102
82636
  }
83103
82637
  /**
83104
- * Check whether a texture reference string would resolve to a builtin texture.
83105
- */
83106
- isBuiltin(ref) {
83107
- return ref.startsWith("builtin:");
83108
- }
83109
- /**
83110
- * Dispose user textures (called on viewer.clear() when shape data is replaced).
82638
+ * Dispose textures (called on viewer.clear() when shape data is replaced).
83111
82639
  *
83112
- * Disposes all textures in the user cache and clears in-flight promises.
83113
- * The builtin procedural texture cache is preserved.
82640
+ * Disposes all textures in the cache and clears in-flight promises.
83114
82641
  */
83115
82642
  dispose() {
83116
- // Dispose user textures
83117
82643
  for (const [key, texture] of this._cache) {
83118
82644
  gpuTracker.untrack("texture", texture);
83119
82645
  texture.dispose();
83120
- logger.debug(`Disposed user texture: ${key}`);
82646
+ logger.debug(`Disposed texture: ${key}`);
83121
82647
  }
83122
82648
  this._cache.clear();
83123
82649
  // Clear in-flight promises (they may resolve but won't be used)
83124
82650
  this._inflight.clear();
83125
- // Clear the textures table (will be set again with new shape data)
83126
- this._texturesTable = {};
83127
82651
  }
83128
82652
  /**
83129
- * Dispose all textures including builtin procedural textures.
82653
+ * Dispose all textures.
83130
82654
  *
83131
82655
  * Called on viewer.dispose() when the viewer is fully torn down.
83132
82656
  * After this call, the TextureCache cannot be used again.
83133
82657
  */
83134
82658
  disposeFull() {
83135
82659
  this._disposed = true;
83136
- // Dispose user textures first
83137
82660
  this.dispose();
83138
- // Dispose builtin textures
83139
- for (const [key, texture] of this._builtinCache) {
83140
- gpuTracker.untrack("texture", texture);
83141
- texture.dispose();
83142
- logger.debug(`Disposed builtin texture: ${key}`);
83143
- }
83144
- this._builtinCache.clear();
83145
- // Null the texture loader reference
83146
82661
  this._textureLoader = null;
83147
82662
  logger.debug("TextureCache fully disposed");
83148
82663
  }
83149
82664
  // ---------------------------------------------------------------------------
83150
- // Private: Builtin procedural textures
83151
- // ---------------------------------------------------------------------------
83152
- /**
83153
- * Get or generate a builtin procedural texture.
83154
- *
83155
- * Builtin textures are cached in the persistent `_builtinCache` and survive
83156
- * `dispose()` calls. They are only freed via `disposeFull()`.
83157
- */
83158
- _getBuiltin(ref, colorSpace) {
83159
- // Extract name after "builtin:" prefix
83160
- const name = ref.slice(8);
83161
- if (!BUILTIN_GENERATORS[name]) {
83162
- logger.warn(`Unknown builtin texture: "${ref}". Available: ${BUILTIN_NAMES.join(", ")}`);
83163
- return null;
83164
- }
83165
- // Check builtin cache
83166
- const cached = this._builtinCache.get(ref);
83167
- if (cached) {
83168
- // TODO: If the same builtin is used in different roles (sRGB vs Linear),
83169
- // mutating colorSpace in-place would affect other materials. This is
83170
- // unlikely for builtins (almost always normal maps) but could be fixed
83171
- // with a composite cache key (ref + colorSpace) if needed.
83172
- cached.colorSpace = colorSpace;
83173
- return cached;
83174
- }
83175
- // Generate the procedural texture
83176
- const canvas = BUILTIN_GENERATORS[name](BUILTIN_SIZE);
83177
- const texture = new CanvasTexture(canvas);
83178
- texture.wrapS = RepeatWrapping;
83179
- texture.wrapT = RepeatWrapping;
83180
- texture.colorSpace = colorSpace;
83181
- texture.needsUpdate = true;
83182
- // Cache in the persistent builtin cache
83183
- this._builtinCache.set(ref, texture);
83184
- gpuTracker.trackTexture(texture, `Builtin procedural texture: ${ref}`);
83185
- logger.debug(`Generated builtin texture: ${ref}`);
83186
- return texture;
83187
- }
83188
- // ---------------------------------------------------------------------------
83189
- // Private: Table entry loading
83190
- // ---------------------------------------------------------------------------
83191
- /**
83192
- * Load a texture from the root-level textures table entry.
83193
- *
83194
- * Handles both embedded (base64 data) and URL-referenced entries.
83195
- */
83196
- async _getFromTable(key, entry, colorSpace) {
83197
- // Check user cache
83198
- // TODO: If the same texture table entry is used in different roles
83199
- // (sRGB vs Linear), mutating colorSpace in-place would affect other
83200
- // materials. Could be fixed with a composite cache key (key + colorSpace).
83201
- const cached = this._cache.get(key);
83202
- if (cached) {
83203
- cached.colorSpace = colorSpace;
83204
- return cached;
83205
- }
83206
- // Check in-flight
83207
- const inflight = this._inflight.get(key);
83208
- if (inflight) {
83209
- const texture = await inflight;
83210
- texture.colorSpace = colorSpace;
83211
- return texture;
83212
- }
83213
- // Resolve table entry to a loadable source
83214
- if (entry.data && entry.format) {
83215
- // Embedded: construct data URI from base64 data
83216
- const mimeType = this._formatToMime(entry.format);
83217
- const dataUri = `data:${mimeType};base64,${entry.data}`;
83218
- return this._loadAndCache(key, dataUri, colorSpace);
83219
- }
83220
- if (entry.url) {
83221
- // URL reference
83222
- return this._loadAndCache(key, entry.url, colorSpace);
83223
- }
83224
- // Invalid entry (neither data nor url)
83225
- logger.warn(`Texture table entry "${key}" has neither data nor url, skipping`);
83226
- return null;
83227
- }
83228
- // ---------------------------------------------------------------------------
83229
82665
  // Private: Data URI loading
83230
82666
  // ---------------------------------------------------------------------------
83231
82667
  /**
@@ -83348,24 +82784,6 @@ class TextureCache {
83348
82784
  }
83349
82785
  return this._textureLoader;
83350
82786
  }
83351
- /**
83352
- * Convert a format string (e.g. "png", "jpg", "webp") to a MIME type.
83353
- */
83354
- _formatToMime(format) {
83355
- switch (format.toLowerCase()) {
83356
- case "jpg":
83357
- case "jpeg":
83358
- return "image/jpeg";
83359
- case "png":
83360
- return "image/png";
83361
- case "webp":
83362
- return "image/webp";
83363
- default:
83364
- // Default to PNG for unknown formats
83365
- logger.warn(`Unknown texture format "${format}", defaulting to image/png`);
83366
- return "image/png";
83367
- }
83368
- }
83369
82787
  }
83370
82788
  /**
83371
82789
  * Get the correct color space for a Three.js material map property name.
@@ -83752,6 +83170,9 @@ class MaterialFactory {
83752
83170
  const [r, g, b] = prop.value;
83753
83171
  matOptions[key] = new Color(r, g, b);
83754
83172
  }
83173
+ else if ((key === "normalScale" || key === "clearcoatNormalScale") && Array.isArray(prop.value)) {
83174
+ matOptions[key] = new Vector2(prop.value[0], prop.value[1]);
83175
+ }
83755
83176
  else if (key === "iridescenceThicknessRange" && Array.isArray(prop.value)) {
83756
83177
  matOptions[key] = prop.value;
83757
83178
  }
@@ -84196,18 +83617,6 @@ const MATERIAL_PRESETS = {
84196
83617
  // ---------------------------------------------------------------------------
84197
83618
  // Natural / Other
84198
83619
  // ---------------------------------------------------------------------------
84199
- "wood-light": {
84200
- name: "Wood (Light)",
84201
- color: [0.89, 0.8, 0.68, 1],
84202
- metalness: 0.0,
84203
- roughness: 0.6,
84204
- },
84205
- "wood-dark": {
84206
- name: "Wood (Dark)",
84207
- color: [0.63, 0.51, 0.38, 1],
84208
- metalness: 0.0,
84209
- roughness: 0.55,
84210
- },
84211
83620
  "ceramic-white": {
84212
83621
  name: "Ceramic (White)",
84213
83622
  color: [0.98, 0.98, 0.97, 1],
@@ -84629,7 +84038,6 @@ class NestedGroup {
84629
84038
  this.bsphere = null;
84630
84039
  this.groups = {};
84631
84040
  this.clipPlanes = null;
84632
- this.texturesTable = null;
84633
84041
  this.materialsTable = null;
84634
84042
  this.resolvedMaterials = new Map();
84635
84043
  this.resolvedMaterialX = new Map();
@@ -84660,7 +84068,6 @@ class NestedGroup {
84660
84068
  this._disposeStudioResources();
84661
84069
  this.resolvedMaterials.clear();
84662
84070
  this.resolvedMaterialX.clear();
84663
- this.texturesTable = null;
84664
84071
  this.materialsTable = null;
84665
84072
  }
84666
84073
  /**
@@ -85115,7 +84522,6 @@ class NestedGroup {
85115
84522
  if (this.shapes.format == "GDS") {
85116
84523
  this.instances = this.shapes.instances || null;
85117
84524
  }
85118
- this.texturesTable = this.shapes.textures || null;
85119
84525
  this.materialsTable = this.shapes.materials || null;
85120
84526
  this.resolvedMaterials.clear();
85121
84527
  this.resolvedMaterialX.clear();
@@ -85317,7 +84723,6 @@ class NestedGroup {
85317
84723
  if (!this._textureCache) {
85318
84724
  this._textureCache = new TextureCache();
85319
84725
  }
85320
- this._textureCache.setTexturesTable(this.texturesTable ?? undefined);
85321
84726
  // Track material tags that failed to resolve
85322
84727
  const unresolvedTags = new Set();
85323
84728
  // Iterate all ObjectGroups with front meshes
@@ -94874,7 +94279,7 @@ class Tools {
94874
94279
  }
94875
94280
  }
94876
94281
 
94877
- const version = "4.2.0";
94282
+ const version = "4.3.0";
94878
94283
 
94879
94284
  /**
94880
94285
  * Clean room environment for Studio mode PMREM generation.
@@ -104953,7 +104358,6 @@ class StudioManager {
104953
104358
  this._composer = null;
104954
104359
  this._active = false;
104955
104360
  this._savedClippingState = null;
104956
- this._savedViewState = null;
104957
104361
  this._shadowLights = [];
104958
104362
  // -------------------------------------------------------------------------
104959
104363
  // Mode enter/leave
@@ -104972,14 +104376,7 @@ class StudioManager {
104972
104376
  if (clipping.planeHelpers) {
104973
104377
  clipping.planeHelpers.visible = false;
104974
104378
  }
104975
- // 2. Save view state (applied after env is loaded to avoid
104976
- // subscriber side-effects before the texture is ready)
104977
- this._savedViewState = {
104978
- ortho: state.get("ortho"),
104979
- axes: state.get("axes"),
104980
- grid: [...state.get("grid")],
104981
- };
104982
- // 3. Build/swap studio materials (async due to textures)
104379
+ // 2. Build/swap studio materials (async due to textures)
104983
104380
  const nestedGroup = this._ctx.getNestedGroup();
104984
104381
  const unresolvedTags = await nestedGroup.enterStudioMode(state.get("studioTextureMapping"));
104985
104382
  if (!this._active)
@@ -104988,23 +104385,15 @@ class StudioManager {
104988
104385
  if (unresolvedTags.length > 0) {
104989
104386
  this._ctx.dispatchEvent(new CustomEvent("tcv-material-warnings", { detail: unresolvedTags }));
104990
104387
  }
104991
- // 4. Load environment map
104388
+ // 3. Load environment map
104992
104389
  const envName = state.get("studioEnvironment");
104993
104390
  await this.envManager.loadEnvironment(envName, renderer);
104994
104391
  if (!this._active)
104995
104392
  return;
104996
- // 5. Apply ALL rendering changes atomically
104393
+ // 4. Apply ALL rendering changes atomically
104997
104394
  const scene = this._ctx.getScene();
104998
104395
  const camera = this._ctx.getCamera();
104999
104396
  this.envManager.apply(scene, state.get("studioEnvIntensity"), state.get("studioBackground"), state.get("up") === "Z", camera.ortho, state.get("studioEnvRotation"));
105000
- // 6. Override camera, axes, grid (after env is loaded so
105001
- // ortho subscriber doesn't trigger reapplyEnv with null texture)
105002
- if (this._savedViewState?.ortho)
105003
- this._ctx.setOrtho(false);
105004
- if (this._savedViewState?.axes)
105005
- this._ctx.setAxes(false);
105006
- if (state.get("grid").some(Boolean))
105007
- this._ctx.setGrids([false, false, false]);
105008
104397
  // Lighting: disable CAD lights; environment IBL provides all illumination
105009
104398
  this._ctx.getAmbientLight().intensity = 0;
105010
104399
  this._ctx.getDirectLight().intensity = 0;
@@ -105066,20 +104455,8 @@ class StudioManager {
105066
104455
  this._ctx.getClipping().restoreState(this._savedClippingState);
105067
104456
  this._savedClippingState = null;
105068
104457
  }
105069
- // 7. Clear active flag (before restoring view state, so subscribers don't re-apply studio)
104458
+ // 7. Clear active flag; edges restored by ObjectGroup.leaveStudioMode()
105070
104459
  this._active = false;
105071
- // 8. Restore camera, axes, grid
105072
- if (this._savedViewState) {
105073
- const { ortho, axes, grid } = this._savedViewState;
105074
- if (ortho)
105075
- this._ctx.setOrtho(true);
105076
- if (axes)
105077
- this._ctx.setAxes(true);
105078
- if (grid.some(Boolean))
105079
- this._ctx.setGrids(grid);
105080
- this._savedViewState = null;
105081
- }
105082
- // 9. Edges restored by ObjectGroup.leaveStudioMode()
105083
104460
  this._ctx.update(true, false);
105084
104461
  };
105085
104462
  this.resetStudio = () => {