@pascal-app/viewer 0.9.1 → 0.9.2

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 (176) hide show
  1. package/dist/components/error-boundary.d.ts +6 -0
  2. package/dist/components/error-boundary.d.ts.map +1 -1
  3. package/dist/components/error-boundary.js +6 -0
  4. package/dist/components/renderers/node-renderer.d.ts +5 -1
  5. package/dist/components/renderers/node-renderer.d.ts.map +1 -1
  6. package/dist/components/renderers/node-renderer.js +5 -2
  7. package/dist/components/renderers/parametric-node-renderer.d.ts.map +1 -1
  8. package/dist/components/renderers/parametric-node-renderer.js +7 -4
  9. package/dist/components/viewer/bvh-ecctrl.d.ts +77 -0
  10. package/dist/components/viewer/bvh-ecctrl.d.ts.map +1 -0
  11. package/dist/components/viewer/bvh-ecctrl.js +552 -0
  12. package/dist/components/viewer/frame-limiter.d.ts.map +1 -1
  13. package/dist/components/viewer/frame-limiter.js +39 -4
  14. package/dist/components/viewer/glb-interactive.d.ts +41 -0
  15. package/dist/components/viewer/glb-interactive.d.ts.map +1 -0
  16. package/dist/components/viewer/glb-interactive.js +394 -0
  17. package/dist/components/viewer/glb-reference-nodes.d.ts +29 -0
  18. package/dist/components/viewer/glb-reference-nodes.d.ts.map +1 -0
  19. package/dist/components/viewer/glb-reference-nodes.js +80 -0
  20. package/dist/components/viewer/glb-replace-instances.d.ts +18 -0
  21. package/dist/components/viewer/glb-replace-instances.d.ts.map +1 -0
  22. package/dist/components/viewer/glb-replace-instances.js +61 -0
  23. package/dist/components/viewer/glb-scene.d.ts +54 -0
  24. package/dist/components/viewer/glb-scene.d.ts.map +1 -0
  25. package/dist/components/viewer/glb-scene.js +920 -0
  26. package/dist/components/viewer/glb-walkthrough-controller.d.ts +12 -0
  27. package/dist/components/viewer/glb-walkthrough-controller.d.ts.map +1 -0
  28. package/dist/components/viewer/glb-walkthrough-controller.js +357 -0
  29. package/dist/components/viewer/index.d.ts +19 -2
  30. package/dist/components/viewer/index.d.ts.map +1 -1
  31. package/dist/components/viewer/index.js +104 -12
  32. package/dist/components/viewer/lights.d.ts.map +1 -1
  33. package/dist/components/viewer/lights.js +36 -5
  34. package/dist/components/viewer/perf-monitor.d.ts.map +1 -1
  35. package/dist/components/viewer/perf-monitor.js +17 -2
  36. package/dist/components/viewer/post-processing.d.ts +7 -1
  37. package/dist/components/viewer/post-processing.d.ts.map +1 -1
  38. package/dist/components/viewer/post-processing.js +179 -24
  39. package/dist/components/viewer/registered-systems.d.ts.map +1 -1
  40. package/dist/components/viewer/registered-systems.js +6 -4
  41. package/dist/components/viewer/scene-bvh.d.ts +2 -1
  42. package/dist/components/viewer/scene-bvh.d.ts.map +1 -1
  43. package/dist/components/viewer/scene-bvh.js +3 -0
  44. package/dist/components/viewer/scene-bvh.test.d.ts +2 -0
  45. package/dist/components/viewer/scene-bvh.test.d.ts.map +1 -0
  46. package/dist/components/viewer/scene-bvh.test.js +13 -0
  47. package/dist/components/viewer/scene-environment.d.ts +2 -0
  48. package/dist/components/viewer/scene-environment.d.ts.map +1 -0
  49. package/dist/components/viewer/scene-environment.js +79 -0
  50. package/dist/components/viewer/selection-manager.d.ts.map +1 -1
  51. package/dist/components/viewer/selection-manager.js +18 -10
  52. package/dist/components/viewer/walkthrough-controls.d.ts +1 -0
  53. package/dist/components/viewer/walkthrough-controls.d.ts.map +1 -1
  54. package/dist/components/viewer/walkthrough-controls.js +20 -0
  55. package/dist/hooks/use-gltf-ktx2.d.ts.map +1 -1
  56. package/dist/hooks/use-gltf-ktx2.js +3 -22
  57. package/dist/hooks/use-node-events.d.ts +1 -1
  58. package/dist/hooks/use-node-events.d.ts.map +1 -1
  59. package/dist/hooks/use-node-events.js +22 -14
  60. package/dist/index.d.ts +19 -8
  61. package/dist/index.d.ts.map +1 -1
  62. package/dist/index.js +22 -8
  63. package/dist/lib/backdrop.d.ts +33 -0
  64. package/dist/lib/backdrop.d.ts.map +1 -0
  65. package/dist/lib/backdrop.js +92 -0
  66. package/dist/lib/box-uv.d.ts +13 -0
  67. package/dist/lib/box-uv.d.ts.map +1 -0
  68. package/dist/lib/box-uv.js +32 -0
  69. package/dist/lib/csg-utils.d.ts +3 -9
  70. package/dist/lib/csg-utils.d.ts.map +1 -1
  71. package/dist/lib/csg-utils.js +65 -5
  72. package/dist/lib/csg-utils.test.d.ts +2 -0
  73. package/dist/lib/csg-utils.test.d.ts.map +1 -0
  74. package/dist/lib/csg-utils.test.js +45 -0
  75. package/dist/lib/edge-style.d.ts +1 -0
  76. package/dist/lib/edge-style.d.ts.map +1 -1
  77. package/dist/lib/edge-style.js +26 -6
  78. package/dist/lib/ink-edges.d.ts +1 -1
  79. package/dist/lib/ink-edges.d.ts.map +1 -1
  80. package/dist/lib/ink-edges.js +13 -7
  81. package/dist/lib/ktx2-loader.d.ts +43 -0
  82. package/dist/lib/ktx2-loader.d.ts.map +1 -0
  83. package/dist/lib/ktx2-loader.js +136 -0
  84. package/dist/lib/layers.d.ts +10 -0
  85. package/dist/lib/layers.d.ts.map +1 -1
  86. package/dist/lib/layers.js +10 -0
  87. package/dist/lib/materials.d.ts +14 -1
  88. package/dist/lib/materials.d.ts.map +1 -1
  89. package/dist/lib/materials.js +103 -26
  90. package/dist/lib/scene-themes.d.ts +1 -0
  91. package/dist/lib/scene-themes.d.ts.map +1 -1
  92. package/dist/lib/scene-themes.js +25 -16
  93. package/dist/lib/shadow-only.d.ts +4 -0
  94. package/dist/lib/shadow-only.d.ts.map +1 -0
  95. package/dist/lib/shadow-only.js +36 -0
  96. package/dist/lib/tsl-compat.d.ts +12 -0
  97. package/dist/lib/tsl-compat.d.ts.map +1 -0
  98. package/dist/lib/tsl-compat.js +13 -0
  99. package/dist/store/use-viewer.d.ts +60 -18
  100. package/dist/store/use-viewer.d.ts.map +1 -1
  101. package/dist/store/use-viewer.js +175 -3
  102. package/dist/store/use-viewer.test.d.ts +2 -0
  103. package/dist/store/use-viewer.test.d.ts.map +1 -0
  104. package/dist/store/use-viewer.test.js +42 -0
  105. package/dist/systems/ceiling/ceiling-system.d.ts.map +1 -1
  106. package/dist/systems/ceiling/ceiling-system.js +4 -1
  107. package/dist/systems/door/door-animation.test.d.ts +2 -0
  108. package/dist/systems/door/door-animation.test.d.ts.map +1 -0
  109. package/dist/systems/door/door-animation.test.js +51 -0
  110. package/dist/systems/door/door-floor-alignment.test.d.ts +2 -0
  111. package/dist/systems/door/door-floor-alignment.test.d.ts.map +1 -0
  112. package/dist/systems/door/door-floor-alignment.test.js +50 -0
  113. package/dist/systems/door/door-system.d.ts +24 -0
  114. package/dist/systems/door/door-system.d.ts.map +1 -1
  115. package/dist/systems/door/door-system.js +494 -161
  116. package/dist/systems/fence/fence-system.d.ts +17 -0
  117. package/dist/systems/fence/fence-system.d.ts.map +1 -1
  118. package/dist/systems/fence/fence-system.js +221 -53
  119. package/dist/systems/geometry/geometry-system.d.ts +7 -0
  120. package/dist/systems/geometry/geometry-system.d.ts.map +1 -1
  121. package/dist/systems/geometry/geometry-system.js +95 -26
  122. package/dist/systems/geometry/geometry-system.test.d.ts +2 -0
  123. package/dist/systems/geometry/geometry-system.test.d.ts.map +1 -0
  124. package/dist/systems/geometry/geometry-system.test.js +30 -0
  125. package/dist/systems/interactive/control-widget.d.ts +10 -0
  126. package/dist/systems/interactive/control-widget.d.ts.map +1 -0
  127. package/dist/systems/interactive/control-widget.js +35 -0
  128. package/dist/systems/interactive/interactive-system.d.ts.map +1 -1
  129. package/dist/systems/interactive/interactive-system.js +55 -56
  130. package/dist/systems/item/item-system.d.ts.map +1 -1
  131. package/dist/systems/item/item-system.js +20 -6
  132. package/dist/systems/level/level-system.d.ts.map +1 -1
  133. package/dist/systems/level/level-system.js +28 -4
  134. package/dist/systems/level/level-utils.d.ts +0 -3
  135. package/dist/systems/level/level-utils.d.ts.map +1 -1
  136. package/dist/systems/level/level-utils.js +2 -41
  137. package/dist/systems/roof/roof-materials.d.ts.map +1 -1
  138. package/dist/systems/roof/roof-materials.js +29 -8
  139. package/dist/systems/roof/roof-system.d.ts +21 -8
  140. package/dist/systems/roof/roof-system.d.ts.map +1 -1
  141. package/dist/systems/roof/roof-system.js +1079 -221
  142. package/dist/systems/slab/slab-system.d.ts +5 -3
  143. package/dist/systems/slab/slab-system.d.ts.map +1 -1
  144. package/dist/systems/slab/slab-system.js +41 -23
  145. package/dist/systems/slab/slab-system.test.js +3 -2
  146. package/dist/systems/stair/stair-system.js +6 -3
  147. package/dist/systems/wall/opening-cutout-geometry.d.ts +31 -0
  148. package/dist/systems/wall/opening-cutout-geometry.d.ts.map +1 -0
  149. package/dist/systems/wall/opening-cutout-geometry.js +167 -0
  150. package/dist/systems/wall/opening-cutout-geometry.test.d.ts +2 -0
  151. package/dist/systems/wall/opening-cutout-geometry.test.d.ts.map +1 -0
  152. package/dist/systems/wall/opening-cutout-geometry.test.js +172 -0
  153. package/dist/systems/wall/wall-cutout.d.ts.map +1 -1
  154. package/dist/systems/wall/wall-cutout.js +46 -17
  155. package/dist/systems/wall/wall-materials.d.ts +11 -7
  156. package/dist/systems/wall/wall-materials.d.ts.map +1 -1
  157. package/dist/systems/wall/wall-materials.js +244 -35
  158. package/dist/systems/wall/wall-opening-cutout.test.d.ts +2 -0
  159. package/dist/systems/wall/wall-opening-cutout.test.d.ts.map +1 -0
  160. package/dist/systems/wall/wall-opening-cutout.test.js +59 -0
  161. package/dist/systems/wall/wall-support-extension.test.d.ts +2 -0
  162. package/dist/systems/wall/wall-support-extension.test.d.ts.map +1 -0
  163. package/dist/systems/wall/wall-support-extension.test.js +48 -0
  164. package/dist/systems/wall/wall-system.d.ts +2 -2
  165. package/dist/systems/wall/wall-system.d.ts.map +1 -1
  166. package/dist/systems/wall/wall-system.js +329 -174
  167. package/dist/systems/window/window-animation-system.d.ts +12 -0
  168. package/dist/systems/window/window-animation-system.d.ts.map +1 -1
  169. package/dist/systems/window/window-animation-system.js +16 -5
  170. package/dist/systems/window/window-system.d.ts +7 -0
  171. package/dist/systems/window/window-system.d.ts.map +1 -1
  172. package/dist/systems/window/window-system.js +240 -14
  173. package/package.json +4 -4
  174. package/dist/components/viewer/ground-occluder.d.ts +0 -2
  175. package/dist/components/viewer/ground-occluder.d.ts.map +0 -1
  176. package/dist/components/viewer/ground-occluder.js +0 -67
@@ -0,0 +1,92 @@
1
+ import { abs, exp, mix, smoothstep } from 'three/tsl';
2
+ /**
3
+ * Shared backdrop gradient: flat background looking down, a warm haze band
4
+ * hugging the horizon, the theme sky arriving just above it and deepening
5
+ * toward the zenith. One formula serves the post-processing backdrop and the
6
+ * thumbnail pipeline (per-pixel view ray) AND the site horizon disc's
7
+ * far-field dissolve (per-fragment view direction) — the disc converges to
8
+ * exactly this colour, so ground and backdrop meet without a seam from any
9
+ * camera pose.
10
+ *
11
+ * `dirY` is the world-space view direction's Y component (horizon = 0).
12
+ * Colour inputs are TSL nodes (uniforms or literals).
13
+ */
14
+ export function backdropGradient({ dirY, background, haze, sky, skyDeep, }) {
15
+ // The pale sky arrives fast (full by ≈8° elevation) so eye-level frames
16
+ // actually show it, then deepens toward the zenith for a real gradient —
17
+ // a single pale stop reads as a white void with blue "hiding" up top.
18
+ let base = mix(background, sky, smoothstep(-0.02, 0.14, dirY));
19
+ base = mix(base, skyDeep, smoothstep(0.1, 0.55, dirY));
20
+ // Haze as an exponential glow hugging the horizon tightly: C¹-smooth on
21
+ // both sides, so it brightens the junction without ever drawing an edge.
22
+ const hazeWeight = exp(abs(dirY).mul(-11)).mul(0.8);
23
+ return mix(base, haze, hazeWeight);
24
+ }
25
+ // Warm sun tint the haze pulls toward — aerial perspective is sky-coloured
26
+ // light plus a little sun scatter, not white fog.
27
+ const HAZE_SUN_TINT = [255, 244, 222];
28
+ /**
29
+ * Atmospheric haze at the horizon: the theme's *sky* colour pulled toward a
30
+ * warm sun tint — skyish and sunish at once, so the band reads as part of
31
+ * the sky rather than a white stripe. Dark themes keep it faint (a low glow
32
+ * over the night zenith).
33
+ */
34
+ export function horizonHazeColor(sky, appearance) {
35
+ const amount = appearance === 'dark' ? 0.25 : 0.5;
36
+ const hex = sky.replace('#', '');
37
+ let out = '#';
38
+ for (let i = 0; i < 3; i++) {
39
+ const c = Number.parseInt(hex.slice(i * 2, i * 2 + 2), 16);
40
+ out += Math.round(c + (HAZE_SUN_TINT[i] - c) * amount)
41
+ .toString(16)
42
+ .padStart(2, '0');
43
+ }
44
+ return out;
45
+ }
46
+ /**
47
+ * Zenith colour derived from the theme's sky: saturated and darkened in HSL
48
+ * so the hue stays the theme's own (blue studio, lavender sunset, near-black
49
+ * night) while the top of the frame gets real colour depth.
50
+ */
51
+ export function deepSkyColor(sky) {
52
+ const hex = sky.replace('#', '');
53
+ const r = Number.parseInt(hex.slice(0, 2), 16) / 255;
54
+ const g = Number.parseInt(hex.slice(2, 4), 16) / 255;
55
+ const b = Number.parseInt(hex.slice(4, 6), 16) / 255;
56
+ const max = Math.max(r, g, b);
57
+ const min = Math.min(r, g, b);
58
+ const l = (max + min) / 2;
59
+ const d = max - min;
60
+ let h = 0;
61
+ let s = 0;
62
+ if (d > 0) {
63
+ s = d / (1 - Math.abs(2 * l - 1));
64
+ if (max === r)
65
+ h = ((g - b) / d + (g < b ? 6 : 0)) / 6;
66
+ else if (max === g)
67
+ h = ((b - r) / d + 2) / 6;
68
+ else
69
+ h = ((r - g) / d + 4) / 6;
70
+ }
71
+ const s2 = Math.min(1, s * 1.5 + 0.05);
72
+ const l2 = l * 0.72;
73
+ const c = (1 - Math.abs(2 * l2 - 1)) * s2;
74
+ const x = c * (1 - Math.abs(((h * 6) % 2) - 1));
75
+ const m = l2 - c / 2;
76
+ const sector = Math.floor(h * 6) % 6;
77
+ const rgb = [
78
+ [c, x, 0],
79
+ [x, c, 0],
80
+ [0, c, x],
81
+ [0, x, c],
82
+ [x, 0, c],
83
+ [c, 0, x],
84
+ ][sector];
85
+ let out = '#';
86
+ for (const channel of rgb) {
87
+ out += Math.round((channel + m) * 255)
88
+ .toString(16)
89
+ .padStart(2, '0');
90
+ }
91
+ return out;
92
+ }
@@ -0,0 +1,13 @@
1
+ import type { BoxGeometry } from 'three';
2
+ /**
3
+ * Rewrite a default `BoxGeometry`'s UVs to world scale — 1 UV unit = 1 metre —
4
+ * so tiled finishes (with `repeat` in tiles-per-metre) render at a consistent
5
+ * real-world scale instead of stretching to fit each face. Matches the
6
+ * world-scale UV convention used by the procedural slab/wall geometry.
7
+ *
8
+ * three.js builds box faces in the fixed order [+X, -X, +Y, -Y, +Z, -Z], four
9
+ * verts each, with UVs spanning 0→1 across the face. Each face's two in-plane
10
+ * dimensions differ, so we scale U/V per face by that face's size in metres.
11
+ */
12
+ export declare function applyWorldScaleBoxUVs(geometry: BoxGeometry, w: number, h: number, d: number): void;
13
+ //# sourceMappingURL=box-uv.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"box-uv.d.ts","sourceRoot":"","sources":["../../src/lib/box-uv.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,OAAO,CAAA;AAExC;;;;;;;;;GASG;AACH,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,WAAW,EACrB,CAAC,EAAE,MAAM,EACT,CAAC,EAAE,MAAM,EACT,CAAC,EAAE,MAAM,GACR,IAAI,CAsBN"}
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Rewrite a default `BoxGeometry`'s UVs to world scale — 1 UV unit = 1 metre —
3
+ * so tiled finishes (with `repeat` in tiles-per-metre) render at a consistent
4
+ * real-world scale instead of stretching to fit each face. Matches the
5
+ * world-scale UV convention used by the procedural slab/wall geometry.
6
+ *
7
+ * three.js builds box faces in the fixed order [+X, -X, +Y, -Y, +Z, -Z], four
8
+ * verts each, with UVs spanning 0→1 across the face. Each face's two in-plane
9
+ * dimensions differ, so we scale U/V per face by that face's size in metres.
10
+ */
11
+ export function applyWorldScaleBoxUVs(geometry, w, h, d) {
12
+ const uv = geometry.getAttribute('uv');
13
+ if (!uv || uv.count < 24)
14
+ return; // non-default segmentation — leave as-is
15
+ // [uScaleMetres, vScaleMetres] per face, in three's face order.
16
+ const faceScale = [
17
+ [d, h], // +X
18
+ [d, h], // -X
19
+ [w, d], // +Y
20
+ [w, d], // -Y
21
+ [w, h], // +Z
22
+ [w, h], // -Z
23
+ ];
24
+ for (let face = 0; face < 6; face += 1) {
25
+ const [us, vs] = faceScale[face];
26
+ for (let v = 0; v < 4; v += 1) {
27
+ const i = face * 4 + v;
28
+ uv.setXY(i, uv.getX(i) * us, uv.getY(i) * vs);
29
+ }
30
+ }
31
+ uv.needsUpdate = true;
32
+ }
@@ -1,16 +1,10 @@
1
- import type * as THREE from 'three';
1
+ import * as THREE from 'three';
2
2
  import { type Brush, Evaluator } from 'three-bvh-csg';
3
- /**
4
- * Shared CSG primitives used by kinds whose geometry subtracts pieces
5
- * against their host (chimney trimmed by the roof shell, skylight
6
- * frame as a ring cut from a box, etc.). Lives in viewer because
7
- * three-bvh-csg + three-mesh-bvh are viewer-only deps; kinds living
8
- * in `@pascal-app/nodes` import these through the public surface.
9
- */
3
+ export declare function ensureRenderableGeometryAttributes(geometry: THREE.BufferGeometry): THREE.BufferGeometry;
10
4
  export declare function csgGeometry(brush: Brush): THREE.BufferGeometry;
11
5
  export declare function csgMaterials(brush: Brush): THREE.Material[];
12
6
  export declare const csgEvaluator: Evaluator;
13
7
  export declare function computeGeometryBoundsTree(geometry: THREE.BufferGeometry): void;
14
8
  export declare function prepareBrushForCSG(brush: Brush): void;
15
- export { ADDITION, Brush, SUBTRACTION } from 'three-bvh-csg';
9
+ export { ADDITION, Brush, INTERSECTION, SUBTRACTION } from 'three-bvh-csg';
16
10
  //# sourceMappingURL=csg-utils.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"csg-utils.d.ts","sourceRoot":"","sources":["../../src/lib/csg-utils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,KAAK,MAAM,OAAO,CAAA;AACnC,OAAO,EAAE,KAAK,KAAK,EAAE,SAAS,EAAE,MAAM,eAAe,CAAA;AAGrD;;;;;;GAMG;AAEH,wBAAgB,WAAW,CAAC,KAAK,EAAE,KAAK,GAAG,KAAK,CAAC,cAAc,CAE9D;AAED,wBAAgB,YAAY,CAAC,KAAK,EAAE,KAAK,GAAG,KAAK,CAAC,QAAQ,EAAE,CAG3D;AAED,eAAO,MAAM,YAAY,WAAkB,CAAA;AAK3C,wBAAgB,yBAAyB,CAAC,QAAQ,EAAE,KAAK,CAAC,cAAc,QAMvE;AAED,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,KAAK,QAG9C;AAID,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAA"}
1
+ {"version":3,"file":"csg-utils.d.ts","sourceRoot":"","sources":["../../src/lib/csg-utils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAC9B,OAAO,EAAE,KAAK,KAAK,EAAE,SAAS,EAAE,MAAM,eAAe,CAAA;AA4CrD,wBAAgB,kCAAkC,CAChD,QAAQ,EAAE,KAAK,CAAC,cAAc,GAC7B,KAAK,CAAC,cAAc,CAoCtB;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,KAAK,GAAG,KAAK,CAAC,cAAc,CAE9D;AAED,wBAAgB,YAAY,CAAC,KAAK,EAAE,KAAK,GAAG,KAAK,CAAC,QAAQ,EAAE,CAG3D;AAED,eAAO,MAAM,YAAY,WAAkB,CAAA;AAK3C,wBAAgB,yBAAyB,CAAC,QAAQ,EAAE,KAAK,CAAC,cAAc,QAMvE;AAED,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,KAAK,QAI9C;AAID,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,eAAe,CAAA"}
@@ -1,3 +1,4 @@
1
+ import * as THREE from 'three';
1
2
  import { Evaluator } from 'three-bvh-csg';
2
3
  import { computeBoundsTree } from 'three-mesh-bvh';
3
4
  /**
@@ -7,8 +8,66 @@ import { computeBoundsTree } from 'three-mesh-bvh';
7
8
  * three-bvh-csg + three-mesh-bvh are viewer-only deps; kinds living
8
9
  * in `@pascal-app/nodes` import these through the public surface.
9
10
  */
11
+ function zeroAttribute(count, itemSize) {
12
+ return new THREE.Float32BufferAttribute(new Float32Array(count * itemSize), itemSize);
13
+ }
14
+ function upNormalAttribute(count) {
15
+ const values = new Float32Array(count * 3);
16
+ for (let index = 0; index < count; index += 1) {
17
+ values[index * 3 + 1] = 1;
18
+ }
19
+ return new THREE.Float32BufferAttribute(values, 3);
20
+ }
21
+ function ensureAttributeCount(geometry, name, itemSize, count) {
22
+ const attribute = geometry.getAttribute(name);
23
+ if (attribute?.count === count && attribute.itemSize === itemSize)
24
+ return;
25
+ geometry.setAttribute(name, zeroAttribute(count, itemSize));
26
+ }
27
+ function copyVec2Attribute(attribute) {
28
+ const values = new Float32Array(attribute.count * 2);
29
+ for (let index = 0; index < attribute.count; index += 1) {
30
+ values[index * 2] = attribute.getX(index);
31
+ values[index * 2 + 1] = attribute.getY(index);
32
+ }
33
+ return new THREE.Float32BufferAttribute(values, 2);
34
+ }
35
+ export function ensureRenderableGeometryAttributes(geometry) {
36
+ const position = geometry.getAttribute('position');
37
+ if (!position || position.count === 0 || position.itemSize !== 3) {
38
+ geometry.setIndex(null);
39
+ geometry.clearGroups();
40
+ geometry.setAttribute('position', zeroAttribute(3, 3));
41
+ geometry.setAttribute('normal', upNormalAttribute(3));
42
+ geometry.setAttribute('uv', zeroAttribute(3, 2));
43
+ geometry.setAttribute('uv2', zeroAttribute(3, 2));
44
+ return geometry;
45
+ }
46
+ const count = position.count;
47
+ const normal = geometry.getAttribute('normal');
48
+ if (normal?.count !== count || normal.itemSize !== 3) {
49
+ geometry.deleteAttribute('normal');
50
+ try {
51
+ geometry.computeVertexNormals();
52
+ }
53
+ catch {
54
+ geometry.deleteAttribute('normal');
55
+ }
56
+ }
57
+ const computedNormal = geometry.getAttribute('normal');
58
+ if (computedNormal?.count !== count || computedNormal.itemSize !== 3) {
59
+ geometry.setAttribute('normal', upNormalAttribute(count));
60
+ }
61
+ ensureAttributeCount(geometry, 'uv', 2, count);
62
+ const uv = geometry.getAttribute('uv');
63
+ const uv2 = geometry.getAttribute('uv2');
64
+ if (uv2?.count !== count || uv2.itemSize !== 2) {
65
+ geometry.setAttribute('uv2', copyVec2Attribute(uv));
66
+ }
67
+ return geometry;
68
+ }
10
69
  export function csgGeometry(brush) {
11
- return brush.geometry;
70
+ return ensureRenderableGeometryAttributes(brush.geometry);
12
71
  }
13
72
  export function csgMaterials(brush) {
14
73
  const mat = brush.material;
@@ -17,7 +76,7 @@ export function csgMaterials(brush) {
17
76
  export const csgEvaluator = new Evaluator();
18
77
  csgEvaluator.useGroups = true;
19
78
  csgEvaluator.consolidateGroups = false;
20
- csgEvaluator.attributes = ['position', 'normal', 'uv'];
79
+ csgEvaluator.attributes = ['position', 'normal', 'uv', 'uv2'];
21
80
  export function computeGeometryBoundsTree(geometry) {
22
81
  ;
23
82
  geometry.computeBoundsTree =
@@ -25,9 +84,10 @@ export function computeGeometryBoundsTree(geometry) {
25
84
  geometry.computeBoundsTree({ maxLeafSize: 10 });
26
85
  }
27
86
  export function prepareBrushForCSG(brush) {
87
+ ensureRenderableGeometryAttributes(brush.geometry);
28
88
  computeGeometryBoundsTree(brush.geometry);
29
89
  brush.updateMatrixWorld();
30
90
  }
31
- // Re-export Brush + SUBTRACTION + ADDITION so kinds don't need a direct
32
- // `three-bvh-csg` dependency.
33
- export { ADDITION, Brush, SUBTRACTION } from 'three-bvh-csg';
91
+ // Re-export Brush + SUBTRACTION + ADDITION + INTERSECTION so kinds don't need a
92
+ // direct `three-bvh-csg` dependency.
93
+ export { ADDITION, Brush, INTERSECTION, SUBTRACTION } from 'three-bvh-csg';
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=csg-utils.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"csg-utils.test.d.ts","sourceRoot":"","sources":["../../src/lib/csg-utils.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,45 @@
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 * as THREE from 'three';
5
+ import { ensureRenderableGeometryAttributes } from './csg-utils';
6
+ describe('ensureRenderableGeometryAttributes', () => {
7
+ test('fills missing render attributes to match position count', () => {
8
+ const geometry = new THREE.BufferGeometry();
9
+ geometry.setAttribute('position', new THREE.Float32BufferAttribute(new Float32Array([0, 0, 0, 1, 0, 0, 0, 1, 0]), 3));
10
+ ensureRenderableGeometryAttributes(geometry);
11
+ expect(geometry.getAttribute('position')?.count).toBe(3);
12
+ expect(geometry.getAttribute('normal')?.count).toBe(3);
13
+ expect(geometry.getAttribute('uv')?.count).toBe(3);
14
+ expect(geometry.getAttribute('uv2')?.count).toBe(3);
15
+ });
16
+ test('replaces render attributes with the wrong item size', () => {
17
+ const geometry = new THREE.BufferGeometry();
18
+ geometry.setAttribute('position', new THREE.Float32BufferAttribute(new Float32Array([0, 0, 0, 1, 0, 0, 0, 1, 0]), 3));
19
+ geometry.setAttribute('uv', new THREE.Float32BufferAttribute(new Float32Array([0, 0, 0]), 1));
20
+ geometry.setAttribute('uv2', new THREE.Float32BufferAttribute(new Float32Array([0, 0, 0]), 1));
21
+ ensureRenderableGeometryAttributes(geometry);
22
+ expect(geometry.getAttribute('uv')?.itemSize).toBe(2);
23
+ expect(geometry.getAttribute('uv2')?.itemSize).toBe(2);
24
+ expect(geometry.getAttribute('uv')?.count).toBe(3);
25
+ expect(geometry.getAttribute('uv2')?.count).toBe(3);
26
+ });
27
+ test('copies uv into uv2 without depending on backing array layout', () => {
28
+ const geometry = new THREE.BufferGeometry();
29
+ geometry.setAttribute('position', new THREE.Float32BufferAttribute(new Float32Array([0, 0, 0, 1, 0, 0, 0, 1, 0]), 3));
30
+ geometry.setAttribute('uv', new THREE.InterleavedBufferAttribute(new THREE.InterleavedBuffer(new Float32Array([0, 0, 7, 1, 0, 8, 0, 1, 9]), 3), 2, 0));
31
+ ensureRenderableGeometryAttributes(geometry);
32
+ const uv2 = geometry.getAttribute('uv2');
33
+ expect(Array.from(uv2.array)).toEqual([0, 0, 1, 0, 0, 1]);
34
+ });
35
+ test('replaces empty geometries with a degenerate renderable triangle', () => {
36
+ const geometry = new THREE.BufferGeometry();
37
+ ensureRenderableGeometryAttributes(geometry);
38
+ expect(geometry.getIndex()).toBeNull();
39
+ expect(geometry.groups).toHaveLength(0);
40
+ expect(geometry.getAttribute('position')?.count).toBe(3);
41
+ expect(geometry.getAttribute('normal')?.count).toBe(3);
42
+ expect(geometry.getAttribute('uv')?.count).toBe(3);
43
+ expect(geometry.getAttribute('uv2')?.count).toBe(3);
44
+ });
45
+ });
@@ -1,3 +1,4 @@
1
1
  export type EdgeMode = 'off' | 'soft' | 'strong';
2
2
  export declare function edgeColorFor(background: string): string;
3
+ export declare function edgeOpacityScaleFor(background: string): number;
3
4
  //# sourceMappingURL=edge-style.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"edge-style.d.ts","sourceRoot":"","sources":["../../src/lib/edge-style.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,QAAQ,GAAG,KAAK,GAAG,MAAM,GAAG,QAAQ,CAAA;AAKhD,wBAAgB,YAAY,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAOvD"}
1
+ {"version":3,"file":"edge-style.d.ts","sourceRoot":"","sources":["../../src/lib/edge-style.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,QAAQ,GAAG,KAAK,GAAG,MAAM,GAAG,QAAQ,CAAA;AAgBhD,wBAAgB,YAAY,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAWvD;AAID,wBAAgB,mBAAmB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAE9D"}
@@ -1,11 +1,31 @@
1
- // Ink line colour follows background luminance — light backgrounds get
2
- // near-black lines, dark backgrounds get near-white. Same rule Mapbox uses for
3
- // label outlines, so edges stay legible across every scene theme.
4
- export function edgeColorFor(background) {
1
+ function lumaOf(background) {
5
2
  const hex = background.replace('#', '');
6
3
  const r = Number.parseInt(hex.slice(0, 2), 16) / 255;
7
4
  const g = Number.parseInt(hex.slice(2, 4), 16) / 255;
8
5
  const b = Number.parseInt(hex.slice(4, 6), 16) / 255;
9
- const luma = 0.2126 * r + 0.7152 * g + 0.0722 * b;
10
- return luma > 0.5 ? '#1a1d24' : '#dde2eb';
6
+ return 0.2126 * r + 0.7152 * g + 0.0722 * b;
7
+ }
8
+ // Ink line colour follows background luminance — light backgrounds get
9
+ // near-black lines (the rule Mapbox uses for label outlines). Dark backgrounds
10
+ // get a tint *derived from the background* rather than a near-white constant:
11
+ // full-white lines glow harshly against night/twilight scenes, while lifting
12
+ // the background's own hue toward white stays legible but reads native to the
13
+ // theme.
14
+ export function edgeColorFor(background) {
15
+ if (lumaOf(background) > 0.5)
16
+ return '#1a1d24';
17
+ const hex = background.replace('#', '');
18
+ let out = '#';
19
+ for (let i = 0; i < 3; i++) {
20
+ const c = Number.parseInt(hex.slice(i * 2, i * 2 + 2), 16) / 255;
21
+ out += Math.round((c + (1 - c) * 0.42) * 255)
22
+ .toString(16)
23
+ .padStart(2, '0');
24
+ }
25
+ return out;
26
+ }
27
+ // Dark scenes additionally run the ink slightly transparent — even a muted
28
+ // tint at full alpha reads as glowing wireframe against a low-luma backdrop.
29
+ export function edgeOpacityScaleFor(background) {
30
+ return lumaOf(background) > 0.5 ? 1 : 0.7;
11
31
  }
@@ -3,7 +3,7 @@ export declare function inkedEdges({ depthTex, normalTex, inkColor, radius, opac
3
3
  normalTex: any;
4
4
  inkColor: any;
5
5
  radius: number;
6
- opacity: number;
6
+ opacity: any;
7
7
  sceneRgb: any;
8
8
  }): any;
9
9
  //# sourceMappingURL=ink-edges.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"ink-edges.d.ts","sourceRoot":"","sources":["../../src/lib/ink-edges.ts"],"names":[],"mappings":"AA6BA,wBAAgB,UAAU,CAAC,EACzB,QAAQ,EACR,SAAS,EACT,QAAQ,EACR,MAAM,EACN,OAAO,EACP,QAAQ,GACT,EAAE;IACD,QAAQ,EAAE,GAAG,CAAA;IACb,SAAS,EAAE,GAAG,CAAA;IACd,QAAQ,EAAE,GAAG,CAAA;IAIb,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,EAAE,MAAM,CAAA;IACf,QAAQ,EAAE,GAAG,CAAA;CACd,OAgCA"}
1
+ {"version":3,"file":"ink-edges.d.ts","sourceRoot":"","sources":["../../src/lib/ink-edges.ts"],"names":[],"mappings":"AAoBA,wBAAgB,UAAU,CAAC,EACzB,QAAQ,EACR,SAAS,EACT,QAAQ,EACR,MAAM,EACN,OAAO,EACP,QAAQ,GACT,EAAE;IACD,QAAQ,EAAE,GAAG,CAAA;IACb,SAAS,EAAE,GAAG,CAAA;IACd,QAAQ,EAAE,GAAG,CAAA;IAKb,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,EAAE,GAAG,CAAA;IACZ,QAAQ,EAAE,GAAG,CAAA;CACd,OAsCA"}
@@ -1,4 +1,5 @@
1
- import { abs, colorToDirection, float, max, min, mix, screenSize, screenUV, smoothstep, vec2, } from 'three/tsl';
1
+ import { abs, float, max, min, mix, screenSize, screenUV, smoothstep, vec2 } from 'three/tsl';
2
+ import { unpackRGBToNormal } from './tsl-compat';
2
3
  // Screen-space ink outline (SketchUp / Moebius look). Reads the scene-pass
3
4
  // depth + normal MRT and inks two signals:
4
5
  //
@@ -29,15 +30,20 @@ export function inkedEdges({ depthTex, normalTex, inkColor, radius, opacity, sce
29
30
  const noiseGate = smoothstep(float(0.00002), float(0.00006), depthLap);
30
31
  // ≈ metres of step / near (near≈0.1): ~5cm starts a line, ~25cm solid.
31
32
  const depthEdge = smoothstep(float(0.5), float(2.5), depthMetric).mul(noiseGate);
32
- const nC = colorToDirection(normalTex.sample(uvN)).normalize();
33
- const nR = colorToDirection(normalTex.sample(uvN.add(vec2(px.x, 0)))).normalize();
34
- const nL = colorToDirection(normalTex.sample(uvN.sub(vec2(px.x, 0)))).normalize();
35
- const nU = colorToDirection(normalTex.sample(uvN.add(vec2(0, px.y)))).normalize();
36
- const nD = colorToDirection(normalTex.sample(uvN.sub(vec2(0, px.y)))).normalize();
33
+ const nC = unpackRGBToNormal(normalTex.sample(uvN)).normalize();
34
+ const nR = unpackRGBToNormal(normalTex.sample(uvN.add(vec2(px.x, 0)))).normalize();
35
+ const nL = unpackRGBToNormal(normalTex.sample(uvN.sub(vec2(px.x, 0)))).normalize();
36
+ const nU = unpackRGBToNormal(normalTex.sample(uvN.add(vec2(0, px.y)))).normalize();
37
+ const nD = unpackRGBToNormal(normalTex.sample(uvN.sub(vec2(0, px.y)))).normalize();
38
+ // Ink is a near/mid-field affordance: fade it out with raw depth so the
39
+ // horizon (the infinite ground disc vanishing against the backdrop) and
40
+ // other far-field depth cliffs never draw a line across the sky junction.
41
+ // ~full ink below ≈150 m, none beyond ≈350 m (perspective near 0.1/far 1000).
42
+ const distanceFade = float(1).sub(smoothstep(float(0.9994), float(0.9998), dC));
37
43
  const nDiff = max(max(float(1).sub(nC.dot(nR)), float(1).sub(nC.dot(nL))), max(float(1).sub(nC.dot(nU)), float(1).sub(nC.dot(nD))));
38
44
  const normalEdge = smoothstep(float(0.01), float(0.05), nDiff);
39
45
  // TSL's typed overloads are finicky across versions; the runtime is proven in
40
46
  // the aesthetic sandbox, so cast at the mask/mix boundary.
41
- const edgeMask = min(max(depthEdge, normalEdge).mul(opacity), float(1));
47
+ const edgeMask = min(max(depthEdge, normalEdge).mul(opacity).mul(distanceFade), float(1));
42
48
  return mix(sceneRgb, inkColor, edgeMask);
43
49
  }
@@ -0,0 +1,43 @@
1
+ import { KTX2Loader } from 'three/examples/jsm/loaders/KTX2Loader.js';
2
+ /**
3
+ * KTX2Loader that survives block-misaligned textures. WebGPU rejects
4
+ * block-compressed textures whose base dimensions aren't multiples of 4;
5
+ * three's KTX2Loader only warns and transcodes anyway, and the resulting
6
+ * invalid texture poisons every render pass that binds it (endless
7
+ * "Invalid BindGroup … Invalid CommandBuffer" spam — baked GLBs carrying an
8
+ * odd-sized user-item texture triggered exactly this). Such payloads are
9
+ * routed to a fallback loader whose support flags report no compressed
10
+ * formats, which makes the transcoder emit plain RGBA32 — uncompressed
11
+ * textures have no alignment requirement.
12
+ */
13
+ declare class AlignmentSafeKTX2Loader extends KTX2Loader {
14
+ private rgbaFallback;
15
+ private fallbackLoader;
16
+ /** Both `load()` and GLTFLoader's KHR_texture_basisu path funnel through
17
+ * this internal — overriding it covers every entry point. */
18
+ _createTexture(buffer: ArrayBuffer, config?: Record<string, unknown>): Promise<unknown>;
19
+ dispose(): this;
20
+ }
21
+ /**
22
+ * Single shared KTX2 loader for the whole viewer — used both by the GLB loader
23
+ * (`use-gltf-ktx2`) and by catalog finish textures (`materials.ts`). KTX2 must
24
+ * be transcoded at load via the Basis WASM, and `detectSupport(renderer)` has to
25
+ * run once before any `.ktx2` is loaded so the loader picks a GPU format the
26
+ * device supports. `ensureKtx2Support` is idempotent per renderer and is called
27
+ * from the viewer root the moment the renderer is ready (even when no GLB is in
28
+ * the scene, so catalog `.ktx2` finishes still load).
29
+ */
30
+ export declare const ktx2Loader: AlignmentSafeKTX2Loader;
31
+ /**
32
+ * Resolves once `detectSupport` has succeeded for any renderer. `.ktx2` loads
33
+ * issued before that point would throw inside KTX2Loader ("Missing
34
+ * initialization with `.detectSupport( renderer )`"), so texture loaders await
35
+ * this instead of failing — covers materials created while the renderer is
36
+ * still initializing (e.g. a standalone capture canvas).
37
+ */
38
+ export declare function whenKtx2Ready(): Promise<void>;
39
+ /** Returns true once support has been detected for this renderer (KTX2 safe to load). */
40
+ export declare function ensureKtx2Support(renderer: unknown): boolean;
41
+ export declare function isKtx2Url(url: string): boolean;
42
+ export {};
43
+ //# sourceMappingURL=ktx2-loader.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ktx2-loader.d.ts","sourceRoot":"","sources":["../../src/lib/ktx2-loader.ts"],"names":[],"mappings":"AAWA,OAAO,EAAE,UAAU,EAAE,MAAM,0CAA0C,CAAA;AAwBrE;;;;;;;;;;GAUG;AACH,cAAM,uBAAwB,SAAQ,UAAU;IAC9C,OAAO,CAAC,YAAY,CAA0B;IAE9C,OAAO,CAAC,cAAc;IAkBtB;kEAC8D;IACxD,cAAc,CAAC,MAAM,EAAE,WAAW,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC;IAmC7F,OAAO,IAAI,IAAI;CAMhB;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,UAAU,yBAAgC,CAAA;AAWvD;;;;;;GAMG;AACH,wBAAgB,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC,CAE7C;AAED,yFAAyF;AACzF,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,OAAO,GAAG,OAAO,CAkB5D;AAED,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAE9C"}
@@ -0,0 +1,136 @@
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 isKtx2Url(url) {
135
+ return url.toLowerCase().endsWith('.ktx2');
136
+ }
@@ -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;