@pascal-app/viewer 0.8.0 → 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 (284) 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 +6 -2
  5. package/dist/components/renderers/node-renderer.d.ts.map +1 -1
  6. package/dist/components/renderers/node-renderer.js +41 -22
  7. package/dist/components/renderers/parametric-node-renderer.d.ts +5 -0
  8. package/dist/components/renderers/parametric-node-renderer.d.ts.map +1 -0
  9. package/dist/components/renderers/parametric-node-renderer.js +37 -0
  10. package/dist/components/renderers/scene-renderer.d.ts +1 -1
  11. package/dist/components/renderers/scene-renderer.d.ts.map +1 -1
  12. package/dist/components/viewer/bvh-ecctrl.d.ts +77 -0
  13. package/dist/components/viewer/bvh-ecctrl.d.ts.map +1 -0
  14. package/dist/components/viewer/bvh-ecctrl.js +552 -0
  15. package/dist/components/viewer/frame-limiter.d.ts.map +1 -1
  16. package/dist/components/viewer/frame-limiter.js +39 -4
  17. package/dist/components/viewer/glb-interactive.d.ts +41 -0
  18. package/dist/components/viewer/glb-interactive.d.ts.map +1 -0
  19. package/dist/components/viewer/glb-interactive.js +394 -0
  20. package/dist/components/viewer/glb-reference-nodes.d.ts +29 -0
  21. package/dist/components/viewer/glb-reference-nodes.d.ts.map +1 -0
  22. package/dist/components/viewer/glb-reference-nodes.js +80 -0
  23. package/dist/components/viewer/glb-replace-instances.d.ts +18 -0
  24. package/dist/components/viewer/glb-replace-instances.d.ts.map +1 -0
  25. package/dist/components/viewer/glb-replace-instances.js +61 -0
  26. package/dist/components/viewer/glb-scene.d.ts +54 -0
  27. package/dist/components/viewer/glb-scene.d.ts.map +1 -0
  28. package/dist/components/viewer/glb-scene.js +920 -0
  29. package/dist/components/viewer/glb-walkthrough-controller.d.ts +12 -0
  30. package/dist/components/viewer/glb-walkthrough-controller.d.ts.map +1 -0
  31. package/dist/components/viewer/glb-walkthrough-controller.js +357 -0
  32. package/dist/components/viewer/index.d.ts +56 -3
  33. package/dist/components/viewer/index.d.ts.map +1 -1
  34. package/dist/components/viewer/index.js +280 -65
  35. package/dist/components/viewer/lights.d.ts +1 -1
  36. package/dist/components/viewer/lights.d.ts.map +1 -1
  37. package/dist/components/viewer/lights.js +201 -43
  38. package/dist/components/viewer/perf-monitor.d.ts +1 -1
  39. package/dist/components/viewer/perf-monitor.d.ts.map +1 -1
  40. package/dist/components/viewer/perf-monitor.js +115 -12
  41. package/dist/components/viewer/post-processing.d.ts +7 -1
  42. package/dist/components/viewer/post-processing.d.ts.map +1 -1
  43. package/dist/components/viewer/post-processing.js +395 -63
  44. package/dist/components/viewer/registered-systems.d.ts +12 -0
  45. package/dist/components/viewer/registered-systems.d.ts.map +1 -0
  46. package/dist/components/viewer/registered-systems.js +49 -0
  47. package/dist/components/viewer/scene-bvh.d.ts +18 -0
  48. package/dist/components/viewer/scene-bvh.d.ts.map +1 -0
  49. package/dist/components/viewer/scene-bvh.js +97 -0
  50. package/dist/components/viewer/scene-bvh.test.d.ts +2 -0
  51. package/dist/components/viewer/scene-bvh.test.d.ts.map +1 -0
  52. package/dist/components/viewer/scene-bvh.test.js +13 -0
  53. package/dist/components/viewer/scene-environment.d.ts +2 -0
  54. package/dist/components/viewer/scene-environment.d.ts.map +1 -0
  55. package/dist/components/viewer/scene-environment.js +79 -0
  56. package/dist/components/viewer/selection-manager.d.ts +1 -1
  57. package/dist/components/viewer/selection-manager.d.ts.map +1 -1
  58. package/dist/components/viewer/selection-manager.js +37 -14
  59. package/dist/components/viewer/viewer-camera.d.ts +1 -1
  60. package/dist/components/viewer/viewer-camera.d.ts.map +1 -1
  61. package/dist/components/viewer/walkthrough-controls.d.ts +2 -1
  62. package/dist/components/viewer/walkthrough-controls.d.ts.map +1 -1
  63. package/dist/components/viewer/walkthrough-controls.js +20 -0
  64. package/dist/hooks/use-gltf-ktx2.d.ts.map +1 -1
  65. package/dist/hooks/use-gltf-ktx2.js +3 -22
  66. package/dist/hooks/use-node-events.d.ts +6 -74
  67. package/dist/hooks/use-node-events.d.ts.map +1 -1
  68. package/dist/hooks/use-node-events.js +28 -8
  69. package/dist/index.d.ts +48 -5
  70. package/dist/index.d.ts.map +1 -1
  71. package/dist/index.js +82 -4
  72. package/dist/lib/backdrop.d.ts +33 -0
  73. package/dist/lib/backdrop.d.ts.map +1 -0
  74. package/dist/lib/backdrop.js +92 -0
  75. package/dist/lib/box-uv.d.ts +13 -0
  76. package/dist/lib/box-uv.d.ts.map +1 -0
  77. package/dist/lib/box-uv.js +32 -0
  78. package/dist/lib/csg-utils.d.ts +10 -0
  79. package/dist/lib/csg-utils.d.ts.map +1 -0
  80. package/dist/lib/csg-utils.js +93 -0
  81. package/dist/lib/csg-utils.test.d.ts +2 -0
  82. package/dist/lib/csg-utils.test.d.ts.map +1 -0
  83. package/dist/lib/csg-utils.test.js +45 -0
  84. package/dist/lib/drawable-geometry.d.ts +21 -0
  85. package/dist/lib/drawable-geometry.d.ts.map +1 -0
  86. package/dist/lib/drawable-geometry.js +22 -0
  87. package/dist/lib/edge-style.d.ts +4 -0
  88. package/dist/lib/edge-style.d.ts.map +1 -0
  89. package/dist/lib/edge-style.js +31 -0
  90. package/dist/lib/gpu-perf.d.ts +4 -0
  91. package/dist/lib/gpu-perf.d.ts.map +1 -0
  92. package/dist/lib/gpu-perf.js +23 -0
  93. package/dist/lib/ink-edges.d.ts +9 -0
  94. package/dist/lib/ink-edges.d.ts.map +1 -0
  95. package/dist/lib/ink-edges.js +49 -0
  96. package/dist/lib/isolation.d.ts +39 -0
  97. package/dist/lib/isolation.d.ts.map +1 -0
  98. package/dist/lib/isolation.js +103 -0
  99. package/dist/lib/ktx2-loader.d.ts +43 -0
  100. package/dist/lib/ktx2-loader.d.ts.map +1 -0
  101. package/dist/lib/ktx2-loader.js +136 -0
  102. package/dist/lib/layers.d.ts +28 -0
  103. package/dist/lib/layers.d.ts.map +1 -1
  104. package/dist/lib/layers.js +28 -0
  105. package/dist/lib/materials.d.ts +38 -15
  106. package/dist/lib/materials.d.ts.map +1 -1
  107. package/dist/lib/materials.js +364 -72
  108. package/dist/lib/merged-outline-node.d.ts.map +1 -1
  109. package/dist/lib/merged-outline-node.js +7 -0
  110. package/dist/lib/polygon-union.d.ts +4 -0
  111. package/dist/lib/polygon-union.d.ts.map +1 -0
  112. package/dist/lib/polygon-union.js +289 -0
  113. package/dist/lib/polygon-union.test.d.ts +2 -0
  114. package/dist/lib/polygon-union.test.d.ts.map +1 -0
  115. package/dist/lib/polygon-union.test.js +105 -0
  116. package/dist/lib/scene-themes.d.ts +30 -0
  117. package/dist/lib/scene-themes.d.ts.map +1 -0
  118. package/dist/lib/scene-themes.js +202 -0
  119. package/dist/lib/shadow-only.d.ts +4 -0
  120. package/dist/lib/shadow-only.d.ts.map +1 -0
  121. package/dist/lib/shadow-only.js +36 -0
  122. package/dist/lib/tsl-compat.d.ts +12 -0
  123. package/dist/lib/tsl-compat.d.ts.map +1 -0
  124. package/dist/lib/tsl-compat.js +13 -0
  125. package/dist/store/use-viewer.d.ts +92 -12
  126. package/dist/store/use-viewer.d.ts.map +1 -1
  127. package/dist/store/use-viewer.js +201 -6
  128. package/dist/store/use-viewer.test.d.ts +2 -0
  129. package/dist/store/use-viewer.test.d.ts.map +1 -0
  130. package/dist/store/use-viewer.test.js +42 -0
  131. package/dist/systems/ceiling/ceiling-system.d.ts +6 -2
  132. package/dist/systems/ceiling/ceiling-system.d.ts.map +1 -1
  133. package/dist/systems/ceiling/ceiling-system.js +66 -13
  134. package/dist/systems/door/door-animation.test.d.ts +2 -0
  135. package/dist/systems/door/door-animation.test.d.ts.map +1 -0
  136. package/dist/systems/door/door-animation.test.js +51 -0
  137. package/dist/systems/door/door-floor-alignment.test.d.ts +2 -0
  138. package/dist/systems/door/door-floor-alignment.test.d.ts.map +1 -0
  139. package/dist/systems/door/door-floor-alignment.test.js +50 -0
  140. package/dist/systems/door/door-system.d.ts +24 -0
  141. package/dist/systems/door/door-system.d.ts.map +1 -1
  142. package/dist/systems/door/door-system.js +864 -180
  143. package/dist/systems/elevator/elevator-interaction-system.d.ts +2 -0
  144. package/dist/systems/elevator/elevator-interaction-system.d.ts.map +1 -0
  145. package/dist/systems/elevator/elevator-interaction-system.js +62 -0
  146. package/dist/systems/fence/fence-system.d.ts +20 -0
  147. package/dist/systems/fence/fence-system.d.ts.map +1 -1
  148. package/dist/systems/fence/fence-system.js +229 -57
  149. package/dist/systems/floor-elevation/floor-elevation-system.d.ts +20 -0
  150. package/dist/systems/floor-elevation/floor-elevation-system.d.ts.map +1 -0
  151. package/dist/systems/floor-elevation/floor-elevation-system.js +78 -0
  152. package/dist/systems/geometry/geometry-system.d.ts +41 -0
  153. package/dist/systems/geometry/geometry-system.d.ts.map +1 -0
  154. package/dist/systems/geometry/geometry-system.js +348 -0
  155. package/dist/systems/geometry/geometry-system.test.d.ts +2 -0
  156. package/dist/systems/geometry/geometry-system.test.d.ts.map +1 -0
  157. package/dist/systems/geometry/geometry-system.test.js +30 -0
  158. package/dist/systems/interactive/control-widget.d.ts +10 -0
  159. package/dist/systems/interactive/control-widget.d.ts.map +1 -0
  160. package/dist/systems/interactive/control-widget.js +35 -0
  161. package/dist/systems/interactive/interactive-system.d.ts +1 -1
  162. package/dist/systems/interactive/interactive-system.d.ts.map +1 -1
  163. package/dist/systems/interactive/interactive-system.js +55 -56
  164. package/dist/systems/item/item-system.d.ts +7 -0
  165. package/dist/systems/item/item-system.d.ts.map +1 -1
  166. package/dist/systems/item/item-system.js +28 -17
  167. package/dist/systems/item-light/item-light-system.d.ts +1 -1
  168. package/dist/systems/item-light/item-light-system.d.ts.map +1 -1
  169. package/dist/systems/item-light/item-light-system.js +18 -5
  170. package/dist/systems/level/level-system.d.ts.map +1 -1
  171. package/dist/systems/level/level-system.js +28 -4
  172. package/dist/systems/level/level-utils.d.ts +0 -3
  173. package/dist/systems/level/level-utils.d.ts.map +1 -1
  174. package/dist/systems/level/level-utils.js +2 -41
  175. package/dist/systems/roof/roof-materials.d.ts +3 -2
  176. package/dist/systems/roof/roof-materials.d.ts.map +1 -1
  177. package/dist/systems/roof/roof-materials.js +59 -14
  178. package/dist/systems/roof/roof-system.d.ts +46 -6
  179. package/dist/systems/roof/roof-system.d.ts.map +1 -1
  180. package/dist/systems/roof/roof-system.js +1365 -170
  181. package/dist/systems/slab/slab-system.d.ts +5 -3
  182. package/dist/systems/slab/slab-system.d.ts.map +1 -1
  183. package/dist/systems/slab/slab-system.js +204 -68
  184. package/dist/systems/slab/slab-system.test.d.ts +2 -0
  185. package/dist/systems/slab/slab-system.test.d.ts.map +1 -0
  186. package/dist/systems/slab/slab-system.test.js +63 -0
  187. package/dist/systems/stair/stair-materials.d.ts +4 -3
  188. package/dist/systems/stair/stair-materials.d.ts.map +1 -1
  189. package/dist/systems/stair/stair-materials.js +30 -15
  190. package/dist/systems/stair/stair-system.d.ts.map +1 -1
  191. package/dist/systems/stair/stair-system.js +55 -63
  192. package/dist/systems/surface-hole-geometry.d.ts +3 -0
  193. package/dist/systems/surface-hole-geometry.d.ts.map +1 -0
  194. package/dist/systems/surface-hole-geometry.js +4 -0
  195. package/dist/systems/wall/opening-cutout-geometry.d.ts +31 -0
  196. package/dist/systems/wall/opening-cutout-geometry.d.ts.map +1 -0
  197. package/dist/systems/wall/opening-cutout-geometry.js +167 -0
  198. package/dist/systems/wall/opening-cutout-geometry.test.d.ts +2 -0
  199. package/dist/systems/wall/opening-cutout-geometry.test.d.ts.map +1 -0
  200. package/dist/systems/wall/opening-cutout-geometry.test.js +172 -0
  201. package/dist/systems/wall/wall-cutout.d.ts.map +1 -1
  202. package/dist/systems/wall/wall-cutout.js +62 -17
  203. package/dist/systems/wall/wall-materials.d.ts +12 -7
  204. package/dist/systems/wall/wall-materials.d.ts.map +1 -1
  205. package/dist/systems/wall/wall-materials.js +279 -46
  206. package/dist/systems/wall/wall-opening-cutout.test.d.ts +2 -0
  207. package/dist/systems/wall/wall-opening-cutout.test.d.ts.map +1 -0
  208. package/dist/systems/wall/wall-opening-cutout.test.js +59 -0
  209. package/dist/systems/wall/wall-support-extension.test.d.ts +2 -0
  210. package/dist/systems/wall/wall-support-extension.test.d.ts.map +1 -0
  211. package/dist/systems/wall/wall-support-extension.test.js +48 -0
  212. package/dist/systems/wall/wall-system.d.ts +2 -2
  213. package/dist/systems/wall/wall-system.d.ts.map +1 -1
  214. package/dist/systems/wall/wall-system.js +493 -193
  215. package/dist/systems/window/window-animation-system.d.ts +12 -0
  216. package/dist/systems/window/window-animation-system.d.ts.map +1 -1
  217. package/dist/systems/window/window-animation-system.js +16 -5
  218. package/dist/systems/window/window-system.d.ts +7 -0
  219. package/dist/systems/window/window-system.d.ts.map +1 -1
  220. package/dist/systems/window/window-system.js +308 -18
  221. package/package.json +5 -6
  222. package/dist/components/renderers/building/building-renderer.d.ts +0 -5
  223. package/dist/components/renderers/building/building-renderer.d.ts.map +0 -1
  224. package/dist/components/renderers/building/building-renderer.js +0 -11
  225. package/dist/components/renderers/ceiling/ceiling-renderer.d.ts +0 -5
  226. package/dist/components/renderers/ceiling/ceiling-renderer.d.ts.map +0 -1
  227. package/dist/components/renderers/ceiling/ceiling-renderer.js +0 -58
  228. package/dist/components/renderers/column/column-renderer.d.ts +0 -5
  229. package/dist/components/renderers/column/column-renderer.d.ts.map +0 -1
  230. package/dist/components/renderers/column/column-renderer.js +0 -597
  231. package/dist/components/renderers/door/door-renderer.d.ts +0 -5
  232. package/dist/components/renderers/door/door-renderer.d.ts.map +0 -1
  233. package/dist/components/renderers/door/door-renderer.js +0 -16
  234. package/dist/components/renderers/fence/fence-renderer.d.ts +0 -5
  235. package/dist/components/renderers/fence/fence-renderer.d.ts.map +0 -1
  236. package/dist/components/renderers/fence/fence-renderer.js +0 -15
  237. package/dist/components/renderers/guide/guide-renderer.d.ts +0 -5
  238. package/dist/components/renderers/guide/guide-renderer.d.ts.map +0 -1
  239. package/dist/components/renderers/guide/guide-renderer.js +0 -38
  240. package/dist/components/renderers/item/item-renderer.d.ts +0 -5
  241. package/dist/components/renderers/item/item-renderer.d.ts.map +0 -1
  242. package/dist/components/renderers/item/item-renderer.js +0 -172
  243. package/dist/components/renderers/level/level-renderer.d.ts +0 -5
  244. package/dist/components/renderers/level/level-renderer.d.ts.map +0 -1
  245. package/dist/components/renderers/level/level-renderer.js +0 -11
  246. package/dist/components/renderers/roof/roof-materials.d.ts +0 -4
  247. package/dist/components/renderers/roof/roof-materials.d.ts.map +0 -1
  248. package/dist/components/renderers/roof/roof-materials.js +0 -16
  249. package/dist/components/renderers/roof/roof-renderer.d.ts +0 -5
  250. package/dist/components/renderers/roof/roof-renderer.d.ts.map +0 -1
  251. package/dist/components/renderers/roof/roof-renderer.js +0 -32
  252. package/dist/components/renderers/roof-segment/roof-segment-renderer.d.ts +0 -5
  253. package/dist/components/renderers/roof-segment/roof-segment-renderer.d.ts.map +0 -1
  254. package/dist/components/renderers/roof-segment/roof-segment-renderer.js +0 -40
  255. package/dist/components/renderers/scan/scan-renderer.d.ts +0 -5
  256. package/dist/components/renderers/scan/scan-renderer.d.ts.map +0 -1
  257. package/dist/components/renderers/scan/scan-renderer.js +0 -54
  258. package/dist/components/renderers/site/site-renderer.d.ts +0 -5
  259. package/dist/components/renderers/site/site-renderer.d.ts.map +0 -1
  260. package/dist/components/renderers/site/site-renderer.js +0 -106
  261. package/dist/components/renderers/slab/slab-renderer.d.ts +0 -5
  262. package/dist/components/renderers/slab/slab-renderer.d.ts.map +0 -1
  263. package/dist/components/renderers/slab/slab-renderer.js +0 -58
  264. package/dist/components/renderers/spawn/spawn-renderer.d.ts +0 -5
  265. package/dist/components/renderers/spawn/spawn-renderer.d.ts.map +0 -1
  266. package/dist/components/renderers/spawn/spawn-renderer.js +0 -32
  267. package/dist/components/renderers/stair/stair-renderer.d.ts +0 -5
  268. package/dist/components/renderers/stair/stair-renderer.d.ts.map +0 -1
  269. package/dist/components/renderers/stair/stair-renderer.js +0 -603
  270. package/dist/components/renderers/stair-segment/stair-segment-renderer.d.ts +0 -5
  271. package/dist/components/renderers/stair-segment/stair-segment-renderer.d.ts.map +0 -1
  272. package/dist/components/renderers/stair-segment/stair-segment-renderer.js +0 -51
  273. package/dist/components/renderers/wall/wall-renderer.d.ts +0 -5
  274. package/dist/components/renderers/wall/wall-renderer.d.ts.map +0 -1
  275. package/dist/components/renderers/wall/wall-renderer.js +0 -16
  276. package/dist/components/renderers/window/window-renderer.d.ts +0 -5
  277. package/dist/components/renderers/window/window-renderer.d.ts.map +0 -1
  278. package/dist/components/renderers/window/window-renderer.js +0 -21
  279. package/dist/components/renderers/zone/zone-renderer.d.ts +0 -5
  280. package/dist/components/renderers/zone/zone-renderer.d.ts.map +0 -1
  281. package/dist/components/renderers/zone/zone-renderer.js +0 -181
  282. package/dist/components/viewer/ground-occluder.d.ts +0 -2
  283. package/dist/components/viewer/ground-occluder.d.ts.map +0 -1
  284. package/dist/components/viewer/ground-occluder.js +0 -79
@@ -1,9 +1,22 @@
1
- import { sceneRegistry, useInteractive, useScene, } from '@pascal-app/core';
1
+ import { DEFAULT_WALL_THICKNESS, getEffectiveNode, getWallThickness, sceneRegistry, useInteractive, useLiveNodeOverrides, useScene, } from '@pascal-app/core';
2
2
  import { useFrame } from '@react-three/fiber';
3
+ import { useEffect, useRef } from 'react';
3
4
  import * as THREE from 'three';
4
- import { baseMaterial, glassMaterial } from '../../lib/materials';
5
+ import { applyWorldScaleBoxUVs } from '../../lib/box-uv';
6
+ import { createSurfaceRoleMaterial, glassMaterial as defaultGlassMaterial, baseMaterial as getBaseMaterial, resolveMaterialRef, } from '../../lib/materials';
7
+ import useViewer from '../../store/use-viewer';
8
+ import { getOpeningCutoutProxyDepth } from '../wall/opening-cutout-geometry';
5
9
  // Invisible material for root mesh — used as selection hitbox only
6
10
  const hitboxMaterial = new THREE.MeshBasicMaterial({ visible: false });
11
+ let baseMaterial = getBaseMaterial();
12
+ let glassMaterial = defaultGlassMaterial;
13
+ let currentWindowSlot;
14
+ // Per-frame viewer state, captured so the per-node mesh builder (which runs
15
+ // outside React) can resolve each window's slot materials.
16
+ let currentShading = 'rendered';
17
+ let currentTextures = true;
18
+ let currentColorPreset = 'clay';
19
+ let currentSceneMaterials;
7
20
  export const CASEMENT_WINDOW_SASH_NAME = 'casement-window-sash';
8
21
  export const FRENCH_CASEMENT_LEFT_SASH_NAME = 'french-casement-left-sash';
9
22
  export const FRENCH_CASEMENT_RIGHT_SASH_NAME = 'french-casement-right-sash';
@@ -14,33 +27,173 @@ export const DOUBLE_HUNG_BOTTOM_SASH_NAME = 'double-hung-bottom-sash';
14
27
  export const LOUVERED_WINDOW_SLATS_NAME = 'louvered-window-slats';
15
28
  export const AWNING_WINDOW_SASH_NAME = 'awning-window-sash';
16
29
  export const HOPPER_WINDOW_SASH_NAME = 'hopper-window-sash';
30
+ const MAX_WINDOW_REBUILDS_PER_FRAME = 16;
31
+ const WINDOW_PROGRESSIVE_DIRTY_THRESHOLD = MAX_WINDOW_REBUILDS_PER_FRAME;
32
+ const WINDOW_PROGRESSIVE_TIME_BUDGET_MS = 8;
17
33
  export const WindowSystem = () => {
18
34
  const dirtyNodes = useScene((state) => state.dirtyNodes);
19
35
  const clearDirty = useScene((state) => state.clearDirty);
36
+ const shading = useViewer((state) => state.shading);
37
+ const textures = useViewer((state) => state.textures);
38
+ const colorPreset = useViewer((state) => state.colorPreset);
39
+ const sceneMaterials = useScene((state) => state.materials);
40
+ const materialRevisionRef = useRef(null);
41
+ // Subscribe so override-only updates re-run this component. Mirrors
42
+ // WallSystem + DoorSystem.
43
+ useLiveNodeOverrides((s) => s.overrides);
44
+ baseMaterial = textures
45
+ ? getBaseMaterial(shading)
46
+ : createSurfaceRoleMaterial('joinery', colorPreset);
47
+ glassMaterial = textures
48
+ ? defaultGlassMaterial
49
+ : createSurfaceRoleMaterial('glazing', colorPreset);
50
+ useEffect(() => {
51
+ const materialRevision = `${shading}:${textures ? 'textures' : 'solid'}:${colorPreset}`;
52
+ if (materialRevisionRef.current === materialRevision)
53
+ return;
54
+ materialRevisionRef.current = materialRevision;
55
+ const nodes = useScene.getState().nodes;
56
+ for (const node of Object.values(nodes)) {
57
+ if (node?.type === 'window') {
58
+ useScene.getState().dirtyNodes.add(node.id);
59
+ }
60
+ }
61
+ });
62
+ // Editing a scene material a window slot references must rebuild that window
63
+ // (window meshes are built by this system, not <GeometrySystem>, so its
64
+ // scene-material re-dirty doesn't cover them).
65
+ useEffect(() => {
66
+ void sceneMaterials;
67
+ const nodes = useScene.getState().nodes;
68
+ for (const node of Object.values(nodes)) {
69
+ if (node?.type !== 'window')
70
+ continue;
71
+ if (!nodeReferencesSceneMaterial(node))
72
+ continue;
73
+ useScene.getState().dirtyNodes.add(node.id);
74
+ }
75
+ }, [sceneMaterials]);
20
76
  useFrame(() => {
21
77
  if (dirtyNodes.size === 0)
22
78
  return;
79
+ baseMaterial = textures
80
+ ? getBaseMaterial(shading)
81
+ : createSurfaceRoleMaterial('joinery', colorPreset);
82
+ glassMaterial = textures
83
+ ? defaultGlassMaterial
84
+ : createSurfaceRoleMaterial('glazing', colorPreset);
85
+ currentShading = shading;
86
+ currentTextures = textures;
87
+ currentColorPreset = colorPreset;
88
+ currentSceneMaterials = sceneMaterials;
23
89
  const nodes = useScene.getState().nodes;
90
+ const dirtyWindowIds = [];
24
91
  dirtyNodes.forEach((id) => {
25
92
  const node = nodes[id];
26
- if (!node || node.type !== 'window')
93
+ if (node?.type !== 'window')
27
94
  return;
95
+ dirtyWindowIds.push(id);
96
+ });
97
+ const useProgressiveWindowRebuilds = dirtyWindowIds.length > WINDOW_PROGRESSIVE_DIRTY_THRESHOLD;
98
+ const frameStartedAt = performance.now();
99
+ let rebuiltWindowsThisFrame = 0;
100
+ for (const id of dirtyWindowIds) {
101
+ if (useProgressiveWindowRebuilds) {
102
+ if (rebuiltWindowsThisFrame >= MAX_WINDOW_REBUILDS_PER_FRAME) {
103
+ break;
104
+ }
105
+ if (rebuiltWindowsThisFrame > 0 &&
106
+ performance.now() - frameStartedAt >= WINDOW_PROGRESSIVE_TIME_BUDGET_MS) {
107
+ break;
108
+ }
109
+ }
110
+ const node = nodes[id];
111
+ if (node?.type !== 'window')
112
+ continue;
28
113
  const mesh = sceneRegistry.nodes.get(id);
29
114
  if (!mesh)
30
- return; // Keep dirty until mesh mounts
31
- updateWindowMesh(node, mesh);
115
+ continue; // Keep dirty until mesh mounts
116
+ // Merge any live override (width / height / position) so the mesh
117
+ // rebuild reflects the in-flight drag without zustand churn.
118
+ const effectiveNode = getEffectiveNode(node);
119
+ updateWindowMesh(effectiveNode, mesh);
32
120
  clearDirty(id);
121
+ rebuiltWindowsThisFrame += 1;
33
122
  // Rebuild the parent wall so its cutout reflects the updated window geometry
34
- if (node.parentId) {
35
- useScene.getState().dirtyNodes.add(node.parentId);
123
+ // Avoid triggering expensive wall CSG rebuilds while the window is being interactively moved/duplicated.
124
+ // The editor tools will request a final wall rebuild on commit.
125
+ const isTransient = !!node.metadata?.isTransient;
126
+ if (!isTransient && effectiveNode.parentId) {
127
+ useScene.getState().dirtyNodes.add(effectiveNode.parentId);
36
128
  }
37
- });
129
+ }
38
130
  }, 3);
39
131
  return null;
40
132
  };
133
+ function tagWindowSlot(mesh) {
134
+ mesh.userData.slotId = currentWindowSlot;
135
+ return mesh;
136
+ }
137
+ const NO_RAYCAST = () => { };
138
+ // An open casement sash swings perpendicular to the wall, so in a top-down view
139
+ // its flat panel blankets the room interior and wins the selection raycast over
140
+ // the slab/items beneath it. Drop the swung sash out of the raycast so a floor
141
+ // click falls through; the window stays selectable via its proud invisible
142
+ // cutout proxy at the opening (see syncWindowCutout). Skipped while closed so
143
+ // paint-by-slot still resolves on the sash.
144
+ function disableSubtreeRaycastIfSwung(object, rotationY) {
145
+ if (Math.abs(rotationY) <= 1e-3)
146
+ return;
147
+ object.traverse((child) => {
148
+ ;
149
+ child.raycast = NO_RAYCAST;
150
+ });
151
+ }
152
+ function nodeReferencesSceneMaterial(node) {
153
+ const slots = node.slots;
154
+ if (!slots)
155
+ return false;
156
+ for (const ref of Object.values(slots)) {
157
+ if (typeof ref === 'string' && ref.startsWith('scene:'))
158
+ return true;
159
+ }
160
+ return false;
161
+ }
162
+ // Window frame/glass default to catalog finishes (generic approach). `preset-glass`
163
+ // is now FrontSide (it was the only glass we use), so it's safe for the WebGPU
164
+ // MRT scene pass.
165
+ const FRAME_DEFAULT_REF = 'library:preset-softwhite';
166
+ const GLASS_DEFAULT_REF = 'library:preset-glass';
167
+ function windowSlotDefault(slotId) {
168
+ if (slotId === 'glass') {
169
+ if (!currentTextures)
170
+ return createSurfaceRoleMaterial('glazing', currentColorPreset);
171
+ return (resolveMaterialRef(GLASS_DEFAULT_REF, currentSceneMaterials, currentShading) ??
172
+ defaultGlassMaterial);
173
+ }
174
+ if (!currentTextures)
175
+ return createSurfaceRoleMaterial('joinery', currentColorPreset);
176
+ return (resolveMaterialRef(FRAME_DEFAULT_REF, currentSceneMaterials, currentShading) ??
177
+ getBaseMaterial(currentShading));
178
+ }
179
+ // Resolve a window's slot to a material: the `node.slots` override (colored mode
180
+ // only) → the role/base default. Textures-off ignores overrides — the monochrome
181
+ // escape hatch.
182
+ function resolveWindowSlotMaterial(node, slotId) {
183
+ const fallback = windowSlotDefault(slotId);
184
+ if (!currentTextures)
185
+ return fallback;
186
+ const ref = node.slots?.[slotId];
187
+ if (!ref)
188
+ return fallback;
189
+ return resolveMaterialRef(ref, currentSceneMaterials, currentShading) ?? fallback;
190
+ }
41
191
  function addBox(parent, material, w, h, d, x, y, z) {
42
- const m = new THREE.Mesh(new THREE.BoxGeometry(w, h, d), material);
192
+ const geometry = new THREE.BoxGeometry(w, h, d);
193
+ applyWorldScaleBoxUVs(geometry, w, h, d);
194
+ const m = new THREE.Mesh(geometry, material);
43
195
  m.position.set(x, y, z);
196
+ tagWindowSlot(m);
44
197
  parent.add(m);
45
198
  }
46
199
  function addShape(parent, material, shape, depth, z = 0) {
@@ -51,6 +204,7 @@ function addShape(parent, material, shape, depth, z = 0) {
51
204
  });
52
205
  geometry.translate(0, 0, -depth / 2 + z);
53
206
  const mesh = new THREE.Mesh(geometry, material);
207
+ tagWindowSlot(mesh);
54
208
  parent.add(mesh);
55
209
  }
56
210
  function disposeObjectGeometry(object) {
@@ -267,9 +421,11 @@ function addRoundedWindowVisuals(node, mesh) {
267
421
  const innerW = innerRight - innerLeft;
268
422
  const innerH = innerTop - innerBottom;
269
423
  const innerRadii = insetCornerRadii(outerRadii, inset, innerW, innerH);
424
+ currentWindowSlot = 'frame';
270
425
  addShape(mesh, baseMaterial, createRoundedFrameShape(width, height, inset, outerRadii), frameDepth);
271
426
  if (innerW > 0.01 && innerH > 0.01) {
272
427
  const glassDepth = Math.max(0.004, frameDepth * 0.08);
428
+ currentWindowSlot = 'glass';
273
429
  addShape(mesh, glassMaterial, createRoundedShape(innerLeft, innerRight, innerBottom, innerTop, innerRadii), glassDepth);
274
430
  const numCols = columnRatios.length;
275
431
  const numRows = rowRatios.length;
@@ -280,6 +436,7 @@ function addRoundedWindowVisuals(node, mesh) {
280
436
  const colWidths = columnRatios.map((r) => (r / colSum) * usableW);
281
437
  const rowHeights = rowRatios.map((r) => (r / rowSum) * usableH);
282
438
  let x = innerLeft;
439
+ currentWindowSlot = 'frame';
283
440
  for (let c = 0; c < numCols - 1; c++) {
284
441
  x += colWidths[c];
285
442
  const x1 = x;
@@ -291,6 +448,7 @@ function addRoundedWindowVisuals(node, mesh) {
291
448
  x += columnDividerThickness;
292
449
  }
293
450
  let y = innerTop;
451
+ currentWindowSlot = 'frame';
294
452
  for (let r = 0; r < numRows - 1; r++) {
295
453
  y -= rowHeights[r];
296
454
  const yTop = y;
@@ -305,6 +463,7 @@ function addRoundedWindowVisuals(node, mesh) {
305
463
  if (sill) {
306
464
  const sillW = width + sillDepth * 0.4;
307
465
  const sillZ = frameDepth / 2 + sillDepth / 2;
466
+ currentWindowSlot = 'frame';
308
467
  addBox(mesh, baseMaterial, sillW, sillThickness, sillDepth, 0, -height / 2 - sillThickness / 2, sillZ);
309
468
  }
310
469
  }
@@ -323,9 +482,11 @@ function addArchedWindowVisuals(node, mesh) {
323
482
  const innerH = innerTop - innerBottom;
324
483
  const innerArchHeight = getClampedArchHeight(innerW, innerH, archHeight - inset);
325
484
  const innerSpringY = innerTop - innerArchHeight;
485
+ currentWindowSlot = 'frame';
326
486
  addShape(mesh, baseMaterial, createArchedFrameShape(width, height, archHeight, inset), frameDepth);
327
487
  if (innerW > 0.01 && innerH > 0.01) {
328
488
  const glassDepth = Math.max(0.004, frameDepth * 0.08);
489
+ currentWindowSlot = 'glass';
329
490
  addShape(mesh, glassMaterial, createArchShape(innerLeft, innerRight, innerBottom, innerTop, innerArchHeight), glassDepth);
330
491
  const numCols = columnRatios.length;
331
492
  const numRows = rowRatios.length;
@@ -337,6 +498,7 @@ function addArchedWindowVisuals(node, mesh) {
337
498
  const rowHeights = rowRatios.map((r) => (r / rowSum) * usableH);
338
499
  const innerHalfWidth = innerW / 2;
339
500
  let x = innerLeft;
501
+ currentWindowSlot = 'frame';
340
502
  for (let c = 0; c < numCols - 1; c++) {
341
503
  x += colWidths[c];
342
504
  const x1 = x;
@@ -348,6 +510,7 @@ function addArchedWindowVisuals(node, mesh) {
348
510
  x += columnDividerThickness;
349
511
  }
350
512
  let y = innerTop;
513
+ currentWindowSlot = 'frame';
351
514
  for (let r = 0; r < numRows - 1; r++) {
352
515
  y -= rowHeights[r];
353
516
  const yTop = y;
@@ -364,6 +527,7 @@ function addArchedWindowVisuals(node, mesh) {
364
527
  if (sill) {
365
528
  const sillW = width + sillDepth * 0.4;
366
529
  const sillZ = frameDepth / 2 + sillDepth / 2;
530
+ currentWindowSlot = 'frame';
367
531
  addBox(mesh, baseMaterial, sillW, sillThickness, sillDepth, 0, -height / 2 - sillThickness / 2, sillZ);
368
532
  }
369
533
  }
@@ -386,6 +550,7 @@ function addSlidingWindowVisuals(node, mesh) {
386
550
  const innerW = width - 2 * frameThickness;
387
551
  const innerH = height - 2 * frameThickness;
388
552
  // Outer frame.
553
+ currentWindowSlot = 'frame';
389
554
  addBox(mesh, baseMaterial, width, frameThickness, frameDepth, 0, height / 2 - frameThickness / 2, 0);
390
555
  addBox(mesh, baseMaterial, width, frameThickness, frameDepth, 0, -height / 2 + frameThickness / 2, 0);
391
556
  addBox(mesh, baseMaterial, frameThickness, innerH, frameDepth, -width / 2 + frameThickness / 2, 0, 0);
@@ -409,11 +574,14 @@ function addSlidingWindowVisuals(node, mesh) {
409
574
  activePanel.position.set(leftPanelX, 0, leftZ);
410
575
  mesh.add(activePanel);
411
576
  // Twin tracks signal the sliding operation without adding editor-only state.
577
+ currentWindowSlot = 'frame';
412
578
  addBox(mesh, baseMaterial, innerW, trackThickness, frameDepth, 0, innerH / 2 - trackThickness / 2, 0);
413
579
  addBox(mesh, baseMaterial, innerW, trackThickness, frameDepth, 0, -innerH / 2 + trackThickness / 2, 0);
580
+ currentWindowSlot = 'glass';
414
581
  addBox(activePanel, glassMaterial, panelWidth, panelH, glassDepth, 0, 0, 0);
415
582
  addBox(mesh, glassMaterial, panelWidth, panelH, glassDepth, rightPanelX, 0, rightZ);
416
583
  // The right sash stays fixed. The left sash is the active panel that slides across it.
584
+ currentWindowSlot = 'frame';
417
585
  addBox(activePanel, baseMaterial, railThickness, panelH, frameDepth * 0.72, -panelWidth / 2 + railThickness / 2, 0, 0);
418
586
  addBox(mesh, baseMaterial, railThickness, panelH, frameDepth * 0.72, rightPanelX + panelWidth / 2 - railThickness / 2, 0, rightZ);
419
587
  addBox(activePanel, baseMaterial, railThickness, panelH, frameDepth * 0.78, panelWidth / 2 - railThickness / 2, 0, 0);
@@ -422,6 +590,7 @@ function addSlidingWindowVisuals(node, mesh) {
422
590
  if (sill) {
423
591
  const sillW = width + sillDepth * 0.4;
424
592
  const sillZ = frameDepth / 2 + sillDepth / 2;
593
+ currentWindowSlot = 'frame';
425
594
  addBox(mesh, baseMaterial, sillW, sillThickness, sillDepth, 0, -height / 2 - sillThickness / 2, sillZ);
426
595
  }
427
596
  }
@@ -436,15 +605,19 @@ function addRectCasementSash(parent, name, panelW, panelH, frameThickness, frame
436
605
  sash.position.set(pivotX, 0, frameDepth * 0.06);
437
606
  sash.rotation.y = rotationY;
438
607
  parent.add(sash);
608
+ currentWindowSlot = 'frame';
439
609
  addBox(sash, baseMaterial, panelW, sashFrameThickness, sashDepth, sashCenterX, panelH / 2 - sashFrameThickness / 2, 0);
440
610
  addBox(sash, baseMaterial, panelW, sashFrameThickness, sashDepth, sashCenterX, -panelH / 2 + sashFrameThickness / 2, 0);
441
611
  addBox(sash, baseMaterial, sashFrameThickness, panelH, sashDepth, sashCenterX - panelW / 2 + sashFrameThickness / 2, 0, 0);
442
612
  addBox(sash, baseMaterial, sashFrameThickness, panelH, sashDepth, sashCenterX + panelW / 2 - sashFrameThickness / 2, 0, 0);
613
+ currentWindowSlot = 'glass';
443
614
  addBox(sash, glassMaterial, glassW, glassH, glassDepth, sashCenterX, 0, sashDepth * 0.08);
615
+ disableSubtreeRaycastIfSwung(sash, rotationY);
444
616
  }
445
617
  function addFrenchCasementHingeMarkers(mesh, innerW, innerH, frameThickness, frameDepth) {
446
618
  const markerW = Math.max(frameThickness * 0.38, 0.018);
447
619
  const markerH = innerH * 0.24;
620
+ currentWindowSlot = 'frame';
448
621
  for (const pivotX of [-innerW / 2, innerW / 2]) {
449
622
  addBox(mesh, baseMaterial, markerW, markerH, frameDepth * 1.1, pivotX, innerH * 0.25, frameDepth * 0.08);
450
623
  addBox(mesh, baseMaterial, markerW, markerH, frameDepth * 1.1, pivotX, -innerH * 0.25, frameDepth * 0.08);
@@ -535,25 +708,32 @@ function addShapedFrenchCasementSash(parent, node, name, side, fullW, leafW, lea
535
708
  const outerArchHeight = getClampedArchHeight(node.width, node.height, node.archHeight);
536
709
  const sashArchHeight = getClampedArchHeight(fullW, leafH, outerArchHeight - frameThickness);
537
710
  const sashSpringY = node.height / 2 - outerArchHeight;
711
+ currentWindowSlot = 'frame';
538
712
  addShape(sashVisual, baseMaterial, createFrenchArchLeafFrameShape(leafW, leafH, fullW, sashSpringY, sashArchHeight, sashFrameThickness, side), sashDepth);
539
713
  const glassInset = Math.min(sashFrameThickness, leafW / 2 - 0.005, leafH / 2 - 0.005);
540
714
  if (glassInset > 0.001) {
715
+ currentWindowSlot = 'glass';
541
716
  addShape(sashVisual, glassMaterial, createFrenchArchLeafShape(leafW, leafH, fullW, sashSpringY, sashArchHeight, side, glassInset), glassDepth, sashDepth * 0.08);
542
717
  }
718
+ disableSubtreeRaycastIfSwung(sash, rotationY);
543
719
  return;
544
720
  }
545
721
  const frameRadii = insetCornerRadii(getWindowRoundedRadii(node, node.width, node.height), frameThickness, fullW, leafH);
722
+ currentWindowSlot = 'frame';
546
723
  addShape(sashVisual, baseMaterial, createFrenchRoundedLeafFrameShape(leafW, leafH, fullW, frameRadii, sashFrameThickness, side), sashDepth);
547
724
  const glassInset = Math.min(sashFrameThickness, leafW / 2 - 0.005, leafH / 2 - 0.005);
548
725
  if (glassInset > 0.001) {
726
+ currentWindowSlot = 'glass';
549
727
  addShape(sashVisual, glassMaterial, createFrenchRoundedLeafShape(leafW, leafH, fullW, frameRadii, side, glassInset), glassDepth, sashDepth * 0.08);
550
728
  }
729
+ disableSubtreeRaycastIfSwung(sash, rotationY);
551
730
  }
552
731
  function addFrenchCasementWindowVisuals(node, mesh) {
553
732
  const { width, height, frameDepth, frameThickness, sill, sillDepth, sillThickness } = node;
554
733
  const innerW = width - 2 * frameThickness;
555
734
  const innerH = height - 2 * frameThickness;
556
735
  // Fixed outer frame.
736
+ currentWindowSlot = 'frame';
557
737
  addBox(mesh, baseMaterial, width, frameThickness, frameDepth, 0, height / 2 - frameThickness / 2, 0);
558
738
  addBox(mesh, baseMaterial, width, frameThickness, frameDepth, 0, -height / 2 + frameThickness / 2, 0);
559
739
  addBox(mesh, baseMaterial, frameThickness, innerH, frameDepth, -width / 2 + frameThickness / 2, 0, 0);
@@ -568,6 +748,7 @@ function addFrenchCasementWindowVisuals(node, mesh) {
568
748
  if (sill) {
569
749
  const sillW = width + sillDepth * 0.4;
570
750
  const sillZ = frameDepth / 2 + sillDepth / 2;
751
+ currentWindowSlot = 'frame';
571
752
  addBox(mesh, baseMaterial, sillW, sillThickness, sillDepth, 0, -height / 2 - sillThickness / 2, sillZ);
572
753
  }
573
754
  }
@@ -576,9 +757,11 @@ function addShapedCasementWindowVisuals(node, mesh) {
576
757
  const innerW = width - 2 * frameThickness;
577
758
  const innerH = height - 2 * frameThickness;
578
759
  if (node.openingShape === 'arch') {
760
+ currentWindowSlot = 'frame';
579
761
  addShape(mesh, baseMaterial, createArchedFrameShape(width, height, getClampedArchHeight(width, height, node.archHeight), frameThickness), frameDepth);
580
762
  }
581
763
  else {
764
+ currentWindowSlot = 'frame';
582
765
  addShape(mesh, baseMaterial, createRoundedFrameShape(width, height, frameThickness, getWindowRoundedRadii(node, width, height)), frameDepth);
583
766
  }
584
767
  if ((node.casementStyle ?? 'single') === 'french') {
@@ -592,6 +775,7 @@ function addShapedCasementWindowVisuals(node, mesh) {
592
775
  if (sill) {
593
776
  const sillW = width + sillDepth * 0.4;
594
777
  const sillZ = frameDepth / 2 + sillDepth / 2;
778
+ currentWindowSlot = 'frame';
595
779
  addBox(mesh, baseMaterial, sillW, sillThickness, sillDepth, 0, -height / 2 - sillThickness / 2, sillZ);
596
780
  }
597
781
  return;
@@ -616,30 +800,37 @@ function addShapedCasementWindowVisuals(node, mesh) {
616
800
  mesh.add(sash);
617
801
  if (node.openingShape === 'arch') {
618
802
  const sashArchHeight = getClampedArchHeight(innerW, innerH, (node.archHeight ?? innerW / 2) - frameThickness);
803
+ currentWindowSlot = 'frame';
619
804
  addShape(sashVisual, baseMaterial, createArchedFrameShape(innerW, innerH, sashArchHeight, sashFrameThickness), sashDepth);
620
805
  const glassInset = Math.min(sashFrameThickness, innerW / 2 - 0.005, innerH / 2 - 0.005);
621
806
  if (glassInset > 0.001) {
622
807
  const glassW = innerW - 2 * glassInset;
623
808
  const glassH = innerH - 2 * glassInset;
809
+ currentWindowSlot = 'glass';
624
810
  addShape(sashVisual, glassMaterial, createArchShape(-glassW / 2, glassW / 2, -glassH / 2, glassH / 2, getClampedArchHeight(glassW, glassH, sashArchHeight - glassInset)), glassDepth, sashDepth * 0.08);
625
811
  }
626
812
  }
627
813
  else {
628
814
  const outerRadii = getWindowRoundedRadii(node, innerW, innerH);
815
+ currentWindowSlot = 'frame';
629
816
  addShape(sashVisual, baseMaterial, createRoundedFrameShape(innerW, innerH, sashFrameThickness, outerRadii), sashDepth);
630
817
  const glassInset = Math.min(sashFrameThickness, innerW / 2 - 0.005, innerH / 2 - 0.005);
631
818
  if (glassInset > 0.001) {
632
819
  const glassW = innerW - 2 * glassInset;
633
820
  const glassH = innerH - 2 * glassInset;
821
+ currentWindowSlot = 'glass';
634
822
  addShape(sashVisual, glassMaterial, createRoundedShape(-glassW / 2, glassW / 2, -glassH / 2, glassH / 2, insetCornerRadii(outerRadii, glassInset, glassW, glassH)), glassDepth, sashDepth * 0.08);
635
823
  }
636
824
  }
825
+ disableSubtreeRaycastIfSwung(sash, sash.rotation.y);
826
+ currentWindowSlot = 'frame';
637
827
  addBox(mesh, baseMaterial, Math.max(frameThickness * 0.38, 0.018), innerH * 0.28, frameDepth * 1.1, pivotX, innerH * 0.24, frameDepth * 0.08);
638
828
  addBox(mesh, baseMaterial, Math.max(frameThickness * 0.38, 0.018), innerH * 0.28, frameDepth * 1.1, pivotX, -innerH * 0.24, frameDepth * 0.08);
639
829
  }
640
830
  if (sill) {
641
831
  const sillW = width + sillDepth * 0.4;
642
832
  const sillZ = frameDepth / 2 + sillDepth / 2;
833
+ currentWindowSlot = 'frame';
643
834
  addBox(mesh, baseMaterial, sillW, sillThickness, sillDepth, 0, -height / 2 - sillThickness / 2, sillZ);
644
835
  }
645
836
  }
@@ -656,6 +847,7 @@ function addCasementWindowVisuals(node, mesh) {
656
847
  const innerW = width - 2 * frameThickness;
657
848
  const innerH = height - 2 * frameThickness;
658
849
  // Fixed outer frame.
850
+ currentWindowSlot = 'frame';
659
851
  addBox(mesh, baseMaterial, width, frameThickness, frameDepth, 0, height / 2 - frameThickness / 2, 0);
660
852
  addBox(mesh, baseMaterial, width, frameThickness, frameDepth, 0, -height / 2 + frameThickness / 2, 0);
661
853
  addBox(mesh, baseMaterial, frameThickness, innerH, frameDepth, -width / 2 + frameThickness / 2, 0, 0);
@@ -677,18 +869,23 @@ function addCasementWindowVisuals(node, mesh) {
677
869
  sash.position.set(pivotX, 0, frameDepth * 0.06);
678
870
  sash.rotation.y = hingeSign * openAngle;
679
871
  mesh.add(sash);
872
+ currentWindowSlot = 'frame';
680
873
  addBox(sash, baseMaterial, innerW, sashFrameThickness, sashDepth, sashCenterX, innerH / 2 - sashFrameThickness / 2, 0);
681
874
  addBox(sash, baseMaterial, innerW, sashFrameThickness, sashDepth, sashCenterX, -innerH / 2 + sashFrameThickness / 2, 0);
682
875
  addBox(sash, baseMaterial, sashFrameThickness, innerH, sashDepth, sashCenterX - innerW / 2 + sashFrameThickness / 2, 0, 0);
683
876
  addBox(sash, baseMaterial, sashFrameThickness, innerH, sashDepth, sashCenterX + innerW / 2 - sashFrameThickness / 2, 0, 0);
877
+ currentWindowSlot = 'glass';
684
878
  addBox(sash, glassMaterial, glassW, glassH, glassDepth, sashCenterX, 0, sashDepth * 0.08);
879
+ disableSubtreeRaycastIfSwung(sash, sash.rotation.y);
685
880
  // Small hinge markers make the pivot side legible when the sash is closed.
881
+ currentWindowSlot = 'frame';
686
882
  addBox(mesh, baseMaterial, Math.max(frameThickness * 0.38, 0.018), innerH * 0.28, frameDepth * 1.1, pivotX, innerH * 0.24, frameDepth * 0.08);
687
883
  addBox(mesh, baseMaterial, Math.max(frameThickness * 0.38, 0.018), innerH * 0.28, frameDepth * 1.1, pivotX, -innerH * 0.24, frameDepth * 0.08);
688
884
  }
689
885
  if (sill) {
690
886
  const sillW = width + sillDepth * 0.4;
691
887
  const sillZ = frameDepth / 2 + sillDepth / 2;
888
+ currentWindowSlot = 'frame';
692
889
  addBox(mesh, baseMaterial, sillW, sillThickness, sillDepth, 0, -height / 2 - sillThickness / 2, sillZ);
693
890
  }
694
891
  }
@@ -701,6 +898,7 @@ function addAwningWindowVisuals(node, mesh) {
701
898
  const innerW = width - 2 * frameThickness;
702
899
  const innerH = height - 2 * frameThickness;
703
900
  // Fixed outer frame.
901
+ currentWindowSlot = 'frame';
704
902
  addBox(mesh, baseMaterial, width, frameThickness, frameDepth, 0, height / 2 - frameThickness / 2, 0);
705
903
  addBox(mesh, baseMaterial, width, frameThickness, frameDepth, 0, -height / 2 + frameThickness / 2, 0);
706
904
  addBox(mesh, baseMaterial, frameThickness, innerH, frameDepth, -width / 2 + frameThickness / 2, 0, 0);
@@ -721,17 +919,21 @@ function addAwningWindowVisuals(node, mesh) {
721
919
  sash.position.set(0, pivotY, frameDepth * 0.06);
722
920
  sash.rotation.x = -openAngle;
723
921
  mesh.add(sash);
922
+ currentWindowSlot = 'frame';
724
923
  addBox(sash, baseMaterial, innerW, sashFrameThickness, sashDepth, 0, sashCenterY + innerH / 2 - sashFrameThickness / 2, 0);
725
924
  addBox(sash, baseMaterial, innerW, sashFrameThickness, sashDepth, 0, sashCenterY - innerH / 2 + sashFrameThickness / 2, 0);
726
925
  addBox(sash, baseMaterial, sashFrameThickness, innerH, sashDepth, -innerW / 2 + sashFrameThickness / 2, sashCenterY, 0);
727
926
  addBox(sash, baseMaterial, sashFrameThickness, innerH, sashDepth, innerW / 2 - sashFrameThickness / 2, sashCenterY, 0);
927
+ currentWindowSlot = 'glass';
728
928
  addBox(sash, glassMaterial, glassW, glassH, glassDepth, 0, sashCenterY, sashDepth * 0.08);
729
929
  // Compact hinge rail, visible even when the sash is closed.
930
+ currentWindowSlot = 'frame';
730
931
  addBox(mesh, baseMaterial, innerW * 0.42, Math.max(frameThickness * 0.38, 0.018), frameDepth * 1.1, 0, pivotY, frameDepth * 0.08);
731
932
  }
732
933
  if (sill) {
733
934
  const sillW = width + sillDepth * 0.4;
734
935
  const sillZ = frameDepth / 2 + sillDepth / 2;
936
+ currentWindowSlot = 'frame';
735
937
  addBox(mesh, baseMaterial, sillW, sillThickness, sillDepth, 0, -height / 2 - sillThickness / 2, sillZ);
736
938
  }
737
939
  }
@@ -740,9 +942,11 @@ function addShapedAwningWindowVisuals(node, mesh) {
740
942
  const innerW = width - 2 * frameThickness;
741
943
  const innerH = height - 2 * frameThickness;
742
944
  if (node.openingShape === 'arch') {
945
+ currentWindowSlot = 'frame';
743
946
  addShape(mesh, baseMaterial, createArchedFrameShape(width, height, getClampedArchHeight(width, height, node.archHeight), frameThickness), frameDepth);
744
947
  }
745
948
  else {
949
+ currentWindowSlot = 'frame';
746
950
  addShape(mesh, baseMaterial, createRoundedFrameShape(width, height, frameThickness, getWindowRoundedRadii(node, width, height)), frameDepth);
747
951
  }
748
952
  if (innerW > 0.01 && innerH > 0.01) {
@@ -763,29 +967,35 @@ function addShapedAwningWindowVisuals(node, mesh) {
763
967
  mesh.add(sash);
764
968
  if (node.openingShape === 'arch') {
765
969
  const sashArchHeight = getClampedArchHeight(innerW, innerH, (node.archHeight ?? innerW / 2) - frameThickness);
970
+ currentWindowSlot = 'frame';
766
971
  addShape(sashVisual, baseMaterial, createArchedFrameShape(innerW, innerH, sashArchHeight, sashFrameThickness), sashDepth);
767
972
  const glassInset = Math.min(sashFrameThickness, innerW / 2 - 0.005, innerH / 2 - 0.005);
768
973
  if (glassInset > 0.001) {
769
974
  const glassW = innerW - 2 * glassInset;
770
975
  const glassH = innerH - 2 * glassInset;
976
+ currentWindowSlot = 'glass';
771
977
  addShape(sashVisual, glassMaterial, createArchShape(-glassW / 2, glassW / 2, -glassH / 2, glassH / 2, getClampedArchHeight(glassW, glassH, sashArchHeight - glassInset)), glassDepth, sashDepth * 0.08);
772
978
  }
773
979
  }
774
980
  else {
775
981
  const outerRadii = getWindowRoundedRadii(node, innerW, innerH);
982
+ currentWindowSlot = 'frame';
776
983
  addShape(sashVisual, baseMaterial, createRoundedFrameShape(innerW, innerH, sashFrameThickness, outerRadii), sashDepth);
777
984
  const glassInset = Math.min(sashFrameThickness, innerW / 2 - 0.005, innerH / 2 - 0.005);
778
985
  if (glassInset > 0.001) {
779
986
  const glassW = innerW - 2 * glassInset;
780
987
  const glassH = innerH - 2 * glassInset;
988
+ currentWindowSlot = 'glass';
781
989
  addShape(sashVisual, glassMaterial, createRoundedShape(-glassW / 2, glassW / 2, -glassH / 2, glassH / 2, insetCornerRadii(outerRadii, glassInset, glassW, glassH)), glassDepth, sashDepth * 0.08);
782
990
  }
783
991
  }
992
+ currentWindowSlot = 'frame';
784
993
  addBox(mesh, baseMaterial, innerW * 0.42, Math.max(frameThickness * 0.38, 0.018), frameDepth * 1.1, 0, pivotY, frameDepth * 0.08);
785
994
  }
786
995
  if (sill) {
787
996
  const sillW = width + sillDepth * 0.4;
788
997
  const sillZ = frameDepth / 2 + sillDepth / 2;
998
+ currentWindowSlot = 'frame';
789
999
  addBox(mesh, baseMaterial, sillW, sillThickness, sillDepth, 0, -height / 2 - sillThickness / 2, sillZ);
790
1000
  }
791
1001
  }
@@ -798,6 +1008,7 @@ function addHopperWindowVisuals(node, mesh) {
798
1008
  const innerW = width - 2 * frameThickness;
799
1009
  const innerH = height - 2 * frameThickness;
800
1010
  // Fixed outer frame.
1011
+ currentWindowSlot = 'frame';
801
1012
  addBox(mesh, baseMaterial, width, frameThickness, frameDepth, 0, height / 2 - frameThickness / 2, 0);
802
1013
  addBox(mesh, baseMaterial, width, frameThickness, frameDepth, 0, -height / 2 + frameThickness / 2, 0);
803
1014
  addBox(mesh, baseMaterial, frameThickness, innerH, frameDepth, -width / 2 + frameThickness / 2, 0, 0);
@@ -816,17 +1027,21 @@ function addHopperWindowVisuals(node, mesh) {
816
1027
  sash.position.set(0, pivotY, frameDepth * 0.06);
817
1028
  sash.rotation.x = -openAngle;
818
1029
  mesh.add(sash);
1030
+ currentWindowSlot = 'frame';
819
1031
  addBox(sash, baseMaterial, innerW, sashFrameThickness, sashDepth, 0, innerH - sashFrameThickness / 2, 0);
820
1032
  addBox(sash, baseMaterial, innerW, sashFrameThickness, sashDepth, 0, sashFrameThickness / 2, 0);
821
1033
  addBox(sash, baseMaterial, sashFrameThickness, innerH, sashDepth, -innerW / 2 + sashFrameThickness / 2, innerH / 2, 0);
822
1034
  addBox(sash, baseMaterial, sashFrameThickness, innerH, sashDepth, innerW / 2 - sashFrameThickness / 2, innerH / 2, 0);
1035
+ currentWindowSlot = 'glass';
823
1036
  addBox(sash, glassMaterial, glassW, glassH, glassDepth, 0, innerH / 2, sashDepth * 0.08);
824
1037
  // Compact bottom hinge rail, visible even when the sash is closed.
1038
+ currentWindowSlot = 'frame';
825
1039
  addBox(mesh, baseMaterial, innerW * 0.42, Math.max(frameThickness * 0.38, 0.018), frameDepth * 1.1, 0, pivotY, frameDepth * 0.08);
826
1040
  }
827
1041
  if (sill) {
828
1042
  const sillW = width + sillDepth * 0.4;
829
1043
  const sillZ = frameDepth / 2 + sillDepth / 2;
1044
+ currentWindowSlot = 'frame';
830
1045
  addBox(mesh, baseMaterial, sillW, sillThickness, sillDepth, 0, -height / 2 - sillThickness / 2, sillZ);
831
1046
  }
832
1047
  }
@@ -835,9 +1050,11 @@ function addShapedHopperWindowVisuals(node, mesh) {
835
1050
  const innerW = width - 2 * frameThickness;
836
1051
  const innerH = height - 2 * frameThickness;
837
1052
  if (node.openingShape === 'arch') {
1053
+ currentWindowSlot = 'frame';
838
1054
  addShape(mesh, baseMaterial, createArchedFrameShape(width, height, getClampedArchHeight(width, height, node.archHeight), frameThickness), frameDepth);
839
1055
  }
840
1056
  else {
1057
+ currentWindowSlot = 'frame';
841
1058
  addShape(mesh, baseMaterial, createRoundedFrameShape(width, height, frameThickness, getWindowRoundedRadii(node, width, height)), frameDepth);
842
1059
  }
843
1060
  if (innerW > 0.01 && innerH > 0.01) {
@@ -857,37 +1074,45 @@ function addShapedHopperWindowVisuals(node, mesh) {
857
1074
  mesh.add(sash);
858
1075
  if (node.openingShape === 'arch') {
859
1076
  const sashArchHeight = getClampedArchHeight(innerW, innerH, (node.archHeight ?? innerW / 2) - frameThickness);
1077
+ currentWindowSlot = 'frame';
860
1078
  addShape(sashVisual, baseMaterial, createArchedFrameShape(innerW, innerH, sashArchHeight, sashFrameThickness), sashDepth);
861
1079
  const glassInset = Math.min(sashFrameThickness, innerW / 2 - 0.005, innerH / 2 - 0.005);
862
1080
  if (glassInset > 0.001) {
863
1081
  const glassW = innerW - 2 * glassInset;
864
1082
  const glassH = innerH - 2 * glassInset;
1083
+ currentWindowSlot = 'glass';
865
1084
  addShape(sashVisual, glassMaterial, createArchShape(-glassW / 2, glassW / 2, -glassH / 2, glassH / 2, getClampedArchHeight(glassW, glassH, sashArchHeight - glassInset)), glassDepth, sashDepth * 0.08);
866
1085
  }
867
1086
  }
868
1087
  else {
869
1088
  const outerRadii = getWindowRoundedRadii(node, innerW, innerH);
1089
+ currentWindowSlot = 'frame';
870
1090
  addShape(sashVisual, baseMaterial, createRoundedFrameShape(innerW, innerH, sashFrameThickness, outerRadii), sashDepth);
871
1091
  const glassInset = Math.min(sashFrameThickness, innerW / 2 - 0.005, innerH / 2 - 0.005);
872
1092
  if (glassInset > 0.001) {
873
1093
  const glassW = innerW - 2 * glassInset;
874
1094
  const glassH = innerH - 2 * glassInset;
1095
+ currentWindowSlot = 'glass';
875
1096
  addShape(sashVisual, glassMaterial, createRoundedShape(-glassW / 2, glassW / 2, -glassH / 2, glassH / 2, insetCornerRadii(outerRadii, glassInset, glassW, glassH)), glassDepth, sashDepth * 0.08);
876
1097
  }
877
1098
  }
1099
+ currentWindowSlot = 'frame';
878
1100
  addBox(mesh, baseMaterial, innerW * 0.42, Math.max(frameThickness * 0.38, 0.018), frameDepth * 1.1, 0, pivotY, frameDepth * 0.08);
879
1101
  }
880
1102
  if (sill) {
881
1103
  const sillW = width + sillDepth * 0.4;
882
1104
  const sillZ = frameDepth / 2 + sillDepth / 2;
1105
+ currentWindowSlot = 'frame';
883
1106
  addBox(mesh, baseMaterial, sillW, sillThickness, sillDepth, 0, -height / 2 - sillThickness / 2, sillZ);
884
1107
  }
885
1108
  }
886
1109
  function addHungSash(parent, panelW, panelHeight, sashFrameThickness, frameDepth, glassDepth, glassW, glassH) {
1110
+ currentWindowSlot = 'frame';
887
1111
  addBox(parent, baseMaterial, panelW, sashFrameThickness, frameDepth * 0.72, 0, panelHeight / 2 - sashFrameThickness / 2, 0);
888
1112
  addBox(parent, baseMaterial, panelW, sashFrameThickness, frameDepth * 0.72, 0, -panelHeight / 2 + sashFrameThickness / 2, 0);
889
1113
  addBox(parent, baseMaterial, sashFrameThickness, panelHeight, frameDepth * 0.72, -panelW / 2 + sashFrameThickness / 2, 0, 0);
890
1114
  addBox(parent, baseMaterial, sashFrameThickness, panelHeight, frameDepth * 0.72, panelW / 2 - sashFrameThickness / 2, 0, 0);
1115
+ currentWindowSlot = 'glass';
891
1116
  addBox(parent, glassMaterial, glassW, glassH, glassDepth, 0, 0, 0);
892
1117
  }
893
1118
  function addSingleHungWindowVisuals(node, mesh) {
@@ -895,6 +1120,7 @@ function addSingleHungWindowVisuals(node, mesh) {
895
1120
  const innerW = width - 2 * frameThickness;
896
1121
  const innerH = height - 2 * frameThickness;
897
1122
  // Fixed outer frame.
1123
+ currentWindowSlot = 'frame';
898
1124
  addBox(mesh, baseMaterial, width, frameThickness, frameDepth, 0, height / 2 - frameThickness / 2, 0);
899
1125
  addBox(mesh, baseMaterial, width, frameThickness, frameDepth, 0, -height / 2 + frameThickness / 2, 0);
900
1126
  addBox(mesh, baseMaterial, frameThickness, innerH, frameDepth, -width / 2 + frameThickness / 2, 0, 0);
@@ -920,6 +1146,7 @@ function addSingleHungWindowVisuals(node, mesh) {
920
1146
  activeSash.position.set(0, bottomPanelY, bottomZ);
921
1147
  mesh.add(activeSash);
922
1148
  // Side tracks show the lower sash is the moving element.
1149
+ currentWindowSlot = 'frame';
923
1150
  addBox(mesh, baseMaterial, trackThickness, innerH, frameDepth, -innerW / 2 + trackThickness / 2, 0, 0);
924
1151
  addBox(mesh, baseMaterial, trackThickness, innerH, frameDepth, innerW / 2 - trackThickness / 2, 0, 0);
925
1152
  const topSash = new THREE.Group();
@@ -928,12 +1155,14 @@ function addSingleHungWindowVisuals(node, mesh) {
928
1155
  addHungSash(topSash, panelW, panelHeight, sashFrameThickness, frameDepth, glassDepth, glassW, glassH);
929
1156
  addHungSash(activeSash, panelW, panelHeight, sashFrameThickness, frameDepth, glassDepth, glassW, glassH);
930
1157
  // Meeting rails: top sash fixed, bottom sash moves upward over it.
1158
+ currentWindowSlot = 'frame';
931
1159
  addBox(mesh, baseMaterial, panelW, railThickness, frameDepth * 0.78, 0, topPanelY - panelHeight / 2 + railThickness / 2, topZ);
932
1160
  addBox(activeSash, baseMaterial, panelW, railThickness, frameDepth * 0.78, 0, panelHeight / 2 - railThickness / 2, 0);
933
1161
  }
934
1162
  if (sill) {
935
1163
  const sillW = width + sillDepth * 0.4;
936
1164
  const sillZ = frameDepth / 2 + sillDepth / 2;
1165
+ currentWindowSlot = 'frame';
937
1166
  addBox(mesh, baseMaterial, sillW, sillThickness, sillDepth, 0, -height / 2 - sillThickness / 2, sillZ);
938
1167
  }
939
1168
  }
@@ -942,6 +1171,7 @@ function addDoubleHungWindowVisuals(node, mesh) {
942
1171
  const innerW = width - 2 * frameThickness;
943
1172
  const innerH = height - 2 * frameThickness;
944
1173
  // Fixed outer frame.
1174
+ currentWindowSlot = 'frame';
945
1175
  addBox(mesh, baseMaterial, width, frameThickness, frameDepth, 0, height / 2 - frameThickness / 2, 0);
946
1176
  addBox(mesh, baseMaterial, width, frameThickness, frameDepth, 0, -height / 2 + frameThickness / 2, 0);
947
1177
  addBox(mesh, baseMaterial, frameThickness, innerH, frameDepth, -width / 2 + frameThickness / 2, 0, 0);
@@ -971,17 +1201,20 @@ function addDoubleHungWindowVisuals(node, mesh) {
971
1201
  bottomSash.position.set(0, bottomPanelY, bottomZ);
972
1202
  mesh.add(bottomSash);
973
1203
  // Side tracks show both sashes move vertically.
1204
+ currentWindowSlot = 'frame';
974
1205
  addBox(mesh, baseMaterial, trackThickness, innerH, frameDepth, -innerW / 2 + trackThickness / 2, 0, 0);
975
1206
  addBox(mesh, baseMaterial, trackThickness, innerH, frameDepth, innerW / 2 - trackThickness / 2, 0, 0);
976
1207
  addHungSash(topSash, panelW, panelHeight, sashFrameThickness, frameDepth, glassDepth, glassW, glassH);
977
1208
  addHungSash(bottomSash, panelW, panelHeight, sashFrameThickness, frameDepth, glassDepth, glassW, glassH);
978
1209
  // Opposing meeting rails: top sash descends while bottom sash rises.
1210
+ currentWindowSlot = 'frame';
979
1211
  addBox(topSash, baseMaterial, panelW, railThickness, frameDepth * 0.78, 0, -panelHeight / 2 + railThickness / 2, 0);
980
1212
  addBox(bottomSash, baseMaterial, panelW, railThickness, frameDepth * 0.78, 0, panelHeight / 2 - railThickness / 2, 0);
981
1213
  }
982
1214
  if (sill) {
983
1215
  const sillW = width + sillDepth * 0.4;
984
1216
  const sillZ = frameDepth / 2 + sillDepth / 2;
1217
+ currentWindowSlot = 'frame';
985
1218
  addBox(mesh, baseMaterial, sillW, sillThickness, sillDepth, 0, -height / 2 - sillThickness / 2, sillZ);
986
1219
  }
987
1220
  }
@@ -989,6 +1222,7 @@ function addBayWindowVisuals(node, mesh) {
989
1222
  const { width, height, frameDepth, frameThickness, sill, sillDepth, sillThickness } = node;
990
1223
  const innerW = width - 2 * frameThickness;
991
1224
  const innerH = height - 2 * frameThickness;
1225
+ currentWindowSlot = 'frame';
992
1226
  addBox(mesh, baseMaterial, width, frameThickness, frameDepth, 0, height / 2 - frameThickness / 2, 0);
993
1227
  addBox(mesh, baseMaterial, width, frameThickness, frameDepth, 0, -height / 2 + frameThickness / 2, 0);
994
1228
  addBox(mesh, baseMaterial, frameThickness, innerH, frameDepth, -width / 2 + frameThickness / 2, 0, 0);
@@ -1011,13 +1245,16 @@ function addBayWindowVisuals(node, mesh) {
1011
1245
  const addBayPanel = (parent, panelW) => {
1012
1246
  const glassW = Math.max(panelW - 2 * sashFrameThickness, 0.01);
1013
1247
  const glassH = Math.max(innerH - 2 * sashFrameThickness, 0.01);
1248
+ currentWindowSlot = 'frame';
1014
1249
  addBox(parent, baseMaterial, panelW, sashFrameThickness, panelDepth, 0, innerH / 2 - sashFrameThickness / 2, 0);
1015
1250
  addBox(parent, baseMaterial, panelW, sashFrameThickness, panelDepth, 0, -innerH / 2 + sashFrameThickness / 2, 0);
1016
1251
  addBox(parent, baseMaterial, sashFrameThickness, innerH, panelDepth, -panelW / 2 + sashFrameThickness / 2, 0, 0);
1017
1252
  addBox(parent, baseMaterial, sashFrameThickness, innerH, panelDepth, panelW / 2 - sashFrameThickness / 2, 0, 0);
1253
+ currentWindowSlot = 'glass';
1018
1254
  addBox(parent, glassMaterial, glassW, glassH, glassDepth, 0, 0, panelDepth * 0.08);
1019
1255
  };
1020
1256
  const addBayCap = (centerY) => {
1257
+ currentWindowSlot = 'frame';
1021
1258
  const halfThickness = frameThickness / 2;
1022
1259
  const vertices = [];
1023
1260
  const indices = [];
@@ -1032,7 +1269,7 @@ function addBayWindowVisuals(node, mesh) {
1032
1269
  geometry.setAttribute('position', new THREE.Float32BufferAttribute(vertices, 3));
1033
1270
  geometry.setIndex(indices);
1034
1271
  geometry.computeVertexNormals();
1035
- mesh.add(new THREE.Mesh(geometry, baseMaterial));
1272
+ mesh.add(tagWindowSlot(new THREE.Mesh(geometry, baseMaterial)));
1036
1273
  };
1037
1274
  const center = new THREE.Group();
1038
1275
  center.position.set(0, 0, projectionDepth);
@@ -1054,6 +1291,7 @@ function addBayWindowVisuals(node, mesh) {
1054
1291
  if (sill) {
1055
1292
  const sillW = width + sillDepth * 0.4;
1056
1293
  const sillZ = frameDepth / 2 + sillDepth / 2;
1294
+ currentWindowSlot = 'frame';
1057
1295
  addBox(mesh, baseMaterial, sillW, sillThickness, sillDepth, 0, -height / 2 - sillThickness / 2, sillZ);
1058
1296
  }
1059
1297
  }
@@ -1061,6 +1299,7 @@ function addBowWindowVisuals(node, mesh) {
1061
1299
  const { width, height, frameDepth, frameThickness, sill, sillDepth, sillThickness } = node;
1062
1300
  const innerW = width - 2 * frameThickness;
1063
1301
  const innerH = height - 2 * frameThickness;
1302
+ currentWindowSlot = 'frame';
1064
1303
  addBox(mesh, baseMaterial, width, frameThickness, frameDepth, 0, height / 2 - frameThickness / 2, 0);
1065
1304
  addBox(mesh, baseMaterial, width, frameThickness, frameDepth, 0, -height / 2 + frameThickness / 2, 0);
1066
1305
  addBox(mesh, baseMaterial, frameThickness, innerH, frameDepth, -width / 2 + frameThickness / 2, 0, 0);
@@ -1116,13 +1355,17 @@ function addBowWindowVisuals(node, mesh) {
1116
1355
  return geometry;
1117
1356
  };
1118
1357
  const addCurvedMesh = (material, geometry) => {
1119
- mesh.add(new THREE.Mesh(geometry, material));
1358
+ mesh.add(tagWindowSlot(new THREE.Mesh(geometry, material)));
1120
1359
  };
1360
+ currentWindowSlot = 'frame';
1121
1361
  addCurvedMesh(baseMaterial, createCurvedVerticalBand(glassTop, innerH / 2));
1122
1362
  addCurvedMesh(baseMaterial, createCurvedVerticalBand(-innerH / 2, glassBottom));
1363
+ currentWindowSlot = 'glass';
1123
1364
  addCurvedMesh(glassMaterial, createCurvedVerticalBand(glassBottom, glassTop, frameDepth * 0.04));
1365
+ currentWindowSlot = 'frame';
1124
1366
  addCurvedMesh(baseMaterial, createCurvedCap(slabYTop, frameThickness));
1125
1367
  addCurvedMesh(baseMaterial, createCurvedCap(slabYBottom, frameThickness));
1368
+ currentWindowSlot = 'frame';
1126
1369
  for (let index = 0; index <= mullionCount; index += 1) {
1127
1370
  const x = -halfSpan + (innerW * index) / mullionCount;
1128
1371
  addBox(mesh, baseMaterial, sashFrameThickness, innerH, frameDepth * 0.72, x, 0, arcZAt(x));
@@ -1131,6 +1374,7 @@ function addBowWindowVisuals(node, mesh) {
1131
1374
  if (sill) {
1132
1375
  const sillW = width + sillDepth * 0.4;
1133
1376
  const sillZ = frameDepth / 2 + sillDepth / 2;
1377
+ currentWindowSlot = 'frame';
1134
1378
  addBox(mesh, baseMaterial, sillW, sillThickness, sillDepth, 0, -height / 2 - sillThickness / 2, sillZ);
1135
1379
  }
1136
1380
  }
@@ -1142,6 +1386,7 @@ function addLouveredWindowVisuals(node, mesh) {
1142
1386
  }
1143
1387
  const innerW = width - 2 * frameThickness;
1144
1388
  const innerH = height - 2 * frameThickness;
1389
+ currentWindowSlot = 'frame';
1145
1390
  addBox(mesh, baseMaterial, width, frameThickness, frameDepth, 0, height / 2 - frameThickness / 2, 0);
1146
1391
  addBox(mesh, baseMaterial, width, frameThickness, frameDepth, 0, -height / 2 + frameThickness / 2, 0);
1147
1392
  addBox(mesh, baseMaterial, frameThickness, innerH, frameDepth, -width / 2 + frameThickness / 2, 0, 0);
@@ -1157,8 +1402,10 @@ function addLouveredWindowVisuals(node, mesh) {
1157
1402
  const slats = new THREE.Group();
1158
1403
  slats.name = LOUVERED_WINDOW_SLATS_NAME;
1159
1404
  mesh.add(slats);
1405
+ currentWindowSlot = 'frame';
1160
1406
  addBox(mesh, baseMaterial, railThickness, innerH, frameDepth * 0.95, -innerW / 2 + railThickness / 2, 0, 0);
1161
1407
  addBox(mesh, baseMaterial, railThickness, innerH, frameDepth * 0.95, innerW / 2 - railThickness / 2, 0, 0);
1408
+ currentWindowSlot = 'glass';
1162
1409
  for (let index = 0; index < slatCount; index += 1) {
1163
1410
  const y = innerH / 2 - slatGap * (index + 0.5);
1164
1411
  const slat = new THREE.Group();
@@ -1171,6 +1418,7 @@ function addLouveredWindowVisuals(node, mesh) {
1171
1418
  if (sill) {
1172
1419
  const sillW = width + sillDepth * 0.4;
1173
1420
  const sillZ = frameDepth / 2 + sillDepth / 2;
1421
+ currentWindowSlot = 'frame';
1174
1422
  addBox(mesh, baseMaterial, sillW, sillThickness, sillDepth, 0, -height / 2 - sillThickness / 2, sillZ);
1175
1423
  }
1176
1424
  }
@@ -1187,9 +1435,11 @@ function addShapedLouveredWindowVisuals(node, mesh) {
1187
1435
  const innerW = innerRight - innerLeft;
1188
1436
  const innerH = innerTop - innerBottom;
1189
1437
  if (node.openingShape === 'arch') {
1438
+ currentWindowSlot = 'frame';
1190
1439
  addShape(mesh, baseMaterial, createArchedFrameShape(width, height, getClampedArchHeight(width, height, node.archHeight), frameThickness), frameDepth);
1191
1440
  }
1192
1441
  else {
1442
+ currentWindowSlot = 'frame';
1193
1443
  addShape(mesh, baseMaterial, createRoundedFrameShape(width, height, frameThickness, getWindowRoundedRadii(node, width, height)), frameDepth);
1194
1444
  }
1195
1445
  if (innerW > 0.01 && innerH > 0.01) {
@@ -1219,6 +1469,7 @@ function addShapedLouveredWindowVisuals(node, mesh) {
1219
1469
  return (y) => getRoundedHorizontalBoundsAtY(y, innerLeft, innerRight, innerTop, innerRadii);
1220
1470
  })();
1221
1471
  const addVerticalRail = (x) => {
1472
+ currentWindowSlot = 'frame';
1222
1473
  const railX1 = x;
1223
1474
  const railX2 = x + (x < 0 ? railThickness : -railThickness);
1224
1475
  const sampleX = x < 0 ? Math.max(railX1, railX2) : Math.min(railX1, railX2);
@@ -1229,6 +1480,7 @@ function addShapedLouveredWindowVisuals(node, mesh) {
1229
1480
  };
1230
1481
  addVerticalRail(innerLeft);
1231
1482
  addVerticalRail(innerRight);
1483
+ currentWindowSlot = 'glass';
1232
1484
  for (let index = 0; index < slatCount; index += 1) {
1233
1485
  const y = innerTop - slatGap * (index + 0.5);
1234
1486
  const topBounds = getBoundsAtY(Math.min(y + slatHeight / 2, innerTop));
@@ -1248,10 +1500,12 @@ function addShapedLouveredWindowVisuals(node, mesh) {
1248
1500
  if (sill) {
1249
1501
  const sillW = width + sillDepth * 0.4;
1250
1502
  const sillZ = frameDepth / 2 + sillDepth / 2;
1503
+ currentWindowSlot = 'frame';
1251
1504
  addBox(mesh, baseMaterial, sillW, sillThickness, sillDepth, 0, -height / 2 - sillThickness / 2, sillZ);
1252
1505
  }
1253
1506
  }
1254
1507
  function updateWindowMesh(node, mesh) {
1508
+ currentWindowSlot = undefined;
1255
1509
  // Root mesh is an invisible hitbox; all visuals live in child meshes
1256
1510
  mesh.geometry.dispose();
1257
1511
  mesh.geometry = new THREE.BoxGeometry(node.width, node.height, node.frameDepth);
@@ -1266,6 +1520,11 @@ function updateWindowMesh(node, mesh) {
1266
1520
  disposeObjectGeometry(child);
1267
1521
  mesh.remove(child);
1268
1522
  }
1523
+ // Point the builder-facing frame/glass materials at this window's slot
1524
+ // overrides for the duration of its build (recomputed per node, so the next
1525
+ // window resets cleanly without a restore).
1526
+ baseMaterial = resolveWindowSlotMaterial(node, 'frame');
1527
+ glassMaterial = resolveWindowSlotMaterial(node, 'glass');
1269
1528
  const { width, height, frameDepth, frameThickness, columnRatios, rowRatios, columnDividerThickness, rowDividerThickness, sill, sillDepth, sillThickness, openingKind, openingShape, windowType, } = node;
1270
1529
  if (openingKind === 'opening') {
1271
1530
  syncWindowCutout(node, mesh);
@@ -1330,6 +1589,7 @@ function updateWindowMesh(node, mesh) {
1330
1589
  const innerH = height - 2 * frameThickness;
1331
1590
  // ── Frame members ──
1332
1591
  // Top / bottom — full width
1592
+ currentWindowSlot = 'frame';
1333
1593
  addBox(mesh, baseMaterial, width, frameThickness, frameDepth, 0, height / 2 - frameThickness / 2, 0);
1334
1594
  addBox(mesh, baseMaterial, width, frameThickness, frameDepth, 0, -height / 2 + frameThickness / 2, 0);
1335
1595
  // Left / right — inner height to avoid corner overlap
@@ -1364,6 +1624,7 @@ function updateWindowMesh(node, mesh) {
1364
1624
  }
1365
1625
  // Column dividers — full inner height
1366
1626
  cx = -innerW / 2;
1627
+ currentWindowSlot = 'frame';
1367
1628
  for (let c = 0; c < numCols - 1; c++) {
1368
1629
  cx += colWidths[c];
1369
1630
  addBox(mesh, baseMaterial, columnDividerThickness, innerH, frameDepth, cx + columnDividerThickness / 2, 0, 0);
@@ -1371,6 +1632,7 @@ function updateWindowMesh(node, mesh) {
1371
1632
  }
1372
1633
  // Row dividers — per column width, so they don't overlap column dividers (top to bottom)
1373
1634
  cy = innerH / 2;
1635
+ currentWindowSlot = 'frame';
1374
1636
  for (let r = 0; r < numRows - 1; r++) {
1375
1637
  cy -= rowHeights[r];
1376
1638
  const divY = cy - rowDividerThickness / 2;
@@ -1381,6 +1643,7 @@ function updateWindowMesh(node, mesh) {
1381
1643
  }
1382
1644
  // Glass panes
1383
1645
  const glassDepth = Math.max(0.004, frameDepth * 0.08);
1646
+ currentWindowSlot = 'glass';
1384
1647
  for (let c = 0; c < numCols; c++) {
1385
1648
  for (let r = 0; r < numRows; r++) {
1386
1649
  addBox(mesh, glassMaterial, colWidths[c], rowHeights[r], glassDepth, colXCenters[c], rowYCenters[r], 0);
@@ -1391,40 +1654,67 @@ function updateWindowMesh(node, mesh) {
1391
1654
  const sillW = width + sillDepth * 0.4; // slightly wider than frame
1392
1655
  // Protrudes from the front face of the frame (+Z)
1393
1656
  const sillZ = frameDepth / 2 + sillDepth / 2;
1657
+ currentWindowSlot = 'frame';
1394
1658
  addBox(mesh, baseMaterial, sillW, sillThickness, sillDepth, 0, -height / 2 - sillThickness / 2, sillZ);
1395
1659
  }
1396
1660
  syncWindowCutout(node, mesh);
1397
1661
  }
1398
1662
  function syncWindowCutout(node, mesh) {
1399
- // ── Cutout (for wall CSG) — always full window dimensions, 1m deep ──
1663
+ // ── Cutout: invisible raycast hit target for the whole opening ──
1400
1664
  let cutout = mesh.getObjectByName('cutout');
1401
1665
  if (!cutout) {
1402
1666
  cutout = new THREE.Mesh();
1403
1667
  cutout.name = 'cutout';
1668
+ // The cutout (invisible) is proud of the wall on both faces, so it wins the
1669
+ // scene raycast over the wall in front of the recessed window — making it
1670
+ // the selection AND paint hit target for the whole opening. The paint
1671
+ // capability then re-raycasts the window's parts to find the slot. Its depth
1672
+ // is snug to the wall (not 1m) so it no longer blankets the room floor in a
1673
+ // top-down view; the wall CSG ignores this depth (see getOpeningCutoutProxyDepth).
1404
1674
  mesh.add(cutout);
1405
1675
  }
1406
1676
  cutout.geometry.dispose();
1677
+ const depth = resolveOpeningCutoutProxyDepth(node);
1407
1678
  if (isRectangleOnlyWindowType(node)) {
1408
- cutout.geometry = new THREE.BoxGeometry(node.width, node.height, 1.0);
1679
+ cutout.geometry = new THREE.BoxGeometry(node.width, node.height, depth);
1409
1680
  }
1410
1681
  else if (node.openingShape === 'arch') {
1411
1682
  cutout.geometry = new THREE.ExtrudeGeometry(createArchShape(-node.width / 2, node.width / 2, -node.height / 2, node.height / 2, getClampedArchHeight(node.width, node.height, node.archHeight)), {
1412
- depth: 1,
1683
+ depth,
1413
1684
  bevelEnabled: false,
1414
1685
  curveSegments: 24,
1415
1686
  });
1416
- cutout.geometry.translate(0, 0, -0.5);
1687
+ cutout.geometry.translate(0, 0, -depth / 2);
1417
1688
  }
1418
1689
  else if (node.openingShape === 'rounded') {
1419
1690
  cutout.geometry = new THREE.ExtrudeGeometry(createRoundedShape(-node.width / 2, node.width / 2, -node.height / 2, node.height / 2, getWindowRoundedRadii(node, node.width, node.height)), {
1420
- depth: 1,
1691
+ depth,
1421
1692
  bevelEnabled: false,
1422
1693
  curveSegments: 24,
1423
1694
  });
1424
- cutout.geometry.translate(0, 0, -0.5);
1695
+ cutout.geometry.translate(0, 0, -depth / 2);
1425
1696
  }
1426
1697
  else {
1427
- cutout.geometry = new THREE.BoxGeometry(node.width, node.height, 1.0);
1698
+ cutout.geometry = new THREE.BoxGeometry(node.width, node.height, depth);
1428
1699
  }
1429
1700
  cutout.visible = false;
1430
1701
  }
1702
+ // Resolve the cutout proxy depth from the opening's parent wall thickness so
1703
+ // the proxy stays proud of both wall faces (front/back selection) without the
1704
+ // old 1m depth that blanketed the floor. Falls back to the default thickness
1705
+ // when the parent wall isn't a resolvable wall node.
1706
+ function resolveOpeningCutoutProxyDepth(node) {
1707
+ const parentId = node.parentId;
1708
+ const parent = parentId ? useScene.getState().nodes[parentId] : undefined;
1709
+ const wallThickness = parent?.type === 'wall' ? getWallThickness(parent) : DEFAULT_WALL_THICKNESS;
1710
+ return getOpeningCutoutProxyDepth(wallThickness);
1711
+ }
1712
+ /**
1713
+ * Build a fresh window mesh for preview/ghost rendering.
1714
+ * Returns a mesh with an invisible hitbox root and visible children (frame, glass, sash, hardware).
1715
+ */
1716
+ export function buildWindowPreviewMesh(node) {
1717
+ const mesh = new THREE.Mesh();
1718
+ updateWindowMesh(node, mesh);
1719
+ return mesh;
1720
+ }