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