@pascal-app/viewer 0.9.1 → 1.0.0-beta.1

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.
Files changed (206) hide show
  1. package/README.md +20 -5
  2. package/dist/components/error-boundary.d.ts +6 -0
  3. package/dist/components/error-boundary.d.ts.map +1 -1
  4. package/dist/components/error-boundary.js +6 -0
  5. package/dist/components/renderers/node-renderer.d.ts +5 -1
  6. package/dist/components/renderers/node-renderer.d.ts.map +1 -1
  7. package/dist/components/renderers/node-renderer.js +5 -2
  8. package/dist/components/renderers/parametric-node-renderer.d.ts.map +1 -1
  9. package/dist/components/renderers/parametric-node-renderer.js +7 -4
  10. package/dist/components/viewer/bvh-ecctrl.d.ts +77 -0
  11. package/dist/components/viewer/bvh-ecctrl.d.ts.map +1 -0
  12. package/dist/components/viewer/bvh-ecctrl.js +552 -0
  13. package/dist/components/viewer/frame-limiter.d.ts.map +1 -1
  14. package/dist/components/viewer/frame-limiter.js +59 -4
  15. package/dist/components/viewer/glb-interactive.d.ts +41 -0
  16. package/dist/components/viewer/glb-interactive.d.ts.map +1 -0
  17. package/dist/components/viewer/glb-interactive.js +394 -0
  18. package/dist/components/viewer/glb-reference-nodes.d.ts +29 -0
  19. package/dist/components/viewer/glb-reference-nodes.d.ts.map +1 -0
  20. package/dist/components/viewer/glb-reference-nodes.js +80 -0
  21. package/dist/components/viewer/glb-replace-instances.d.ts +18 -0
  22. package/dist/components/viewer/glb-replace-instances.d.ts.map +1 -0
  23. package/dist/components/viewer/glb-replace-instances.js +61 -0
  24. package/dist/components/viewer/glb-scene.d.ts +54 -0
  25. package/dist/components/viewer/glb-scene.d.ts.map +1 -0
  26. package/dist/components/viewer/glb-scene.js +920 -0
  27. package/dist/components/viewer/glb-walkthrough-controller.d.ts +22 -0
  28. package/dist/components/viewer/glb-walkthrough-controller.d.ts.map +1 -0
  29. package/dist/components/viewer/glb-walkthrough-controller.js +445 -0
  30. package/dist/components/viewer/index.d.ts +19 -2
  31. package/dist/components/viewer/index.d.ts.map +1 -1
  32. package/dist/components/viewer/index.js +113 -12
  33. package/dist/components/viewer/lights.d.ts.map +1 -1
  34. package/dist/components/viewer/lights.js +36 -5
  35. package/dist/components/viewer/perf-monitor.d.ts.map +1 -1
  36. package/dist/components/viewer/perf-monitor.js +17 -2
  37. package/dist/components/viewer/post-processing.d.ts +7 -1
  38. package/dist/components/viewer/post-processing.d.ts.map +1 -1
  39. package/dist/components/viewer/post-processing.js +180 -24
  40. package/dist/components/viewer/registered-systems.d.ts.map +1 -1
  41. package/dist/components/viewer/registered-systems.js +6 -4
  42. package/dist/components/viewer/scene-bvh.d.ts +2 -1
  43. package/dist/components/viewer/scene-bvh.d.ts.map +1 -1
  44. package/dist/components/viewer/scene-bvh.js +3 -0
  45. package/dist/components/viewer/scene-bvh.test.d.ts +2 -0
  46. package/dist/components/viewer/scene-bvh.test.d.ts.map +1 -0
  47. package/dist/components/viewer/scene-bvh.test.js +13 -0
  48. package/dist/components/viewer/scene-environment.d.ts +2 -0
  49. package/dist/components/viewer/scene-environment.d.ts.map +1 -0
  50. package/dist/components/viewer/scene-environment.js +79 -0
  51. package/dist/components/viewer/selection-manager.d.ts.map +1 -1
  52. package/dist/components/viewer/selection-manager.js +23 -12
  53. package/dist/hooks/use-gltf-ktx2.d.ts.map +1 -1
  54. package/dist/hooks/use-gltf-ktx2.js +2 -24
  55. package/dist/hooks/use-node-events.d.ts +1 -1
  56. package/dist/hooks/use-node-events.d.ts.map +1 -1
  57. package/dist/hooks/use-node-events.js +22 -14
  58. package/dist/index.d.ts +24 -10
  59. package/dist/index.d.ts.map +1 -1
  60. package/dist/index.js +29 -13
  61. package/dist/lib/backdrop.d.ts +33 -0
  62. package/dist/lib/backdrop.d.ts.map +1 -0
  63. package/dist/lib/backdrop.js +92 -0
  64. package/dist/lib/box-uv.d.ts +13 -0
  65. package/dist/lib/box-uv.d.ts.map +1 -0
  66. package/dist/lib/box-uv.js +32 -0
  67. package/dist/lib/csg-utils.d.ts +3 -9
  68. package/dist/lib/csg-utils.d.ts.map +1 -1
  69. package/dist/lib/csg-utils.js +65 -5
  70. package/dist/lib/csg-utils.test.d.ts +2 -0
  71. package/dist/lib/csg-utils.test.d.ts.map +1 -0
  72. package/dist/lib/csg-utils.test.js +45 -0
  73. package/dist/lib/edge-style.d.ts +1 -0
  74. package/dist/lib/edge-style.d.ts.map +1 -1
  75. package/dist/lib/edge-style.js +26 -6
  76. package/dist/lib/hero-pose.d.ts +47 -0
  77. package/dist/lib/hero-pose.d.ts.map +1 -0
  78. package/dist/lib/hero-pose.js +227 -0
  79. package/dist/lib/ink-edges.d.ts +1 -1
  80. package/dist/lib/ink-edges.d.ts.map +1 -1
  81. package/dist/lib/ink-edges.js +13 -7
  82. package/dist/lib/ktx2-loader.d.ts +46 -0
  83. package/dist/lib/ktx2-loader.d.ts.map +1 -0
  84. package/dist/lib/ktx2-loader.js +142 -0
  85. package/dist/lib/layers.d.ts +10 -0
  86. package/dist/lib/layers.d.ts.map +1 -1
  87. package/dist/lib/layers.js +10 -0
  88. package/dist/lib/materials.d.ts +16 -1
  89. package/dist/lib/materials.d.ts.map +1 -1
  90. package/dist/lib/materials.js +158 -34
  91. package/dist/lib/materials.test.d.ts +2 -0
  92. package/dist/lib/materials.test.d.ts.map +1 -0
  93. package/dist/lib/materials.test.js +25 -0
  94. package/dist/lib/merged-outline-node.test.d.ts +2 -0
  95. package/dist/lib/merged-outline-node.test.d.ts.map +1 -0
  96. package/dist/lib/merged-outline-node.test.js +22 -0
  97. package/dist/lib/scene-themes.d.ts +1 -0
  98. package/dist/lib/scene-themes.d.ts.map +1 -1
  99. package/dist/lib/scene-themes.js +25 -16
  100. package/dist/lib/shadow-only.d.ts +4 -0
  101. package/dist/lib/shadow-only.d.ts.map +1 -0
  102. package/dist/lib/shadow-only.js +36 -0
  103. package/dist/lib/snapshot-pipeline.d.ts +42 -0
  104. package/dist/lib/snapshot-pipeline.d.ts.map +1 -0
  105. package/dist/lib/snapshot-pipeline.js +254 -0
  106. package/dist/lib/terrain-perimeter-fill.d.ts +7 -0
  107. package/dist/lib/terrain-perimeter-fill.d.ts.map +1 -0
  108. package/dist/lib/terrain-perimeter-fill.js +75 -0
  109. package/dist/lib/texture-node-guard.d.ts +13 -0
  110. package/dist/lib/texture-node-guard.d.ts.map +1 -0
  111. package/dist/lib/texture-node-guard.js +39 -0
  112. package/dist/lib/texture-reference.d.ts +31 -0
  113. package/dist/lib/texture-reference.d.ts.map +1 -0
  114. package/dist/lib/texture-reference.js +139 -0
  115. package/dist/lib/texture-reference.test.d.ts +2 -0
  116. package/dist/lib/texture-reference.test.d.ts.map +1 -0
  117. package/dist/lib/texture-reference.test.js +86 -0
  118. package/dist/lib/tsl-compat.d.ts +12 -0
  119. package/dist/lib/tsl-compat.d.ts.map +1 -0
  120. package/dist/lib/tsl-compat.js +13 -0
  121. package/dist/store/use-viewer.d.ts +73 -18
  122. package/dist/store/use-viewer.d.ts.map +1 -1
  123. package/dist/store/use-viewer.js +191 -4
  124. package/dist/store/use-viewer.test.d.ts +2 -0
  125. package/dist/store/use-viewer.test.d.ts.map +1 -0
  126. package/dist/store/use-viewer.test.js +61 -0
  127. package/dist/systems/ceiling/ceiling-system.d.ts.map +1 -1
  128. package/dist/systems/ceiling/ceiling-system.js +12 -5
  129. package/dist/systems/door/door-animation.test.d.ts +2 -0
  130. package/dist/systems/door/door-animation.test.d.ts.map +1 -0
  131. package/dist/systems/door/door-animation.test.js +51 -0
  132. package/dist/systems/door/door-floor-alignment.test.d.ts +2 -0
  133. package/dist/systems/door/door-floor-alignment.test.d.ts.map +1 -0
  134. package/dist/systems/door/door-floor-alignment.test.js +50 -0
  135. package/dist/systems/door/door-system.d.ts +24 -0
  136. package/dist/systems/door/door-system.d.ts.map +1 -1
  137. package/dist/systems/door/door-system.js +494 -161
  138. package/dist/systems/fence/fence-system.d.ts +17 -0
  139. package/dist/systems/fence/fence-system.d.ts.map +1 -1
  140. package/dist/systems/fence/fence-system.js +221 -53
  141. package/dist/systems/floor-elevation/floor-elevation-system.d.ts.map +1 -1
  142. package/dist/systems/floor-elevation/floor-elevation-system.js +30 -7
  143. package/dist/systems/geometry/geometry-system.d.ts +7 -0
  144. package/dist/systems/geometry/geometry-system.d.ts.map +1 -1
  145. package/dist/systems/geometry/geometry-system.js +95 -26
  146. package/dist/systems/geometry/geometry-system.test.d.ts +2 -0
  147. package/dist/systems/geometry/geometry-system.test.d.ts.map +1 -0
  148. package/dist/systems/geometry/geometry-system.test.js +30 -0
  149. package/dist/systems/interactive/control-widget.d.ts +10 -0
  150. package/dist/systems/interactive/control-widget.d.ts.map +1 -0
  151. package/dist/systems/interactive/control-widget.js +35 -0
  152. package/dist/systems/interactive/interactive-system.d.ts.map +1 -1
  153. package/dist/systems/interactive/interactive-system.js +55 -56
  154. package/dist/systems/item/item-system.d.ts.map +1 -1
  155. package/dist/systems/item/item-system.js +20 -6
  156. package/dist/systems/level/level-system.d.ts.map +1 -1
  157. package/dist/systems/level/level-system.js +34 -9
  158. package/dist/systems/level/level-utils.d.ts +0 -3
  159. package/dist/systems/level/level-utils.d.ts.map +1 -1
  160. package/dist/systems/level/level-utils.js +7 -45
  161. package/dist/systems/roof/roof-materials.d.ts.map +1 -1
  162. package/dist/systems/roof/roof-materials.js +29 -8
  163. package/dist/systems/roof/roof-system.d.ts +21 -8
  164. package/dist/systems/roof/roof-system.d.ts.map +1 -1
  165. package/dist/systems/roof/roof-system.js +1079 -221
  166. package/dist/systems/slab/slab-system.d.ts +7 -4
  167. package/dist/systems/slab/slab-system.d.ts.map +1 -1
  168. package/dist/systems/slab/slab-system.js +31 -83
  169. package/dist/systems/slab/slab-system.test.js +54 -14
  170. package/dist/systems/stair/stair-system.js +7 -4
  171. package/dist/systems/wall/opening-cutout-geometry.d.ts +31 -0
  172. package/dist/systems/wall/opening-cutout-geometry.d.ts.map +1 -0
  173. package/dist/systems/wall/opening-cutout-geometry.js +167 -0
  174. package/dist/systems/wall/opening-cutout-geometry.test.d.ts +2 -0
  175. package/dist/systems/wall/opening-cutout-geometry.test.d.ts.map +1 -0
  176. package/dist/systems/wall/opening-cutout-geometry.test.js +172 -0
  177. package/dist/systems/wall/wall-cutout.d.ts.map +1 -1
  178. package/dist/systems/wall/wall-cutout.js +49 -17
  179. package/dist/systems/wall/wall-materials.d.ts +11 -7
  180. package/dist/systems/wall/wall-materials.d.ts.map +1 -1
  181. package/dist/systems/wall/wall-materials.js +244 -35
  182. package/dist/systems/wall/wall-opening-cutout.test.d.ts +2 -0
  183. package/dist/systems/wall/wall-opening-cutout.test.d.ts.map +1 -0
  184. package/dist/systems/wall/wall-opening-cutout.test.js +59 -0
  185. package/dist/systems/wall/wall-support-extension.test.d.ts +2 -0
  186. package/dist/systems/wall/wall-support-extension.test.d.ts.map +1 -0
  187. package/dist/systems/wall/wall-support-extension.test.js +116 -0
  188. package/dist/systems/wall/wall-system.d.ts +4 -8
  189. package/dist/systems/wall/wall-system.d.ts.map +1 -1
  190. package/dist/systems/wall/wall-system.js +397 -177
  191. package/dist/systems/wall/wall-terrain-bottom.test.d.ts +2 -0
  192. package/dist/systems/wall/wall-terrain-bottom.test.d.ts.map +1 -0
  193. package/dist/systems/wall/wall-terrain-bottom.test.js +34 -0
  194. package/dist/systems/window/window-animation-system.d.ts +12 -0
  195. package/dist/systems/window/window-animation-system.d.ts.map +1 -1
  196. package/dist/systems/window/window-animation-system.js +16 -5
  197. package/dist/systems/window/window-system.d.ts +7 -0
  198. package/dist/systems/window/window-system.d.ts.map +1 -1
  199. package/dist/systems/window/window-system.js +240 -14
  200. package/package.json +4 -4
  201. package/dist/components/viewer/ground-occluder.d.ts +0 -2
  202. package/dist/components/viewer/ground-occluder.d.ts.map +0 -1
  203. package/dist/components/viewer/ground-occluder.js +0 -67
  204. package/dist/components/viewer/walkthrough-controls.d.ts +0 -2
  205. package/dist/components/viewer/walkthrough-controls.d.ts.map +0 -1
  206. package/dist/components/viewer/walkthrough-controls.js +0 -131
@@ -0,0 +1,142 @@
1
+ import { DataTexture, LinearFilter, LinearMipmapLinearFilter, RGBAFormat, UnsignedByteType, } from 'three';
2
+ // Deep path, NOT the `Addons.js` aggregate: the aggregate re-exports
3
+ // LottieLoader/TTFLoader, whose CDN URL imports (`lottie-web`, `opentype.js`)
4
+ // crash bun's test runtime for every consumer of the viewer barrel.
5
+ import { KTX2Loader } from 'three/examples/jsm/loaders/KTX2Loader.js';
6
+ /** Base width/height of a Basis-supercompressed KTX2 payload needing transcode
7
+ * (vkFormat 0 = ETC1S/UASTC) whose dimensions aren't block-aligned, or null
8
+ * for anything safe. Little-endian u32s: vkFormat at byte 12, pixelWidth at
9
+ * 20, pixelHeight at 24. */
10
+ function readMisalignedBasisSize(buffer) {
11
+ if (buffer.byteLength < 28)
12
+ return null;
13
+ const view = new DataView(buffer);
14
+ const vkFormat = view.getUint32(12, true);
15
+ if (vkFormat !== 0)
16
+ return null;
17
+ const width = view.getUint32(20, true);
18
+ const height = view.getUint32(24, true);
19
+ return width % 4 !== 0 || height % 4 !== 0 ? { width, height } : null;
20
+ }
21
+ /**
22
+ * KTX2Loader that survives block-misaligned textures. WebGPU rejects
23
+ * block-compressed textures whose base dimensions aren't multiples of 4;
24
+ * three's KTX2Loader only warns and transcodes anyway, and the resulting
25
+ * invalid texture poisons every render pass that binds it (endless
26
+ * "Invalid BindGroup … Invalid CommandBuffer" spam — baked GLBs carrying an
27
+ * odd-sized user-item texture triggered exactly this). Such payloads are
28
+ * routed to a fallback loader whose support flags report no compressed
29
+ * formats, which makes the transcoder emit plain RGBA32 — uncompressed
30
+ * textures have no alignment requirement.
31
+ */
32
+ class AlignmentSafeKTX2Loader extends KTX2Loader {
33
+ rgbaFallback = null;
34
+ fallbackLoader() {
35
+ if (!this.rgbaFallback) {
36
+ this.rgbaFallback = new KTX2Loader(this.manager);
37
+ this.rgbaFallback.setTranscoderPath(this.transcoderPath);
38
+ this.rgbaFallback.workerConfig = {
39
+ astcSupported: false,
40
+ astcHDRSupported: false,
41
+ etc1Supported: false,
42
+ etc2Supported: false,
43
+ dxtSupported: false,
44
+ bptcSupported: false,
45
+ pvrtcSupported: false,
46
+ };
47
+ }
48
+ return this.rgbaFallback;
49
+ }
50
+ /** Both `load()` and GLTFLoader's KHR_texture_basisu path funnel through
51
+ * this internal — overriding it covers every entry point. */
52
+ async _createTexture(buffer, config) {
53
+ const misaligned = readMisalignedBasisSize(buffer);
54
+ if (misaligned) {
55
+ console.warn(`[viewer] KTX2 texture is ${misaligned.width}x${misaligned.height} (not multiple-of-4); ` +
56
+ 'transcoding uncompressed so WebGPU can create it.');
57
+ const transcoded = (await this.fallbackLoader()._createTexture(buffer, config));
58
+ // The RGBA32 result still arrives as a CompressedTexture; three's WebGPU
59
+ // texture upload treats every CompressedTexture as block-compressed and
60
+ // crashes looking up a block descriptor for rgba8. Repackage the decoded
61
+ // pixels as a plain DataTexture instead.
62
+ const mip = transcoded.mipmaps?.[0];
63
+ if (!mip)
64
+ return transcoded;
65
+ const texture = new DataTexture(mip.data, mip.width, mip.height, RGBAFormat, UnsignedByteType);
66
+ texture.colorSpace = transcoded.colorSpace;
67
+ texture.generateMipmaps = true;
68
+ texture.minFilter = LinearMipmapLinearFilter;
69
+ texture.magFilter = LinearFilter;
70
+ texture.needsUpdate = true;
71
+ transcoded.dispose();
72
+ return texture;
73
+ }
74
+ return KTX2Loader.prototype._createTexture.call(this, buffer, config);
75
+ }
76
+ dispose() {
77
+ this.rgbaFallback?.dispose();
78
+ this.rgbaFallback = null;
79
+ super.dispose();
80
+ return this;
81
+ }
82
+ }
83
+ /**
84
+ * Single shared KTX2 loader for the whole viewer — used both by the GLB loader
85
+ * (`use-gltf-ktx2`) and by catalog finish textures (`materials.ts`). KTX2 must
86
+ * be transcoded at load via the Basis WASM, and `detectSupport(renderer)` has to
87
+ * run once before any `.ktx2` is loaded so the loader picks a GPU format the
88
+ * device supports. `ensureKtx2Support` is idempotent per renderer and is called
89
+ * from the viewer root the moment the renderer is ready (even when no GLB is in
90
+ * the scene, so catalog `.ktx2` finishes still load).
91
+ */
92
+ export const ktx2Loader = new AlignmentSafeKTX2Loader();
93
+ ktx2Loader.setTranscoderPath('https://cdn.jsdelivr.net/gh/pmndrs/drei-assets@master/basis/');
94
+ const configuredRenderers = new WeakSet();
95
+ const warnedRenderers = new WeakSet();
96
+ let resolveKtx2Ready;
97
+ const ktx2ReadyPromise = new Promise((resolve) => {
98
+ resolveKtx2Ready = resolve;
99
+ });
100
+ /**
101
+ * Resolves once `detectSupport` has succeeded for any renderer. `.ktx2` loads
102
+ * issued before that point would throw inside KTX2Loader ("Missing
103
+ * initialization with `.detectSupport( renderer )`"), so texture loaders await
104
+ * this instead of failing — covers materials created while the renderer is
105
+ * still initializing (e.g. a standalone capture canvas).
106
+ */
107
+ export function whenKtx2Ready() {
108
+ return ktx2ReadyPromise;
109
+ }
110
+ /** Returns true once support has been detected for this renderer (KTX2 safe to load). */
111
+ export function ensureKtx2Support(renderer) {
112
+ const key = renderer;
113
+ if (!key)
114
+ return false;
115
+ if (configuredRenderers.has(key))
116
+ return true;
117
+ try {
118
+ ;
119
+ ktx2Loader.detectSupport(renderer);
120
+ configuredRenderers.add(key);
121
+ resolveKtx2Ready();
122
+ return true;
123
+ }
124
+ catch (error) {
125
+ // Some WebGPU flows can transiently call this before backend init; don't
126
+ // crash the scene — a later call (or the next render) retries.
127
+ if (!warnedRenderers.has(key)) {
128
+ console.warn('[viewer] Skipping KTX2 support detection for now.', error);
129
+ warnedRenderers.add(key);
130
+ }
131
+ return false;
132
+ }
133
+ }
134
+ export function configureKtx2Support(loader, renderer) {
135
+ if (!ensureKtx2Support(renderer))
136
+ return false;
137
+ loader.setKTX2Loader(ktx2Loader);
138
+ return true;
139
+ }
140
+ export function isKtx2Url(url) {
141
+ return url.toLowerCase().endsWith('.ktx2');
142
+ }
@@ -20,4 +20,14 @@ export declare const ZONE_LAYER = 2;
20
20
  * from thumbnails like the other editor-only layers.
21
21
  */
22
22
  export declare const GRID_LAYER = 3;
23
+ /**
24
+ * Layer for geometry hidden from the color passes but still rendered into the
25
+ * shadow map ("shadow-caster-only"). Used when cutaway/solo views hide roofs
26
+ * or non-selected levels: the sun keeps casting their shadows, so interiors
27
+ * get window-shaped light patches instead of flooding with uniform sun.
28
+ * No camera or pass enables this layer — only each shadow-casting light's
29
+ * shadow camera does (see `lights.tsx`). Applied per-object (layers don't
30
+ * cascade) via `applyShadowOnly` / `clearShadowOnly` in `lib/shadow-only.ts`.
31
+ */
32
+ export declare const SHADOW_ONLY_LAYER = 4;
23
33
  //# sourceMappingURL=layers.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"layers.d.ts","sourceRoot":"","sources":["../../src/lib/layers.ts"],"names":[],"mappings":"AAAA,sDAAsD;AACtD,eAAO,MAAM,WAAW,IAAI,CAAA;AAE5B;;;;;;;GAOG;AACH,eAAO,MAAM,aAAa,IAAI,CAAA;AAE9B,oEAAoE;AACpE,eAAO,MAAM,UAAU,IAAI,CAAA;AAE3B;;;;;;;GAOG;AACH,eAAO,MAAM,UAAU,IAAI,CAAA"}
1
+ {"version":3,"file":"layers.d.ts","sourceRoot":"","sources":["../../src/lib/layers.ts"],"names":[],"mappings":"AAAA,sDAAsD;AACtD,eAAO,MAAM,WAAW,IAAI,CAAA;AAE5B;;;;;;;GAOG;AACH,eAAO,MAAM,aAAa,IAAI,CAAA;AAE9B,oEAAoE;AACpE,eAAO,MAAM,UAAU,IAAI,CAAA;AAE3B;;;;;;;GAOG;AACH,eAAO,MAAM,UAAU,IAAI,CAAA;AAE3B;;;;;;;;GAQG;AACH,eAAO,MAAM,iBAAiB,IAAI,CAAA"}
@@ -20,3 +20,13 @@ export const ZONE_LAYER = 2;
20
20
  * from thumbnails like the other editor-only layers.
21
21
  */
22
22
  export const GRID_LAYER = 3;
23
+ /**
24
+ * Layer for geometry hidden from the color passes but still rendered into the
25
+ * shadow map ("shadow-caster-only"). Used when cutaway/solo views hide roofs
26
+ * or non-selected levels: the sun keeps casting their shadows, so interiors
27
+ * get window-shaped light patches instead of flooding with uniform sun.
28
+ * No camera or pass enables this layer — only each shadow-casting light's
29
+ * shadow camera does (see `lights.tsx`). Applied per-object (layers don't
30
+ * cascade) via `applyShadowOnly` / `clearShadowOnly` in `lib/shadow-only.ts`.
31
+ */
32
+ export const SHADOW_ONLY_LAYER = 4;
@@ -1,4 +1,4 @@
1
- import { type MaterialPresetPayload, type MaterialSchema, type SurfaceRole } from '@pascal-app/core';
1
+ import { type MaterialPresetPayload, type MaterialSchema, type SceneMaterial, type SceneMaterialId, type SurfaceRole } from '@pascal-app/core';
2
2
  import * as THREE from 'three';
3
3
  import { MeshLambertNodeMaterial } from 'three/webgpu';
4
4
  export type RenderShading = 'solid' | 'rendered';
@@ -10,10 +10,25 @@ export declare const BLUEPRINT_PALETTE: Record<SurfaceRole, string>;
10
10
  export declare const PRESET_PALETTES: Record<ColorPreset, Record<SurfaceRole, string>>;
11
11
  export declare function resolveSurfaceColor(role: SurfaceRole, preset: ColorPreset, sceneThemeId?: string): string;
12
12
  export declare const glassMaterial: MeshLambertNodeMaterial;
13
+ export declare function resolveTextureRepeat(repeat: unknown, scale: unknown): [number, number];
14
+ export declare function getTextureKey(material?: MaterialSchema): string;
13
15
  export declare function applyMaterialPresetToMaterials(materialInput: THREE.Material | THREE.Material[], preset: MaterialPresetPayload | null | undefined): void;
14
16
  export declare function createMaterialFromPreset(preset: MaterialPresetPayload, shading?: RenderShading): THREE.Material;
15
17
  export declare function createMaterialFromPresetRef(materialPreset?: string, shading?: RenderShading): THREE.Material | null;
16
18
  export declare function createMaterial(material?: MaterialSchema, shading?: RenderShading): THREE.Material;
19
+ /**
20
+ * Resolve a MaterialRef ('library:<id>' | 'scene:<id>') to a three.js material.
21
+ * Returns null for an unknown / dangling ref so callers fall back to the
22
+ * slot's default (authored material, then themed default). Never throws.
23
+ */
24
+ export declare function resolveMaterialRef(ref: string | undefined, sceneMaterials: Record<SceneMaterialId, SceneMaterial> | undefined, shading?: RenderShading): THREE.Material | null;
25
+ /**
26
+ * Resolve a node kind's declared slot default — either a catalog `library:<id>`
27
+ * finish or a flat `#rrggbb` colour — to a renderable material. Shared by the
28
+ * procedural kinds whose colored-mode unpainted appearance comes from a
29
+ * declarative default (slab, wall).
30
+ */
31
+ export declare function resolveSlotDefaultMaterial(slotDefault: string, shading?: RenderShading, roughness?: number): THREE.Material;
17
32
  export declare function createDefaultMaterial(color?: string, roughness?: number, shading?: RenderShading, side?: THREE.Side): THREE.Material;
18
33
  export declare function createSurfaceRoleMaterial(role: SurfaceRole, preset: ColorPreset, side?: THREE.Side, sceneThemeId?: string): THREE.Material;
19
34
  export declare function baseMaterial(shading?: RenderShading): THREE.Material;
@@ -1 +1 @@
1
- {"version":3,"file":"materials.d.ts","sourceRoot":"","sources":["../../src/lib/materials.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"materials.d.ts","sourceRoot":"","sources":["../../src/lib/materials.ts"],"names":[],"mappings":"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"}
@@ -1,8 +1,11 @@
1
- import { getMaterialPresetByRef, resolveMaterial, } from '@pascal-app/core';
1
+ import { getMaterialPresetByRef, parseMaterialRef, resolveMaterial, } from '@pascal-app/core';
2
2
  import * as THREE from 'three';
3
+ import { float, mix, positionViewDirection, transformedNormalView } from 'three/tsl';
3
4
  import { MeshLambertNodeMaterial, MeshStandardNodeMaterial } from 'three/webgpu';
4
5
  import { resolveCdnUrl } from './asset-url';
6
+ import { isKtx2Url, ktx2Loader, whenKtx2Ready } from './ktx2-loader';
5
7
  import { getSceneTheme } from './scene-themes';
8
+ import { stampPascalTextureRef } from './texture-reference';
6
9
  export const CLAY_PALETTE = {
7
10
  wall: '#dcd6c7',
8
11
  floor: '#cfc8b6',
@@ -12,14 +15,16 @@ export const CLAY_PALETTE = {
12
15
  glazing: '#c8d4dc',
13
16
  furnishing: '#d2ccbe',
14
17
  };
18
+ // Albedos are clamped to ≈0.83 linear (max channel #eb): real white paint
19
+ // reflects ~80%, and pure-white albedo kills GI/shadow contrast.
15
20
  export const WHITE_PALETTE = {
16
- wall: '#f4f3ef',
17
- floor: '#ece9e2',
18
- ceiling: '#fbfaf6',
21
+ wall: '#ebeae6',
22
+ floor: '#e7e4dd',
23
+ ceiling: '#ebeae6',
19
24
  roof: '#dedbd2',
20
- joinery: '#e8e5dc',
25
+ joinery: '#e5e2d9',
21
26
  glazing: '#dbe8ee',
22
- furnishing: '#efede7',
27
+ furnishing: '#e9e7e1',
23
28
  };
24
29
  export const MONO_PALETTE = {
25
30
  wall: '#c8c8c8',
@@ -49,7 +54,7 @@ export function resolveSurfaceColor(role, preset, sceneThemeId) {
49
54
  // The active scene theme may tint individual roles (e.g. Mediterranean's blue
50
55
  // roof); fall back to the chosen colour preset's palette when it doesn't.
51
56
  const tints = sceneThemeId ? getSceneTheme(sceneThemeId).clayTints : undefined;
52
- return tints?.[role] ?? PRESET_PALETTES[preset][role];
57
+ return tints?.[role] ?? (PRESET_PALETTES[preset] ?? CLAY_PALETTE)[role];
53
58
  }
54
59
  // DoubleSide on any NodeMaterial inside the MRT scenePass (SSGI's output /
55
60
  // diffuseColor / normal targets) causes WebGPU to create a render pipeline
@@ -62,10 +67,14 @@ export const glassMaterial = new MeshLambertNodeMaterial({
62
67
  opacity: 0.35,
63
68
  side: THREE.FrontSide,
64
69
  });
70
+ glassMaterial.userData.__pascalCachedMaterial = true;
71
+ function resolveNodeMaterialSide(side) {
72
+ return side === THREE.DoubleSide ? THREE.FrontSide : side;
73
+ }
65
74
  const sideMap = {
66
75
  front: THREE.FrontSide,
67
76
  back: THREE.BackSide,
68
- double: THREE.DoubleSide,
77
+ double: THREE.FrontSide,
69
78
  };
70
79
  const materialCache = new Map();
71
80
  const defaultMaterialCache = new Map();
@@ -73,6 +82,13 @@ const surfaceRoleMaterialCache = new Map();
73
82
  const textureCache = new Map();
74
83
  const textureLoadPromises = new Map();
75
84
  const textureLoader = new THREE.TextureLoader();
85
+ // `.ktx2` finish maps transcode through the shared KTX2 loader (support is
86
+ // detected once at viewer init); everything else loads as a normal image.
87
+ function pickTextureLoader(url) {
88
+ // KTX2Loader's load/loadAsync are call-compatible with TextureLoader (url →
89
+ // Texture / Promise<Texture>); cast for typing.
90
+ return isKtx2Url(url) ? ktx2Loader : textureLoader;
91
+ }
76
92
  const wrapMap = {
77
93
  Repeat: THREE.RepeatWrapping,
78
94
  ClampToEdge: THREE.ClampToEdgeWrapping,
@@ -100,13 +116,29 @@ function getTextureChannel(slot) {
100
116
  function getCacheKey(props, shading) {
101
117
  return `${shading}-${props.color}-${props.roughness}-${props.metalness}-${props.opacity}-${props.transparent}-${props.side}`;
102
118
  }
103
- function getTextureKey(material) {
119
+ function isFiniteNumber(value) {
120
+ return typeof value === 'number' && Number.isFinite(value);
121
+ }
122
+ export function resolveTextureRepeat(repeat, scale) {
123
+ const fallback = isFiniteNumber(scale) ? scale : 1;
124
+ if (Array.isArray(repeat) && isFiniteNumber(repeat[0]) && isFiniteNumber(repeat[1])) {
125
+ return [repeat[0], repeat[1]];
126
+ }
127
+ if (isFiniteNumber(repeat))
128
+ return [repeat, repeat];
129
+ if (repeat && typeof repeat === 'object' && 'x' in repeat && 'y' in repeat) {
130
+ const { x, y } = repeat;
131
+ if (isFiniteNumber(x) && isFiniteNumber(y))
132
+ return [x, y];
133
+ }
134
+ return [fallback, fallback];
135
+ }
136
+ export function getTextureKey(material) {
104
137
  const texture = material?.texture;
105
138
  if (!texture)
106
139
  return 'none';
107
- const repeat = texture.repeat?.join('x') ?? 'default';
108
- const scale = texture.scale ?? 'default';
109
- return `${texture.url}-${repeat}-${scale}`;
140
+ const [repeatX, repeatY] = resolveTextureRepeat(texture.repeat, texture.scale);
141
+ return `${texture.url}-${repeatX}x${repeatY}`;
110
142
  }
111
143
  function getTexture(material) {
112
144
  const textureConfig = material?.texture;
@@ -116,14 +148,21 @@ function getTexture(material) {
116
148
  const cached = textureCache.get(cacheKey);
117
149
  if (cached)
118
150
  return cached;
119
- const texture = textureLoader.load(textureConfig.url);
151
+ const resolvedUrl = /^(?:asset|blob|data):/.test(textureConfig.url)
152
+ ? textureConfig.url
153
+ : (resolveCdnUrl(textureConfig.url) ?? textureConfig.url);
154
+ const texture = pickTextureLoader(resolvedUrl).load(resolvedUrl);
120
155
  texture.wrapS = THREE.RepeatWrapping;
121
156
  texture.wrapT = THREE.RepeatWrapping;
122
- const repeatX = textureConfig.repeat?.[0] ?? textureConfig.scale ?? 1;
123
- const repeatY = textureConfig.repeat?.[1] ?? textureConfig.scale ?? 1;
157
+ const [repeatX, repeatY] = resolveTextureRepeat(textureConfig.repeat, textureConfig.scale);
124
158
  texture.repeat.set(repeatX, repeatY);
125
159
  texture.updateMatrix();
126
160
  texture.colorSpace = THREE.SRGBColorSpace;
161
+ stampPascalTextureRef(texture, {
162
+ kind: 'project-asset',
163
+ src: resolvedUrl,
164
+ slot: 'map',
165
+ });
127
166
  textureCache.set(cacheKey, texture);
128
167
  return texture;
129
168
  }
@@ -162,8 +201,13 @@ function getPresetTexture(path, props, slot) {
162
201
  const cached = textureCache.get(cacheKey);
163
202
  if (cached)
164
203
  return cached;
165
- const texture = textureLoader.load(resolvedPath);
204
+ const texture = pickTextureLoader(resolvedPath).load(resolvedPath);
166
205
  applyTextureProperties(texture, props, slot);
206
+ stampPascalTextureRef(texture, {
207
+ kind: 'material',
208
+ src: resolvedPath,
209
+ slot: slot ?? 'map',
210
+ });
167
211
  setTextureCacheKey(texture, cacheKey);
168
212
  textureCache.set(cacheKey, texture);
169
213
  return texture;
@@ -195,10 +239,21 @@ async function loadPresetTexture(path, props, slot) {
195
239
  const existingPromise = textureLoadPromises.get(cacheKey);
196
240
  if (existingPromise)
197
241
  return existingPromise;
198
- const promise = textureLoader
199
- .loadAsync(resolvedPath)
242
+ // `.ktx2` loads wait for `detectSupport` (KTX2Loader.load throws before it) —
243
+ // materials can be created while a capture canvas's renderer is still
244
+ // initializing, and failing here would cache the material permanently
245
+ // texture-less (white).
246
+ const load = isKtx2Url(resolvedPath)
247
+ ? whenKtx2Ready().then(() => ktx2Loader.loadAsync(resolvedPath))
248
+ : textureLoader.loadAsync(resolvedPath);
249
+ const promise = load
200
250
  .then((texture) => {
201
251
  applyTextureProperties(texture, props, slot);
252
+ stampPascalTextureRef(texture, {
253
+ kind: 'material',
254
+ src: resolvedPath,
255
+ slot: slot ?? 'map',
256
+ });
202
257
  setTextureCacheKey(texture, cacheKey);
203
258
  textureCache.set(cacheKey, texture);
204
259
  textureLoadPromises.delete(cacheKey);
@@ -215,7 +270,13 @@ async function loadPresetTexture(path, props, slot) {
215
270
  function queueTextureAssignment(material, slot, path, props) {
216
271
  const textureMaterial = material;
217
272
  if (!path) {
218
- textureMaterial[slot] = null;
273
+ if (textureMaterial[slot] != null) {
274
+ // Rebuild the node graph: a cached WebGPU material keeps a TextureNode
275
+ // for the slot, whose per-frame material reference would pull the null
276
+ // and crash in TextureNode.update ("null (reading 'matrix')").
277
+ textureMaterial[slot] = null;
278
+ material.needsUpdate = true;
279
+ }
219
280
  return;
220
281
  }
221
282
  const resolvedPath = resolveCdnUrl(path) ?? path;
@@ -230,7 +291,14 @@ function queueTextureAssignment(material, slot, path, props) {
230
291
  material.needsUpdate = true;
231
292
  return;
232
293
  }
233
- textureMaterial[slot] = null;
294
+ // Cold load: clear the slot for the fetch window, and rebuild the node
295
+ // graph if it previously held a texture — reused cached materials otherwise
296
+ // keep a TextureNode whose reference pulls the null and crashes the render
297
+ // pass. Cold loads are the norm for freshly generated library materials.
298
+ if (textureMaterial[slot] != null) {
299
+ textureMaterial[slot] = null;
300
+ material.needsUpdate = true;
301
+ }
234
302
  loadPresetTexture(path, props, slot).then((texture) => {
235
303
  if (!texture)
236
304
  return;
@@ -257,15 +325,35 @@ function applyMaterialMapProperties(material, mapProperties) {
257
325
  }
258
326
  material.transparent = mapProperties.transparent;
259
327
  material.opacity = mapProperties.opacity;
260
- material.side =
261
- mapProperties.side === 0
262
- ? THREE.FrontSide
263
- : mapProperties.side === 1
264
- ? THREE.BackSide
265
- : THREE.DoubleSide;
328
+ material.side = resolveNodeMaterialSide(mapProperties.side === 0
329
+ ? THREE.FrontSide
330
+ : mapProperties.side === 1
331
+ ? THREE.BackSide
332
+ : THREE.DoubleSide);
266
333
  applyTexturePropertiesToMaterial(material, mapProperties);
267
334
  material.needsUpdate = true;
268
335
  }
336
+ // Glass-like transparency threshold: any standard material authored as
337
+ // `transparent` with opacity below this gets the fresnel treatment.
338
+ const GLASS_OPACITY_THRESHOLD = 0.6;
339
+ /**
340
+ * Fresnel-driven opacity for glass: nearly the authored opacity head-on,
341
+ * increasingly opaque (showing the environment reflection) at grazing angles.
342
+ * This is what makes glass read as a surface instead of a flat blue tint.
343
+ */
344
+ function applyGlassFresnel(material) {
345
+ const facing = transformedNormalView.dot(positionViewDirection).clamp(0, 1);
346
+ const fresnel = facing.oneMinus().pow(3);
347
+ material.opacityNode = mix(float(material.opacity), float(0.92), fresnel);
348
+ material.envMapIntensity = 1.4;
349
+ }
350
+ function maybeApplyGlassFresnel(material) {
351
+ if (material instanceof MeshStandardNodeMaterial &&
352
+ material.transparent &&
353
+ material.opacity < GLASS_OPACITY_THRESHOLD) {
354
+ applyGlassFresnel(material);
355
+ }
356
+ }
269
357
  function applyMaterialPresetTextures(material, preset) {
270
358
  const { maps, mapProperties } = preset;
271
359
  queueTextureAssignment(material, 'map', maps.albedoMap, mapProperties);
@@ -302,6 +390,8 @@ export function createMaterialFromPreset(preset, shading = 'rendered') {
302
390
  }
303
391
  const material = shading === 'solid' ? new MeshLambertNodeMaterial() : new MeshStandardNodeMaterial();
304
392
  applyMaterialPresetToMaterials(material, preset);
393
+ maybeApplyGlassFresnel(material);
394
+ material.userData.__pascalCachedMaterial = true;
305
395
  materialCache.set(cacheKey, material);
306
396
  return material;
307
397
  }
@@ -333,21 +423,53 @@ export function createMaterial(material, shading = 'rendered') {
333
423
  roughness: props.roughness,
334
424
  metalness: props.metalness,
335
425
  });
426
+ maybeApplyGlassFresnel(threeMaterial);
427
+ threeMaterial.userData.__pascalCachedMaterial = true;
336
428
  materialCache.set(cacheKey, threeMaterial);
337
429
  return threeMaterial;
338
430
  }
431
+ /**
432
+ * Resolve a MaterialRef ('library:<id>' | 'scene:<id>') to a three.js material.
433
+ * Returns null for an unknown / dangling ref so callers fall back to the
434
+ * slot's default (authored material, then themed default). Never throws.
435
+ */
436
+ export function resolveMaterialRef(ref, sceneMaterials, shading = 'rendered') {
437
+ const parsed = parseMaterialRef(ref);
438
+ if (!parsed)
439
+ return null;
440
+ if (parsed.kind === 'library')
441
+ return createMaterialFromPresetRef(ref, shading);
442
+ const sceneMaterial = sceneMaterials?.[parsed.id];
443
+ if (!sceneMaterial)
444
+ return null;
445
+ return createMaterial(sceneMaterial.material, shading);
446
+ }
447
+ /**
448
+ * Resolve a node kind's declared slot default — either a catalog `library:<id>`
449
+ * finish or a flat `#rrggbb` colour — to a renderable material. Shared by the
450
+ * procedural kinds whose colored-mode unpainted appearance comes from a
451
+ * declarative default (slab, wall).
452
+ */
453
+ export function resolveSlotDefaultMaterial(slotDefault, shading = 'rendered', roughness = 0.9) {
454
+ if (parseMaterialRef(slotDefault)?.kind === 'library') {
455
+ return (createMaterialFromPresetRef(slotDefault, shading) ??
456
+ createDefaultMaterial('#ffffff', roughness, shading));
457
+ }
458
+ return createDefaultMaterial(slotDefault, roughness, shading);
459
+ }
339
460
  export function createDefaultMaterial(color = '#ffffff', roughness = 0.9, shading = 'rendered', side = THREE.FrontSide) {
461
+ const resolvedSide = resolveNodeMaterialSide(side);
340
462
  if (shading === 'solid') {
341
463
  return new MeshLambertNodeMaterial({
342
464
  color,
343
- side,
465
+ side: resolvedSide,
344
466
  });
345
467
  }
346
468
  return new MeshStandardNodeMaterial({
347
469
  color,
348
470
  roughness,
349
471
  metalness: 0,
350
- side,
472
+ side: resolvedSide,
351
473
  });
352
474
  }
353
475
  function cachedDefaultMaterial(key, color, roughness, shading, side = THREE.FrontSide) {
@@ -356,6 +478,7 @@ function cachedDefaultMaterial(key, color, roughness, shading, side = THREE.Fron
356
478
  if (cached)
357
479
  return cached;
358
480
  const material = createDefaultMaterial(color, roughness, shading, side);
481
+ material.userData.__pascalCachedMaterial = true;
359
482
  defaultMaterialCache.set(cacheKey, material);
360
483
  return material;
361
484
  }
@@ -368,7 +491,7 @@ export function createSurfaceRoleMaterial(role, preset, side = THREE.FrontSide,
368
491
  // on both gable faces on the first frame). Callers that need both sides
369
492
  // visible (e.g. dormer back gable) must rotate the host mesh 180° so the
370
493
  // FrontSide faces the viewer.
371
- const resolvedSide = role === 'glazing' ? THREE.FrontSide : side;
494
+ const resolvedSide = role === 'glazing' ? THREE.FrontSide : resolveNodeMaterialSide(side ?? THREE.FrontSide);
372
495
  const cacheKey = `${role}-${preset}-${resolvedSide}-${sceneThemeId ?? 'base'}`;
373
496
  const cached = surfaceRoleMaterialCache.get(cacheKey);
374
497
  if (cached)
@@ -390,10 +513,10 @@ export function createSurfaceRoleMaterial(role, preset, side = THREE.FrontSide,
390
513
  return material;
391
514
  }
392
515
  export function baseMaterial(shading = 'rendered') {
393
- return cachedDefaultMaterial('base', '#f2f0ed', 0.5, shading);
516
+ return cachedDefaultMaterial('base', '#e9e7e3', 0.5, shading);
394
517
  }
395
518
  export function DEFAULT_WALL_MATERIAL(shading = 'rendered') {
396
- return cachedDefaultMaterial('wall', '#ffffff', 0.9, shading);
519
+ return cachedDefaultMaterial('wall', '#e9e6e0', 0.9, shading);
397
520
  }
398
521
  export function DEFAULT_SLAB_MATERIAL(shading = 'rendered') {
399
522
  return cachedDefaultMaterial('slab', '#e5e5e5', 0.8, shading);
@@ -424,20 +547,21 @@ export function DEFAULT_WINDOW_MATERIAL(shading = 'rendered') {
424
547
  roughness: 0.1,
425
548
  metalness: 0.1,
426
549
  });
550
+ maybeApplyGlassFresnel(material);
427
551
  defaultMaterialCache.set(cacheKey, material);
428
552
  return material;
429
553
  }
430
554
  export function DEFAULT_CEILING_MATERIAL(shading = 'rendered') {
431
- return cachedDefaultMaterial('ceiling', '#f5f5dc', 0.95, shading);
555
+ return cachedDefaultMaterial('ceiling', '#ebebd3', 0.95, shading);
432
556
  }
433
557
  export function DEFAULT_ROOF_MATERIAL(shading = 'rendered') {
434
558
  return cachedDefaultMaterial('roof', '#808080', 0.85, shading);
435
559
  }
436
560
  export function DEFAULT_SHELF_MATERIAL(shading = 'rendered') {
437
- return cachedDefaultMaterial('shelf', '#ffffff', 0.9, shading);
561
+ return cachedDefaultMaterial('shelf', '#e9e6e0', 0.9, shading);
438
562
  }
439
563
  export function DEFAULT_STAIR_MATERIAL(shading = 'rendered') {
440
- return cachedDefaultMaterial('stair', '#ffffff', 0.9, shading);
564
+ return cachedDefaultMaterial('stair', '#e9e6e0', 0.9, shading);
441
565
  }
442
566
  export function disposeMaterial(material) {
443
567
  material.dispose();
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=materials.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"materials.test.d.ts","sourceRoot":"","sources":["../../src/lib/materials.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,25 @@
1
+ // @ts-expect-error — bun:test is provided by the Bun runtime; viewer does not
2
+ // depend on @types/bun so the import type is unresolved at compile time.
3
+ import { describe, expect, test } from 'bun:test';
4
+ import { getTextureKey, resolveTextureRepeat } from './materials';
5
+ function materialWithRepeat(repeat) {
6
+ return {
7
+ texture: {
8
+ url: 'https://example.com/texture.png',
9
+ repeat,
10
+ },
11
+ };
12
+ }
13
+ describe('legacy texture repeat values', () => {
14
+ test('normalizes tuple, scalar, and Vector2-shaped repeats', () => {
15
+ expect(resolveTextureRepeat([2, 3], undefined)).toEqual([2, 3]);
16
+ expect(resolveTextureRepeat(2, undefined)).toEqual([2, 2]);
17
+ expect(resolveTextureRepeat({ x: 2, y: 3 }, undefined)).toEqual([2, 3]);
18
+ });
19
+ test('falls back to scale for malformed repeats', () => {
20
+ expect(resolveTextureRepeat({ width: 2 }, 4)).toEqual([4, 4]);
21
+ });
22
+ test('keeps distinct Vector2-shaped repeats in distinct cache entries', () => {
23
+ expect(getTextureKey(materialWithRepeat({ x: 2, y: 3 }))).not.toBe(getTextureKey(materialWithRepeat({ x: 4, y: 5 })));
24
+ });
25
+ });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=merged-outline-node.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"merged-outline-node.test.d.ts","sourceRoot":"","sources":["../../src/lib/merged-outline-node.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,22 @@
1
+ // @ts-expect-error — bun:test is provided by the Bun runtime; viewer does not
2
+ // depend on @types/bun so the import type is unresolved at compile time.
3
+ import { describe, expect, test } from 'bun:test';
4
+ import { Object3D, PerspectiveCamera, Scene } from 'three';
5
+ import { mergedOutline } from './merged-outline-node';
6
+ describe('merged outline rendering', () => {
7
+ test('keeps selected outlines active during camera interaction', () => {
8
+ const cameraInteractionActive = true;
9
+ const params = {
10
+ enabled: () => !cameraInteractionActive,
11
+ primaryObjects: [new Object3D()],
12
+ };
13
+ const outline = mergedOutline(new Scene(), new PerspectiveCamera(), params);
14
+ const frame = {
15
+ get renderer() {
16
+ throw new Error('outline renderer was reached');
17
+ },
18
+ };
19
+ expect(() => outline.updateBefore(frame)).toThrow('outline renderer was reached');
20
+ outline.dispose();
21
+ });
22
+ });
@@ -4,6 +4,7 @@ export type SceneTheme = {
4
4
  name: string;
5
5
  appearance: 'light' | 'dark';
6
6
  background: string;
7
+ backgroundSky?: string;
7
8
  ground: string;
8
9
  ambient: {
9
10
  color: string;