@pascal-app/viewer 1.0.3 → 1.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (276) hide show
  1. package/dist/capture/capture-runtime.js +1 -1
  2. package/dist/components/error-boundary.d.ts.map +1 -1
  3. package/dist/components/renderers/node-renderer.d.ts +1 -1
  4. package/dist/components/renderers/node-renderer.d.ts.map +1 -1
  5. package/dist/components/renderers/node-renderer.js +12 -4
  6. package/dist/components/renderers/parametric-node-renderer.d.ts.map +1 -1
  7. package/dist/components/viewer/bvh-ecctrl.d.ts.map +1 -1
  8. package/dist/components/viewer/frame-limiter.d.ts +1 -0
  9. package/dist/components/viewer/frame-limiter.d.ts.map +1 -1
  10. package/dist/components/viewer/frame-limiter.js +22 -3
  11. package/dist/components/viewer/glb-interactive.d.ts +25 -1
  12. package/dist/components/viewer/glb-interactive.d.ts.map +1 -1
  13. package/dist/components/viewer/glb-interactive.js +253 -86
  14. package/dist/components/viewer/glb-mechanisms.d.ts +10 -0
  15. package/dist/components/viewer/glb-mechanisms.d.ts.map +1 -0
  16. package/dist/components/viewer/glb-mechanisms.js +19 -0
  17. package/dist/components/viewer/glb-reference-nodes.js +1 -1
  18. package/dist/components/viewer/glb-replace-instances.js +1 -1
  19. package/dist/components/viewer/glb-scene.d.ts +45 -27
  20. package/dist/components/viewer/glb-scene.d.ts.map +1 -1
  21. package/dist/components/viewer/glb-scene.js +421 -508
  22. package/dist/components/viewer/glb-walkthrough-controller.d.ts +1 -1
  23. package/dist/components/viewer/glb-walkthrough-controller.d.ts.map +1 -1
  24. package/dist/components/viewer/glb-walkthrough-controller.js +13 -2
  25. package/dist/components/viewer/index.d.ts +24 -1
  26. package/dist/components/viewer/index.d.ts.map +1 -1
  27. package/dist/components/viewer/index.js +111 -37
  28. package/dist/components/viewer/lights.d.ts.map +1 -1
  29. package/dist/components/viewer/lights.js +113 -22
  30. package/dist/components/viewer/perf-monitor.d.ts.map +1 -1
  31. package/dist/components/viewer/perf-monitor.js +6 -0
  32. package/dist/components/viewer/perf-panel.d.ts.map +1 -1
  33. package/dist/components/viewer/perf-panel.js +1 -1
  34. package/dist/components/viewer/post-processing.d.ts.map +1 -1
  35. package/dist/components/viewer/post-processing.js +104 -36
  36. package/dist/components/viewer/registered-systems.d.ts.map +1 -1
  37. package/dist/components/viewer/registered-systems.js +17 -3
  38. package/dist/components/viewer/render-error.d.ts +18 -0
  39. package/dist/components/viewer/render-error.d.ts.map +1 -0
  40. package/dist/components/viewer/render-error.js +29 -0
  41. package/dist/components/viewer/scene-bvh.d.ts.map +1 -1
  42. package/dist/components/viewer/scene-bvh.js +1 -1
  43. package/dist/components/viewer/scene-environment.d.ts.map +1 -1
  44. package/dist/components/viewer/scene-environment.js +31 -11
  45. package/dist/components/viewer/scene-ground-replacement.d.ts +6 -0
  46. package/dist/components/viewer/scene-ground-replacement.d.ts.map +1 -1
  47. package/dist/components/viewer/scene-ground-replacement.js +15 -0
  48. package/dist/components/viewer/selection-manager.d.ts.map +1 -1
  49. package/dist/components/viewer/selection-manager.js +25 -7
  50. package/dist/components/viewer/underground-ambiance.d.ts +9 -0
  51. package/dist/components/viewer/underground-ambiance.d.ts.map +1 -0
  52. package/dist/components/viewer/underground-ambiance.js +105 -0
  53. package/dist/components/viewer/viewer-camera.d.ts +4 -0
  54. package/dist/components/viewer/viewer-camera.d.ts.map +1 -1
  55. package/dist/components/viewer/viewer-camera.js +12 -2
  56. package/dist/components/viewer/viewer-presentations.d.ts.map +1 -1
  57. package/dist/hooks/use-gltf-ktx2.d.ts.map +1 -1
  58. package/dist/hooks/use-gltf-ktx2.js +2 -0
  59. package/dist/hooks/use-node-events.d.ts.map +1 -1
  60. package/dist/index.d.ts +36 -16
  61. package/dist/index.d.ts.map +1 -1
  62. package/dist/index.js +34 -19
  63. package/dist/lib/asset-url.d.ts.map +1 -1
  64. package/dist/lib/asset-url.js +7 -1
  65. package/dist/lib/backdrop.d.ts +17 -0
  66. package/dist/lib/backdrop.d.ts.map +1 -1
  67. package/dist/lib/backdrop.js +18 -1
  68. package/dist/lib/construction-dust.d.ts +57 -0
  69. package/dist/lib/construction-dust.d.ts.map +1 -0
  70. package/dist/lib/construction-dust.js +187 -0
  71. package/dist/lib/csg-utils.js +1 -1
  72. package/dist/lib/display-state.d.ts +25 -0
  73. package/dist/lib/display-state.d.ts.map +1 -0
  74. package/dist/lib/display-state.js +1 -0
  75. package/dist/lib/foundation-materials.d.ts +26 -0
  76. package/dist/lib/foundation-materials.d.ts.map +1 -0
  77. package/dist/lib/foundation-materials.js +65 -0
  78. package/dist/lib/gltf-unlit.d.ts +10 -0
  79. package/dist/lib/gltf-unlit.d.ts.map +1 -0
  80. package/dist/lib/gltf-unlit.js +23 -0
  81. package/dist/lib/ground-window.d.ts +33 -0
  82. package/dist/lib/ground-window.d.ts.map +1 -0
  83. package/dist/lib/ground-window.js +114 -0
  84. package/dist/lib/hero-pose.d.ts +13 -3
  85. package/dist/lib/hero-pose.d.ts.map +1 -1
  86. package/dist/lib/hero-pose.js +72 -5
  87. package/dist/lib/ink-edges.d.ts +6 -3
  88. package/dist/lib/ink-edges.d.ts.map +1 -1
  89. package/dist/lib/ink-edges.js +17 -7
  90. package/dist/lib/key-light-override.d.ts +11 -0
  91. package/dist/lib/key-light-override.d.ts.map +1 -0
  92. package/dist/lib/key-light-override.js +14 -0
  93. package/dist/lib/ktx2-loader.d.ts +7 -0
  94. package/dist/lib/ktx2-loader.d.ts.map +1 -1
  95. package/dist/lib/ktx2-loader.js +35 -0
  96. package/dist/lib/layer-pass.d.ts.map +1 -1
  97. package/dist/lib/layers.d.ts +11 -7
  98. package/dist/lib/layers.d.ts.map +1 -1
  99. package/dist/lib/layers.js +16 -6
  100. package/dist/lib/live-frame-hold.d.ts +14 -0
  101. package/dist/lib/live-frame-hold.d.ts.map +1 -0
  102. package/dist/lib/live-frame-hold.js +28 -0
  103. package/dist/lib/materials.d.ts +19 -7
  104. package/dist/lib/materials.d.ts.map +1 -1
  105. package/dist/lib/materials.js +65 -9
  106. package/dist/lib/merged-outline-node.d.ts +5 -0
  107. package/dist/lib/merged-outline-node.d.ts.map +1 -1
  108. package/dist/lib/merged-outline-node.js +80 -2
  109. package/dist/lib/node-top-surface-height.d.ts +6 -0
  110. package/dist/lib/node-top-surface-height.d.ts.map +1 -0
  111. package/dist/lib/node-top-surface-height.js +74 -0
  112. package/dist/lib/perf-probe-state.d.ts +39 -0
  113. package/dist/lib/perf-probe-state.d.ts.map +1 -0
  114. package/dist/lib/perf-probe-state.js +17 -0
  115. package/dist/lib/pointer-events.d.ts +26 -2
  116. package/dist/lib/pointer-events.d.ts.map +1 -1
  117. package/dist/lib/pointer-events.js +99 -12
  118. package/dist/lib/post-processing-resources.d.ts.map +1 -1
  119. package/dist/lib/procedural-emission.d.ts +7 -0
  120. package/dist/lib/procedural-emission.d.ts.map +1 -0
  121. package/dist/lib/procedural-emission.js +54 -0
  122. package/dist/lib/reveal-candidates.d.ts +80 -0
  123. package/dist/lib/reveal-candidates.d.ts.map +1 -0
  124. package/dist/lib/reveal-candidates.js +524 -0
  125. package/dist/lib/reveal-ghost.d.ts +9 -0
  126. package/dist/lib/reveal-ghost.d.ts.map +1 -0
  127. package/dist/lib/reveal-ghost.js +62 -0
  128. package/dist/lib/reveal-pending.d.ts +12 -0
  129. package/dist/lib/reveal-pending.d.ts.map +1 -0
  130. package/dist/lib/reveal-pending.js +12 -0
  131. package/dist/lib/reveal-pose.d.ts +70 -0
  132. package/dist/lib/reveal-pose.d.ts.map +1 -0
  133. package/dist/lib/reveal-pose.js +248 -0
  134. package/dist/lib/reveal-schedule.d.ts +208 -0
  135. package/dist/lib/reveal-schedule.d.ts.map +1 -0
  136. package/dist/lib/reveal-schedule.js +410 -0
  137. package/dist/lib/reveal-texture-scale.d.ts +5 -0
  138. package/dist/lib/reveal-texture-scale.d.ts.map +1 -0
  139. package/dist/lib/reveal-texture-scale.js +113 -0
  140. package/dist/lib/reveal-windows.d.ts +23 -0
  141. package/dist/lib/reveal-windows.d.ts.map +1 -0
  142. package/dist/lib/reveal-windows.js +31 -0
  143. package/dist/lib/reveal-wipe.d.ts +71 -0
  144. package/dist/lib/reveal-wipe.d.ts.map +1 -0
  145. package/dist/lib/reveal-wipe.js +105 -0
  146. package/dist/lib/scene-pass-mrt.d.ts +12 -0
  147. package/dist/lib/scene-pass-mrt.d.ts.map +1 -0
  148. package/dist/lib/scene-pass-mrt.js +23 -0
  149. package/dist/lib/scene-support-height.d.ts +4 -0
  150. package/dist/lib/scene-support-height.d.ts.map +1 -0
  151. package/dist/lib/scene-support-height.js +34 -0
  152. package/dist/lib/scene-themes.d.ts +30 -8
  153. package/dist/lib/scene-themes.d.ts.map +1 -1
  154. package/dist/lib/scene-themes.js +36 -0
  155. package/dist/lib/scene-visibility.d.ts +17 -1
  156. package/dist/lib/scene-visibility.d.ts.map +1 -1
  157. package/dist/lib/scene-visibility.js +53 -3
  158. package/dist/lib/scripted-opening.d.ts +11 -0
  159. package/dist/lib/scripted-opening.d.ts.map +1 -0
  160. package/dist/lib/scripted-opening.js +20 -0
  161. package/dist/lib/site-surfaces.d.ts +32 -0
  162. package/dist/lib/site-surfaces.d.ts.map +1 -0
  163. package/dist/lib/site-surfaces.js +70 -0
  164. package/dist/lib/snapshot-pipeline.d.ts +34 -3
  165. package/dist/lib/snapshot-pipeline.d.ts.map +1 -1
  166. package/dist/lib/snapshot-pipeline.js +247 -117
  167. package/dist/lib/terrain-perimeter-fill.d.ts.map +1 -1
  168. package/dist/lib/terrain-perimeter-fill.js +22 -14
  169. package/dist/lib/tsl-compat.d.ts +2 -2
  170. package/dist/lib/tsl-compat.d.ts.map +1 -1
  171. package/dist/lib/underground-ambiance.d.ts +168 -0
  172. package/dist/lib/underground-ambiance.d.ts.map +1 -0
  173. package/dist/lib/underground-ambiance.js +334 -0
  174. package/dist/lib/zone-geometry.d.ts +11 -0
  175. package/dist/lib/zone-geometry.d.ts.map +1 -0
  176. package/dist/lib/zone-geometry.js +46 -0
  177. package/dist/store/use-item-light-pool.d.ts +14 -10
  178. package/dist/store/use-item-light-pool.d.ts.map +1 -1
  179. package/dist/store/use-item-light-pool.js +52 -26
  180. package/dist/store/use-viewer.d.ts +194 -26
  181. package/dist/store/use-viewer.d.ts.map +1 -1
  182. package/dist/store/use-viewer.js +164 -21
  183. package/dist/systems/ceiling/ceiling-system.d.ts +15 -1
  184. package/dist/systems/ceiling/ceiling-system.d.ts.map +1 -1
  185. package/dist/systems/ceiling/ceiling-system.js +88 -6
  186. package/dist/systems/construction-reveal/construction-dust.d.ts +16 -0
  187. package/dist/systems/construction-reveal/construction-dust.d.ts.map +1 -0
  188. package/dist/systems/construction-reveal/construction-dust.js +92 -0
  189. package/dist/systems/construction-reveal/construction-reveal.d.ts +276 -0
  190. package/dist/systems/construction-reveal/construction-reveal.d.ts.map +1 -0
  191. package/dist/systems/construction-reveal/construction-reveal.js +2383 -0
  192. package/dist/systems/construction-reveal/dust-material.d.ts +10 -0
  193. package/dist/systems/construction-reveal/dust-material.d.ts.map +1 -0
  194. package/dist/systems/construction-reveal/dust-material.js +17 -0
  195. package/dist/systems/construction-reveal/ghost-dress.d.ts +10 -0
  196. package/dist/systems/construction-reveal/ghost-dress.d.ts.map +1 -0
  197. package/dist/systems/construction-reveal/ghost-dress.js +24 -0
  198. package/dist/systems/door/door-system.d.ts.map +1 -1
  199. package/dist/systems/door/door-system.js +55 -17
  200. package/dist/systems/elevator/elevator-interaction-system.d.ts +7 -0
  201. package/dist/systems/elevator/elevator-interaction-system.d.ts.map +1 -1
  202. package/dist/systems/elevator/elevator-interaction-system.js +18 -1
  203. package/dist/systems/floor-elevation/floor-elevation-system.d.ts +3 -1
  204. package/dist/systems/floor-elevation/floor-elevation-system.d.ts.map +1 -1
  205. package/dist/systems/floor-elevation/floor-elevation-system.js +33 -5
  206. package/dist/systems/geometry/geometry-system.d.ts.map +1 -1
  207. package/dist/systems/geometry/geometry-system.js +71 -11
  208. package/dist/systems/interactive/control-widget.d.ts.map +1 -1
  209. package/dist/systems/interactive/interactive-system.d.ts.map +1 -1
  210. package/dist/systems/interactive/interactive-system.js +52 -17
  211. package/dist/systems/interactive/procedural-controls.d.ts +13 -0
  212. package/dist/systems/interactive/procedural-controls.d.ts.map +1 -0
  213. package/dist/systems/interactive/procedural-controls.js +17 -0
  214. package/dist/systems/interactive/scripted-clips.d.ts +19 -0
  215. package/dist/systems/interactive/scripted-clips.d.ts.map +1 -0
  216. package/dist/systems/interactive/scripted-clips.js +134 -0
  217. package/dist/systems/item/item-system.d.ts.map +1 -1
  218. package/dist/systems/item/item-system.js +9 -3
  219. package/dist/systems/item/object-cut-invalidation.d.ts +3 -0
  220. package/dist/systems/item/object-cut-invalidation.d.ts.map +1 -0
  221. package/dist/systems/item/object-cut-invalidation.js +68 -0
  222. package/dist/systems/item-light/item-light-system.d.ts +1 -1
  223. package/dist/systems/item-light/item-light-system.d.ts.map +1 -1
  224. package/dist/systems/item-light/item-light-system.js +48 -53
  225. package/dist/systems/level/level-hold.d.ts +4 -0
  226. package/dist/systems/level/level-hold.d.ts.map +1 -0
  227. package/dist/systems/level/level-hold.js +14 -0
  228. package/dist/systems/level/level-system.d.ts.map +1 -1
  229. package/dist/systems/level/level-system.js +19 -6
  230. package/dist/systems/level/level-utils.d.ts +19 -6
  231. package/dist/systems/level/level-utils.d.ts.map +1 -1
  232. package/dist/systems/level/level-utils.js +68 -9
  233. package/dist/systems/level/shadow-only-levels.d.ts +9 -0
  234. package/dist/systems/level/shadow-only-levels.d.ts.map +1 -0
  235. package/dist/systems/level/shadow-only-levels.js +7 -0
  236. package/dist/systems/roof/__fixtures__/roof-plane-cases.d.ts +103 -0
  237. package/dist/systems/roof/__fixtures__/roof-plane-cases.d.ts.map +1 -0
  238. package/dist/systems/roof/__fixtures__/roof-plane-cases.js +317 -0
  239. package/dist/systems/roof/roof-materials.d.ts +16 -2
  240. package/dist/systems/roof/roof-materials.d.ts.map +1 -1
  241. package/dist/systems/roof/roof-materials.js +64 -31
  242. package/dist/systems/roof/roof-system.d.ts +21 -4
  243. package/dist/systems/roof/roof-system.d.ts.map +1 -1
  244. package/dist/systems/roof/roof-system.js +374 -49
  245. package/dist/systems/slab/slab-system.d.ts +1 -1
  246. package/dist/systems/slab/slab-system.d.ts.map +1 -1
  247. package/dist/systems/slab/slab-system.js +49 -19
  248. package/dist/systems/stair/stair-system.d.ts.map +1 -1
  249. package/dist/systems/stair/stair-system.js +133 -354
  250. package/dist/systems/wall/wall-cutout-cache.d.ts +39 -3
  251. package/dist/systems/wall/wall-cutout-cache.d.ts.map +1 -1
  252. package/dist/systems/wall/wall-cutout-cache.js +141 -19
  253. package/dist/systems/wall/wall-cutout.d.ts +6 -0
  254. package/dist/systems/wall/wall-cutout.d.ts.map +1 -1
  255. package/dist/systems/wall/wall-cutout.js +42 -28
  256. package/dist/systems/wall/wall-finish-data.d.ts +22 -0
  257. package/dist/systems/wall/wall-finish-data.d.ts.map +1 -0
  258. package/dist/systems/wall/wall-finish-data.js +17 -0
  259. package/dist/systems/wall/wall-material-variant.d.ts.map +1 -1
  260. package/dist/systems/wall/wall-materials.d.ts +9 -4
  261. package/dist/systems/wall/wall-materials.d.ts.map +1 -1
  262. package/dist/systems/wall/wall-materials.js +135 -153
  263. package/dist/systems/wall/wall-placeholder-sweep.d.ts.map +1 -1
  264. package/dist/systems/wall/wall-system.d.ts +33 -2
  265. package/dist/systems/wall/wall-system.d.ts.map +1 -1
  266. package/dist/systems/wall/wall-system.js +621 -159
  267. package/dist/systems/window/corner-frame.d.ts +10 -0
  268. package/dist/systems/window/corner-frame.d.ts.map +1 -0
  269. package/dist/systems/window/corner-frame.js +17 -0
  270. package/dist/systems/window/window-system.d.ts +1 -1
  271. package/dist/systems/window/window-system.d.ts.map +1 -1
  272. package/dist/systems/window/window-system.js +71 -27
  273. package/package.json +10 -10
  274. package/dist/systems/fence/fence-system.d.ts +0 -22
  275. package/dist/systems/fence/fence-system.d.ts.map +0 -1
  276. package/dist/systems/fence/fence-system.js +0 -357
@@ -1,4 +1,4 @@
1
- import { getMaterialPresetByRef, parseMaterialRef, resolveMaterial, } from '@pascal-app/core';
1
+ import { getMaterialPresetByRef, materialColorPaint, parseMaterialColor, parseMaterialRef, resolveMaterial, } from '@pascal-app/core';
2
2
  import { addAfterEffect, invalidate } from '@react-three/fiber';
3
3
  import * as THREE from 'three';
4
4
  import { float, mix, positionViewDirection, transformedNormalView } from 'three/tsl';
@@ -82,12 +82,35 @@ const defaultMaterialCache = new Map();
82
82
  const surfaceRoleMaterialCache = new Map();
83
83
  const textureCache = new Map();
84
84
  const textureLoadPromises = new Map();
85
+ const pendingTextures = new Set();
86
+ const failedTextures = new Set();
85
87
  const textureLoader = new THREE.TextureLoader();
86
88
  let materialTextureVersion = 0;
87
89
  // Highlight clones can observe late assignments without polling every material.
88
90
  export function getMaterialTextureVersion() {
89
91
  return materialTextureVersion;
90
92
  }
93
+ /** Conservative capture readiness across the current material cache. */
94
+ export function materialTextureLoadState() {
95
+ return { pending: pendingTextures.size + textureLoadPromises.size, failed: failedTextures.size };
96
+ }
97
+ function loadTexture(url, key) {
98
+ pendingTextures.add(key);
99
+ try {
100
+ return pickTextureLoader(url).load(url, () => {
101
+ pendingTextures.delete(key);
102
+ failedTextures.delete(key);
103
+ }, undefined, () => {
104
+ pendingTextures.delete(key);
105
+ failedTextures.add(key);
106
+ });
107
+ }
108
+ catch (error) {
109
+ pendingTextures.delete(key);
110
+ failedTextures.add(key);
111
+ throw error;
112
+ }
113
+ }
91
114
  // `.ktx2` finish maps transcode through the shared KTX2 loader (support is
92
115
  // detected once at viewer init); everything else loads as a normal image.
93
116
  function pickTextureLoader(url) {
@@ -157,7 +180,7 @@ function getTexture(material) {
157
180
  const resolvedUrl = /^(?:asset|blob|data):/.test(textureConfig.url)
158
181
  ? textureConfig.url
159
182
  : (resolveCdnUrl(textureConfig.url) ?? textureConfig.url);
160
- const texture = pickTextureLoader(resolvedUrl).load(resolvedUrl);
183
+ const texture = loadTexture(resolvedUrl, cacheKey);
161
184
  texture.wrapS = THREE.RepeatWrapping;
162
185
  texture.wrapT = THREE.RepeatWrapping;
163
186
  const [repeatX, repeatY] = resolveTextureRepeat(textureConfig.repeat, textureConfig.scale);
@@ -207,7 +230,7 @@ function getPresetTexture(path, props, slot) {
207
230
  const cached = textureCache.get(cacheKey);
208
231
  if (cached)
209
232
  return cached;
210
- const texture = pickTextureLoader(resolvedPath).load(resolvedPath);
233
+ const texture = loadTexture(resolvedPath, cacheKey);
211
234
  applyTextureProperties(texture, props, slot);
212
235
  stampPascalTextureRef(texture, {
213
236
  kind: 'material',
@@ -263,11 +286,13 @@ async function loadPresetTexture(path, props, slot) {
263
286
  setTextureCacheKey(texture, cacheKey);
264
287
  textureCache.set(cacheKey, texture);
265
288
  textureLoadPromises.delete(cacheKey);
289
+ failedTextures.delete(cacheKey);
266
290
  return texture;
267
291
  })
268
292
  .catch((error) => {
269
293
  console.warn('[viewer] Failed to load material texture', resolvedPath, error);
270
294
  textureLoadPromises.delete(cacheKey);
295
+ failedTextures.add(cacheKey);
271
296
  return null;
272
297
  });
273
298
  textureLoadPromises.set(cacheKey, promise);
@@ -346,6 +371,12 @@ function applyMaterialMapProperties(material, mapProperties) {
346
371
  // Glass-like transparency threshold: any standard material authored as
347
372
  // `transparent` with opacity below this gets the fresnel treatment.
348
373
  const GLASS_OPACITY_THRESHOLD = 0.6;
374
+ /** Glass lets light through; shadow maps would otherwise treat it as a solid panel. */
375
+ export function materialCastsShadow(material) {
376
+ const materials = Array.isArray(material) ? material : [material];
377
+ return !materials.some((entry) => entry.transmission > 0 ||
378
+ (entry.transparent && entry.opacity < GLASS_OPACITY_THRESHOLD));
379
+ }
349
380
  /**
350
381
  * Fresnel-driven opacity for glass: nearly the authored opacity head-on,
351
382
  * increasingly opaque (showing the environment reflection) at grazing angles.
@@ -413,7 +444,8 @@ export function createMaterialFromPresetRef(materialPreset, shading = 'rendered'
413
444
  }
414
445
  export function createMaterial(material, shading = 'rendered') {
415
446
  const props = resolveMaterial(material);
416
- const cacheKey = `${getCacheKey(props, shading)}-${getTextureKey(material)}`;
447
+ const glassPreset = material?.preset === 'glass';
448
+ const cacheKey = `${getCacheKey(props, shading)}-${getTextureKey(material)}-${glassPreset}`;
417
449
  if (materialCache.has(cacheKey)) {
418
450
  return materialCache.get(cacheKey);
419
451
  }
@@ -433,7 +465,9 @@ export function createMaterial(material, shading = 'rendered') {
433
465
  roughness: props.roughness,
434
466
  metalness: props.metalness,
435
467
  });
436
- maybeApplyGlassFresnel(threeMaterial);
468
+ // Explicit source transparency keeps its opacity; the glass preset owns this styling.
469
+ if (glassPreset)
470
+ maybeApplyGlassFresnel(threeMaterial);
437
471
  threeMaterial.userData.__pascalCachedMaterial = true;
438
472
  materialCache.set(cacheKey, threeMaterial);
439
473
  return threeMaterial;
@@ -446,14 +480,19 @@ export function createMaterial(material, shading = 'rendered') {
446
480
  * the dangling-ref fallback for the whole session.
447
481
  */
448
482
  export function materialPresetRefSignature(ref) {
449
- return getMaterialPresetByRef(ref) ? ref : `${ref}#unresolved`;
483
+ return getMaterialPresetByRef(ref) || parseMaterialColor(ref) ? ref : `${ref}#unresolved`;
450
484
  }
451
485
  /**
452
- * Resolve a MaterialRef ('library:<id>' | 'scene:<id>') to a three.js material.
453
- * Returns null for an unknown / dangling ref so callers fall back to the
454
- * slot's default (authored material, then themed default). Never throws.
486
+ * Resolve a MaterialRef ('library:<id>' | 'scene:<id>') or a plain '#rrggbb'
487
+ * colour to a three.js material; the colour paints like
488
+ * `material: { properties: { color } }`. Returns null for an unknown / dangling
489
+ * ref so callers fall back to the slot's default (authored material, then
490
+ * themed default). Never throws.
455
491
  */
456
492
  export function resolveMaterialRef(ref, sceneMaterials, shading = 'rendered') {
493
+ const color = parseMaterialColor(ref);
494
+ if (color)
495
+ return createMaterial(materialColorPaint(color), shading);
457
496
  const parsed = parseMaterialRef(ref);
458
497
  if (!parsed)
459
498
  return null;
@@ -470,7 +509,22 @@ export function resolveMaterialRef(ref, sceneMaterials, shading = 'rendered') {
470
509
  * procedural kinds whose colored-mode unpainted appearance comes from a
471
510
  * declarative default (slab, wall).
472
511
  */
512
+ let slotDefaultOverrides = null;
513
+ /**
514
+ * Swaps declared slot DEFAULTS (never painted refs) for another finish, e.g.
515
+ * the worker thumbnail renders unpainted walls as plaster instead of the
516
+ * prepared-drywall default. Must be set before the scene builds its
517
+ * materials; `null` restores the declared defaults.
518
+ */
519
+ export function setSlotDefaultOverrides(overrides) {
520
+ slotDefaultOverrides = overrides;
521
+ }
522
+ /** A declared slot default after `setSlotDefaultOverrides`. */
523
+ export function resolveSlotDefaultRef(slotDefault) {
524
+ return slotDefaultOverrides?.[slotDefault] ?? slotDefault;
525
+ }
473
526
  export function resolveSlotDefaultMaterial(slotDefault, shading = 'rendered', roughness = 0.9) {
527
+ slotDefault = resolveSlotDefaultRef(slotDefault);
474
528
  if (parseMaterialRef(slotDefault)?.kind === 'library') {
475
529
  return (createMaterialFromPresetRef(slotDefault, shading) ??
476
530
  cachedDefaultMaterial(`slot-#ffffff-${roughness}`, '#ffffff', roughness, shading));
@@ -604,6 +658,8 @@ export function clearMaterialCache() {
604
658
  surfaceRoleMaterialCache.clear();
605
659
  textureCache.clear();
606
660
  textureLoadPromises.clear();
661
+ pendingTextures.clear();
662
+ failedTextures.clear();
607
663
  const disposals = [];
608
664
  for (const cleanup of materialCacheCleanups) {
609
665
  const dispose = cleanup();
@@ -63,6 +63,10 @@ export declare class MergedOutlineNode extends TempNode {
63
63
  private readonly _proxiesA;
64
64
  private readonly _proxiesB;
65
65
  private readonly _proxyMaskMaterials;
66
+ private readonly _cutoutMasksA;
67
+ private readonly _cutoutMasksB;
68
+ private readonly _usedCutoutMasksA;
69
+ private readonly _usedCutoutMasksB;
66
70
  private readonly _maskSceneA;
67
71
  private readonly _maskSceneB;
68
72
  private _wroteGroupALastFrame;
@@ -90,6 +94,7 @@ export declare class MergedOutlineNode extends TempNode {
90
94
  private _syncProxies;
91
95
  private _supportsProxies;
92
96
  private _disposeProxyMaterial;
97
+ private _pruneCutoutMasks;
93
98
  private _runEdgePipeline;
94
99
  setup(builder: any): any;
95
100
  dispose(): void;
@@ -1 +1 @@
1
- {"version":3,"file":"merged-outline-node.d.ts","sourceRoot":"","sources":["../../src/lib/merged-outline-node.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"merged-outline-node.d.ts","sourceRoot":"","sources":["../../src/lib/merged-outline-node.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;GAUG;AAEH,OAAO,EAIL,QAAQ,EAKT,MAAM,OAAO,CAAA;AA2Bd,OAAO,EAML,QAAQ,EACT,MAAM,cAAc,CAAA;AAiErB,qBAAa,iBAAkB,SAAQ,QAAQ;IAC7C,MAAM,KAAK,IAAI,WAEd;IAED,KAAK,EAAE,GAAG,CAAA;IACV,MAAM,EAAE,GAAG,CAAA;IACX,cAAc,EAAE,QAAQ,EAAE,CAAA;IAC1B,gBAAgB,EAAE,QAAQ,EAAE,CAAA;IAC5B,wBAAwB,EAAE,GAAG,CAAA;IAC7B,0BAA0B,EAAE,GAAG,CAAA;IAC/B,mBAAmB,EAAE,GAAG,CAAA;IACxB,qBAAqB,EAAE,GAAG,CAAA;IAC1B,eAAe,EAAE,MAAM,CAAA;IACvB,gBAAgB,EAAE,MAAM,CAAA;IAExB,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAK;IACrC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAc;IACvC,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAK;IAEtC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAc;IACtC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAc;IACtC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAK;IAC/B,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAK;IACnC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAK;IAChC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAK;IAChC,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAK;IACpC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAK;IAC/B,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAK;IACnC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAK;IAChC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAK;IAChC,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAK;IACpC,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAK;IACrC,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAK;IACrC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAK;IACjC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAK;IAEhC,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAc;IAC7C,OAAO,CAAC,QAAQ,CAAC,oBAAoB,CAAoB;IACzD,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAc;IAC/C,OAAO,CAAC,QAAQ,CAAC,sBAAsB,CAAoB;IAC3D,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAc;IACxC,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAc;IAC9C,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAc;IACzC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAc;IACzC,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAc;IAC7C,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAc;IAC/C,OAAO,CAAC,QAAQ,CAAC,sBAAsB,CAAoB;IAC3D,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAc;IACxC,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAc;IAC9C,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAc;IACzC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAc;IACzC,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAc;IAE7C,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAsB;IAC9C,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAsB;IAC9C,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAqC;IAC/D,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAqC;IAC/D,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAA6C;IACjF,OAAO,CAAC,QAAQ,CAAC,aAAa,CAA+B;IAC7D,OAAO,CAAC,QAAQ,CAAC,aAAa,CAA+B;IAC7D,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAiB;IACnD,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAiB;IACnD,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAc;IAC1C,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAc;IAO1C,OAAO,CAAC,qBAAqB,CAAQ;IACrC,OAAO,CAAC,qBAAqB,CAAQ;IAErC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAK;IACnC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAK;IAEnC,YACE,KAAK,EAAE,GAAG,EACV,MAAM,EAAE,GAAG,EACX,MAAM,GAAE;QACN,cAAc,CAAC,EAAE,QAAQ,EAAE,CAAA;QAC3B,gBAAgB,CAAC,EAAE,QAAQ,EAAE,CAAA;QAC7B,oBAAoB,CAAC,EAAE,GAAG,CAAA;QAC1B,sBAAsB,CAAC,EAAE,GAAG,CAAA;QAC5B,eAAe,CAAC,EAAE,GAAG,CAAA;QACrB,iBAAiB,CAAC,EAAE,GAAG,CAAA;QACvB,eAAe,CAAC,EAAE,MAAM,CAAA;QACxB,sEAAsE;QACtE,cAAc,CAAC,EAAE,GAAG,CAAA;KAChB,EAgGP;IAED,IAAI,kBAAkB,QAErB;IACD,IAAI,iBAAiB,QAEpB;IACD,IAAI,oBAAoB,QAEvB;IACD,IAAI,mBAAmB,QAEtB;IAED,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,QAIpC;IAED,YAAY,CAAC,KAAK,EAAE,GAAG,QAmGtB;IAED,OAAO,CAAC,WAAW;IAyFnB,OAAO,CAAC,YAAY;IA+CpB,OAAO,CAAC,gBAAgB;IAuBxB,OAAO,CAAC,qBAAqB;IAK7B,OAAO,CAAC,iBAAiB;IAazB,OAAO,CAAC,gBAAgB;IA8CxB,KAAK,CAAC,OAAO,EAAE,GAAG,OA+KjB;IAED,OAAO,SAkCN;IAED,OAAO,CAAC,WAAW;CAQpB;AAED,eAAO,MAAM,aAAa,UACjB,GAAG,UACF,GAAG,WACF,qBAAqB,CAAC,OAAO,iBAAiB,CAAC,CAAC,CAAC,CAAC,sBACZ,CAAA"}
@@ -124,6 +124,10 @@ export class MergedOutlineNode extends TempNode {
124
124
  _proxiesA = new Map();
125
125
  _proxiesB = new Map();
126
126
  _proxyMaskMaterials = new WeakMap();
127
+ _cutoutMasksA = new Map();
128
+ _cutoutMasksB = new Map();
129
+ _usedCutoutMasksA = new Set();
130
+ _usedCutoutMasksB = new Set();
127
131
  _maskSceneA = new Scene();
128
132
  _maskSceneB = new Scene();
129
133
  // Tracks whether either group rendered last frame. We use this to decide
@@ -267,7 +271,23 @@ export class MergedOutlineNode extends TempNode {
267
271
  this._buildCache(this.secondaryObjects, this._cacheB);
268
272
  const useProxiesA = this._syncProxies(this._cacheA, this._proxiesA, this._maskSceneA);
269
273
  const useProxiesB = this._syncProxies(this._cacheB, this._proxiesB, this._maskSceneB);
274
+ this._usedCutoutMasksA.clear();
275
+ this._usedCutoutMasksB.clear();
276
+ for (const [cache, used] of [
277
+ [this._cacheA, this._usedCutoutMasksA],
278
+ [this._cacheB, this._usedCutoutMasksB],
279
+ ]) {
280
+ for (const object of cache) {
281
+ if (!(object instanceof Mesh))
282
+ continue;
283
+ const materials = Array.isArray(object.material) ? object.material : [object.material];
284
+ for (const source of materials)
285
+ if (source?.alphaTest > 0)
286
+ used.add(source);
287
+ }
288
+ }
270
289
  if (!hasAny) {
290
+ this._pruneCutoutMasks();
271
291
  RendererUtils.restoreRendererAndSceneState(renderer, scene, _rendererState);
272
292
  return;
273
293
  }
@@ -300,6 +320,7 @@ export class MergedOutlineNode extends TempNode {
300
320
  this._runEdgePipeline(renderer, 'A');
301
321
  if (hasSecondary)
302
322
  this._runEdgePipeline(renderer, 'B');
323
+ this._pruneCutoutMasks();
303
324
  RendererUtils.restoreRendererAndSceneState(renderer, scene, _rendererState);
304
325
  }
305
326
  _renderMask(renderer, group, useProxies) {
@@ -309,6 +330,8 @@ export class MergedOutlineNode extends TempNode {
309
330
  const maskScene = isA ? this._maskSceneA : this._maskSceneB;
310
331
  const material = isA ? this._prepareMaskMatA : this._prepareMaskMatB;
311
332
  const spriteMaterial = isA ? this._prepareMaskSpriteMatA : this._prepareMaskSpriteMatB;
333
+ const cutoutMasks = isA ? this._cutoutMasksA : this._cutoutMasksB;
334
+ const usedCutoutMasks = isA ? this._usedCutoutMasksA : this._usedCutoutMasksB;
312
335
  if (useProxies) {
313
336
  for (const [source, proxy] of proxies)
314
337
  proxy.matrixWorld.copy(source.matrixWorld);
@@ -316,11 +339,39 @@ export class MergedOutlineNode extends TempNode {
316
339
  renderer.setRenderTarget((isA ? this._groupA : this._groupB).maskBuffer);
317
340
  // Keep source materials on proxies for material visibility and geometry
318
341
  // groups; substitute only at submission, just like the full-scene path.
319
- renderer.setRenderObjectFunction((obj, sc, cam, geo, _mat, grp, lights, clip) => {
342
+ renderer.setRenderObjectFunction((obj, sc, cam, geo, sourceMaterial, grp, lights, clip) => {
320
343
  if (!hasDrawableGeometry(geo))
321
344
  return;
322
345
  if (useProxies || cache.has(obj)) {
323
346
  let maskMaterial = obj.isSprite ? spriteMaterial : material;
347
+ if (!obj.isSprite && sourceMaterial?.alphaTest > 0) {
348
+ let cutoutMask = cutoutMasks.get(sourceMaterial);
349
+ const opacityNode = sourceMaterial.opacityNode ??
350
+ (sourceMaterial.map
351
+ ? cutoutMask?.userData.sourceMap === sourceMaterial.map
352
+ ? cutoutMask.opacityNode
353
+ : texture(sourceMaterial.map).a
354
+ : null);
355
+ if (opacityNode) {
356
+ if (!cutoutMask) {
357
+ cutoutMask = material.clone();
358
+ cutoutMasks.set(sourceMaterial, cutoutMask);
359
+ }
360
+ const positionNode = sourceMaterial.positionNode ?? null;
361
+ const changed = cutoutMask.opacityNode !== opacityNode ||
362
+ cutoutMask.positionNode !== positionNode ||
363
+ cutoutMask.side !== sourceMaterial.side;
364
+ cutoutMask.opacityNode = opacityNode;
365
+ cutoutMask.positionNode = positionNode;
366
+ cutoutMask.alphaTest = sourceMaterial.alphaTest;
367
+ cutoutMask.side = sourceMaterial.side;
368
+ cutoutMask.userData.sourceMap = sourceMaterial.map;
369
+ if (changed)
370
+ cutoutMask.needsUpdate = true;
371
+ maskMaterial = cutoutMask;
372
+ usedCutoutMasks.add(sourceMaterial);
373
+ }
374
+ }
324
375
  if (useProxies) {
325
376
  let ownedMaterial = this._proxyMaskMaterials.get(obj);
326
377
  if (!ownedMaterial) {
@@ -329,8 +380,16 @@ export class MergedOutlineNode extends TempNode {
329
380
  ownedMaterial = maskMaterial.clone();
330
381
  this._proxyMaskMaterials.set(obj, ownedMaterial);
331
382
  }
332
- if (ownedMaterial.colorNode !== maskMaterial.colorNode) {
383
+ if (ownedMaterial.colorNode !== maskMaterial.colorNode ||
384
+ ownedMaterial.opacityNode !== maskMaterial.opacityNode ||
385
+ ownedMaterial.positionNode !== maskMaterial.positionNode ||
386
+ ownedMaterial.alphaTest !== maskMaterial.alphaTest ||
387
+ ownedMaterial.side !== maskMaterial.side) {
333
388
  ownedMaterial.colorNode = maskMaterial.colorNode;
389
+ ownedMaterial.opacityNode = maskMaterial.opacityNode;
390
+ ownedMaterial.positionNode = maskMaterial.positionNode;
391
+ ownedMaterial.alphaTest = maskMaterial.alphaTest;
392
+ ownedMaterial.side = maskMaterial.side;
334
393
  ownedMaterial.needsUpdate = true;
335
394
  }
336
395
  maskMaterial = ownedMaterial;
@@ -413,6 +472,19 @@ export class MergedOutlineNode extends TempNode {
413
472
  this._proxyMaskMaterials.get(proxy)?.dispose();
414
473
  this._proxyMaskMaterials.delete(proxy);
415
474
  }
475
+ _pruneCutoutMasks() {
476
+ for (const [materials, used] of [
477
+ [this._cutoutMasksA, this._usedCutoutMasksA],
478
+ [this._cutoutMasksB, this._usedCutoutMasksB],
479
+ ]) {
480
+ for (const [source, mask] of materials) {
481
+ if (used.has(source))
482
+ continue;
483
+ mask.dispose();
484
+ materials.delete(source);
485
+ }
486
+ }
487
+ }
416
488
  _runEdgePipeline(renderer, group) {
417
489
  const isA = group === 'A';
418
490
  const g = isA ? this._groupA : this._groupB;
@@ -586,6 +658,12 @@ export class MergedOutlineNode extends TempNode {
586
658
  this._proxiesB.clear();
587
659
  this._cacheA.clear();
588
660
  this._cacheB.clear();
661
+ for (const mask of this._cutoutMasksA.values())
662
+ mask.dispose();
663
+ for (const mask of this._cutoutMasksB.values())
664
+ mask.dispose();
665
+ this._cutoutMasksA.clear();
666
+ this._cutoutMasksB.clear();
589
667
  this._depthRT.dispose();
590
668
  this._groupA.dispose();
591
669
  this._groupB.dispose();
@@ -0,0 +1,6 @@
1
+ import { type AnyNodeId } from '@pascal-app/core';
2
+ type HeightSampler = (x: number, z: number) => number | null;
3
+ /** Samples one locked node's upward-facing mesh in level-local coordinates. */
4
+ export declare function createNodeTopSurfaceHeightSampler(hostId: AnyNodeId, levelId: AnyNodeId): HeightSampler | null;
5
+ export {};
6
+ //# sourceMappingURL=node-top-surface-height.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"node-top-surface-height.d.ts","sourceRoot":"","sources":["../../src/lib/node-top-surface-height.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,SAAS,EAAiB,MAAM,kBAAkB,CAAA;AAKhE,KAAK,aAAa,GAAG,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAAA;AAS5D,+EAA+E;AAC/E,wBAAgB,iCAAiC,CAC/C,MAAM,EAAE,SAAS,EACjB,OAAO,EAAE,SAAS,GACjB,aAAa,GAAG,IAAI,CAqEtB"}
@@ -0,0 +1,74 @@
1
+ import { sceneRegistry } from '@pascal-app/core';
2
+ import { Box3, Matrix3, Raycaster, Vector3 } from 'three';
3
+ import useViewer from '../store/use-viewer';
4
+ import { setSurfaceRaycastLayers } from './layers';
5
+ const samplerCache = new WeakMap();
6
+ /** Samples one locked node's upward-facing mesh in level-local coordinates. */
7
+ export function createNodeTopSurfaceHeightSampler(hostId, levelId) {
8
+ const host = sceneRegistry.nodes.get(hostId);
9
+ const level = sceneRegistry.nodes.get(levelId);
10
+ if (!(host && level))
11
+ return null;
12
+ host.updateWorldMatrix(true, false);
13
+ level.updateWorldMatrix(true, false);
14
+ const revision = useViewer.getState().geometryRevision;
15
+ const cached = samplerCache.get(host)?.get(level);
16
+ if (cached?.revision === revision &&
17
+ cached.hostMatrix.every((value, index) => value === host.matrixWorld.elements[index]) &&
18
+ cached.levelMatrix.every((value, index) => value === level.matrixWorld.elements[index])) {
19
+ return cached.sample;
20
+ }
21
+ host.updateWorldMatrix(true, true);
22
+ const bounds = new Box3().setFromObject(host);
23
+ if (bounds.isEmpty())
24
+ return null;
25
+ const raycaster = new Raycaster();
26
+ setSurfaceRaycastLayers(raycaster.layers);
27
+ raycaster.far = Math.max(4, bounds.max.y - bounds.min.y + 4);
28
+ const origin = new Vector3();
29
+ const direction = new Vector3(0, -1, 0);
30
+ const normal = new Vector3();
31
+ const normalMatrix = new Matrix3();
32
+ const registeredRoots = new Set(sceneRegistry.nodes.values());
33
+ const sample = (x, z) => {
34
+ origin.set(x, 0, z);
35
+ level.localToWorld(origin);
36
+ origin.y = bounds.max.y + 2;
37
+ raycaster.set(origin, direction);
38
+ for (const hit of raycaster.intersectObject(host, true)) {
39
+ if (!hit.face)
40
+ continue;
41
+ let owner = hit.object.parent;
42
+ let nestedNode = false;
43
+ while (owner && owner !== host) {
44
+ if (registeredRoots.has(owner)) {
45
+ nestedNode = true;
46
+ break;
47
+ }
48
+ owner = owner.parent;
49
+ }
50
+ if (nestedNode)
51
+ continue;
52
+ normalMatrix.getNormalMatrix(hit.object.matrixWorld);
53
+ normal.copy(hit.face.normal).applyMatrix3(normalMatrix).normalize();
54
+ if (normal.y < 0.1)
55
+ continue;
56
+ const point = hit.point.clone();
57
+ level.worldToLocal(point);
58
+ return point.y;
59
+ }
60
+ return null;
61
+ };
62
+ let byLevel = samplerCache.get(host);
63
+ if (!byLevel) {
64
+ byLevel = new WeakMap();
65
+ samplerCache.set(host, byLevel);
66
+ }
67
+ byLevel.set(level, {
68
+ revision,
69
+ hostMatrix: [...host.matrixWorld.elements],
70
+ levelMatrix: [...level.matrixWorld.elements],
71
+ sample,
72
+ });
73
+ return sample;
74
+ }
@@ -0,0 +1,39 @@
1
+ type TemporalLike = {
2
+ temporal: {
3
+ getState(): {
4
+ pastStates: readonly unknown[];
5
+ futureStates: readonly unknown[];
6
+ isTracking: boolean;
7
+ };
8
+ };
9
+ };
10
+ type SelectionLike = {
11
+ getState(): {
12
+ selection: {
13
+ buildingId: string | null;
14
+ levelId: string | null;
15
+ zoneId: string | null;
16
+ selectedIds: readonly string[];
17
+ };
18
+ };
19
+ };
20
+ export type PerfHistoryState = {
21
+ /** Undo entries. */
22
+ past: number;
23
+ /** Redo entries. */
24
+ future: number;
25
+ /** False while any owner has history paused. */
26
+ tracking: boolean;
27
+ /** Refcounted pause owners (`pauseSceneHistory` + leases); direct `temporal.pause()` is not counted. */
28
+ pauseDepth: number;
29
+ };
30
+ export type PerfSelectionState = {
31
+ buildingId: string | null;
32
+ levelId: string | null;
33
+ zoneId: string | null;
34
+ selectedIds: string[];
35
+ };
36
+ export declare function readPerfHistory(store: TemporalLike): PerfHistoryState;
37
+ export declare function readPerfSelection(store: SelectionLike): PerfSelectionState;
38
+ export {};
39
+ //# sourceMappingURL=perf-probe-state.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"perf-probe-state.d.ts","sourceRoot":"","sources":["../../src/lib/perf-probe-state.ts"],"names":[],"mappings":"AAMA,KAAK,YAAY,GAAG;IAClB,QAAQ,EAAE;QACR,QAAQ,IAAI;YACV,UAAU,EAAE,SAAS,OAAO,EAAE,CAAA;YAC9B,YAAY,EAAE,SAAS,OAAO,EAAE,CAAA;YAChC,UAAU,EAAE,OAAO,CAAA;SACpB,CAAA;KACF,CAAA;CACF,CAAA;AAED,KAAK,aAAa,GAAG;IACnB,QAAQ,IAAI;QACV,SAAS,EAAE;YACT,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;YACzB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;YACtB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;YACrB,WAAW,EAAE,SAAS,MAAM,EAAE,CAAA;SAC/B,CAAA;KACF,CAAA;CACF,CAAA;AAED,MAAM,MAAM,gBAAgB,GAAG;IAC7B,oBAAoB;IACpB,IAAI,EAAE,MAAM,CAAA;IACZ,oBAAoB;IACpB,MAAM,EAAE,MAAM,CAAA;IACd,gDAAgD;IAChD,QAAQ,EAAE,OAAO,CAAA;IACjB,wGAAwG;IACxG,UAAU,EAAE,MAAM,CAAA;CACnB,CAAA;AAED,MAAM,MAAM,kBAAkB,GAAG;IAC/B,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;IACzB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;IACrB,WAAW,EAAE,MAAM,EAAE,CAAA;CACtB,CAAA;AAED,wBAAgB,eAAe,CAAC,KAAK,EAAE,YAAY,GAAG,gBAAgB,CAQrE;AAED,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,aAAa,GAAG,kBAAkB,CAG1E"}
@@ -0,0 +1,17 @@
1
+ // Read-only state for scripted `?perf` receipts (bench/next-house, scripts/perf): the scene
2
+ // history and the viewer selection, so production builds can report undo entries and
3
+ // history pauses without dev-only store handles.
4
+ import { getSceneHistoryPauseDepth } from '@pascal-app/core';
5
+ export function readPerfHistory(store) {
6
+ const temporal = store.temporal.getState();
7
+ return {
8
+ past: temporal.pastStates.length,
9
+ future: temporal.futureStates.length,
10
+ tracking: temporal.isTracking,
11
+ pauseDepth: getSceneHistoryPauseDepth(),
12
+ };
13
+ }
14
+ export function readPerfSelection(store) {
15
+ const { buildingId, levelId, zoneId, selectedIds } = store.getState().selection;
16
+ return { buildingId, levelId, zoneId, selectedIds: [...selectedIds] };
17
+ }
@@ -1,7 +1,31 @@
1
1
  import { events as createWebEvents, type EventManager, type RootStore } from '@react-three/fiber';
2
2
  import * as THREE from 'three';
3
+ /** A diagnostic the viewer hands its host, shaped for an error reporter's breadcrumb trail. */
4
+ export type ViewerBreadcrumb = {
5
+ category: string;
6
+ message: string;
7
+ level: 'info' | 'warning';
8
+ data?: Record<string, unknown>;
9
+ };
10
+ export type ViewerBreadcrumbHandler = (breadcrumb: ViewerBreadcrumb) => void;
3
11
  /** Opt in only when the raycast appends hits without reading prior hits or mutating scene/query state. */
4
12
  export declare function markPureRaycast<T extends THREE.Object3D['raycast']>(raycast: T): T;
5
- export declare function choosePointerEvents(search?: string): typeof createWebEvents;
6
- export declare function createPascalPointerEvents(store: RootStore): EventManager<HTMLElement>;
13
+ export declare function choosePointerEvents(search?: string, onBreadcrumb?: ViewerBreadcrumbHandler): typeof createWebEvents;
14
+ /**
15
+ * Whether a mesh has a material for everything a raycast reads: three indexes
16
+ * a material array by each geometry group, and one with no entry throws on
17
+ * `material.side`. A mesh caught mid-swap (a preview restored an older array
18
+ * over a rebuilt geometry) is skipped for this move instead of taking the whole
19
+ * pointer event down; its children are still tested. `material` asks the same of
20
+ * an array the mesh would wear.
21
+ */
22
+ export declare function hasMaterialsForGroups(object: THREE.Object3D, material?: THREE.Material | THREE.Material[] | undefined): boolean;
23
+ type SkipInvalidMaterial = (object: THREE.Object3D) => void;
24
+ /**
25
+ * Three's recursive `intersectObject`, skipping meshes `hasMaterialsForGroups`
26
+ * rejects. A raycaster with its own `intersectObject` is asked directly.
27
+ */
28
+ export declare function intersectDrawable(raycaster: THREE.Raycaster, object: THREE.Object3D, skip: SkipInvalidMaterial): THREE.Intersection[];
29
+ export declare function createPascalPointerEvents(store: RootStore, onBreadcrumb?: ViewerBreadcrumbHandler): EventManager<HTMLElement>;
30
+ export {};
7
31
  //# sourceMappingURL=pointer-events.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"pointer-events.d.ts","sourceRoot":"","sources":["../../src/lib/pointer-events.ts"],"names":[],"mappings":"AA6BA,OAAO,EACL,MAAM,IAAI,eAAe,EAGzB,KAAK,YAAY,EAMjB,KAAK,SAAS,EAEf,MAAM,oBAAoB,CAAA;AAC3B,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAqC9B,0GAA0G;AAC1G,wBAAgB,eAAe,CAAC,CAAC,SAAS,KAAK,CAAC,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,EAAE,CAAC,GAAG,CAAC,CAGlF;AAYD,wBAAgB,mBAAmB,CACjC,MAAM,SAA8D,GACnE,OAAO,eAAe,CAIxB;AAiLD,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,SAAS,GAAG,YAAY,CAAC,WAAW,CAAC,CAmBrF"}
1
+ {"version":3,"file":"pointer-events.d.ts","sourceRoot":"","sources":["../../src/lib/pointer-events.ts"],"names":[],"mappings":"AA8BA,OAAO,EACL,MAAM,IAAI,eAAe,EAGzB,KAAK,YAAY,EAMjB,KAAK,SAAS,EAEf,MAAM,oBAAoB,CAAA;AAC3B,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAI9B,+FAA+F;AAC/F,MAAM,MAAM,gBAAgB,GAAG;IAC7B,QAAQ,EAAE,MAAM,CAAA;IAChB,OAAO,EAAE,MAAM,CAAA;IACf,KAAK,EAAE,MAAM,GAAG,SAAS,CAAA;IACzB,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAC/B,CAAA;AAED,MAAM,MAAM,uBAAuB,GAAG,CAAC,UAAU,EAAE,gBAAgB,KAAK,IAAI,CAAA;AAmC5E,0GAA0G;AAC1G,wBAAgB,eAAe,CAAC,CAAC,SAAS,KAAK,CAAC,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,EAAE,CAAC,GAAG,CAAC,CAGlF;AAYD,wBAAgB,mBAAmB,CACjC,MAAM,SAA8D,EACpE,YAAY,CAAC,EAAE,uBAAuB,GACrC,OAAO,eAAe,CAIxB;AAiED;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,KAAK,CAAC,QAAQ,EACtB,QAAQ,GAAE,KAAK,CAAC,QAAQ,GAAG,KAAK,CAAC,QAAQ,EAAE,GAAG,SAA2C,GACxF,OAAO,CAST;AAMD,KAAK,mBAAmB,GAAG,CAAC,MAAM,EAAE,KAAK,CAAC,QAAQ,KAAK,IAAI,CAAA;AAmC3D;;;GAGG;AACH,wBAAgB,iBAAiB,CAC/B,SAAS,EAAE,KAAK,CAAC,SAAS,EAC1B,MAAM,EAAE,KAAK,CAAC,QAAQ,EACtB,IAAI,EAAE,mBAAmB,GACxB,KAAK,CAAC,YAAY,EAAE,CAiBtB;AA+GD,wBAAgB,yBAAyB,CACvC,KAAK,EAAE,SAAS,EAChB,YAAY,CAAC,EAAE,uBAAuB,GACrC,YAAY,CAAC,WAAW,CAAC,CAmB3B"}