@pascal-app/core 0.8.0 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (272) hide show
  1. package/dist/events/bus.d.ts +39 -13
  2. package/dist/events/bus.d.ts.map +1 -1
  3. package/dist/hooks/scene-registry/scene-registry.d.ts +6 -22
  4. package/dist/hooks/scene-registry/scene-registry.d.ts.map +1 -1
  5. package/dist/hooks/scene-registry/scene-registry.js +33 -26
  6. package/dist/hooks/spatial-grid/floor-placed-elevation.d.ts +14 -0
  7. package/dist/hooks/spatial-grid/floor-placed-elevation.d.ts.map +1 -0
  8. package/dist/hooks/spatial-grid/floor-placed-elevation.js +51 -0
  9. package/dist/hooks/spatial-grid/floor-placed-elevation.test.d.ts +2 -0
  10. package/dist/hooks/spatial-grid/floor-placed-elevation.test.d.ts.map +1 -0
  11. package/dist/hooks/spatial-grid/floor-placed-elevation.test.js +273 -0
  12. package/dist/hooks/spatial-grid/spatial-grid-manager.d.ts +13 -2
  13. package/dist/hooks/spatial-grid/spatial-grid-manager.d.ts.map +1 -1
  14. package/dist/hooks/spatial-grid/spatial-grid-manager.js +95 -9
  15. package/dist/hooks/spatial-grid/spatial-grid-sync.d.ts +28 -1
  16. package/dist/hooks/spatial-grid/spatial-grid-sync.d.ts.map +1 -1
  17. package/dist/hooks/spatial-grid/spatial-grid-sync.js +91 -18
  18. package/dist/index.d.ts +22 -4
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +19 -2
  21. package/dist/lib/polygon-relations.d.ts +10 -0
  22. package/dist/lib/polygon-relations.d.ts.map +1 -0
  23. package/dist/lib/polygon-relations.js +73 -0
  24. package/dist/lib/space-detection-pause.test.d.ts +2 -0
  25. package/dist/lib/space-detection-pause.test.d.ts.map +1 -0
  26. package/dist/lib/space-detection-pause.test.js +42 -0
  27. package/dist/lib/space-detection.d.ts +30 -1
  28. package/dist/lib/space-detection.d.ts.map +1 -1
  29. package/dist/lib/space-detection.js +215 -36
  30. package/dist/lib/space-detection.test.d.ts +2 -0
  31. package/dist/lib/space-detection.test.d.ts.map +1 -0
  32. package/dist/lib/space-detection.test.js +77 -0
  33. package/dist/material-library.d.ts +1 -1
  34. package/dist/material-library.d.ts.map +1 -1
  35. package/dist/material-library.js +1940 -143
  36. package/dist/registry/__bench__/relations-resolver.bench.d.ts +15 -0
  37. package/dist/registry/__bench__/relations-resolver.bench.d.ts.map +1 -0
  38. package/dist/registry/__bench__/relations-resolver.bench.js +170 -0
  39. package/dist/registry/handles.d.ts +321 -0
  40. package/dist/registry/handles.d.ts.map +1 -0
  41. package/dist/registry/handles.js +24 -0
  42. package/dist/registry/index.d.ts +7 -0
  43. package/dist/registry/index.d.ts.map +1 -0
  44. package/dist/registry/index.js +4 -0
  45. package/dist/registry/registry.d.ts +93 -0
  46. package/dist/registry/registry.d.ts.map +1 -0
  47. package/dist/registry/registry.js +201 -0
  48. package/dist/registry/registry.test.d.ts +2 -0
  49. package/dist/registry/registry.test.d.ts.map +1 -0
  50. package/dist/registry/registry.test.js +183 -0
  51. package/dist/registry/relations-resolver.d.ts +42 -0
  52. package/dist/registry/relations-resolver.d.ts.map +1 -0
  53. package/dist/registry/relations-resolver.js +77 -0
  54. package/dist/registry/relations-resolver.test.d.ts +2 -0
  55. package/dist/registry/relations-resolver.test.d.ts.map +1 -0
  56. package/dist/registry/relations-resolver.test.js +183 -0
  57. package/dist/registry/scene-api.d.ts +44 -0
  58. package/dist/registry/scene-api.d.ts.map +1 -0
  59. package/dist/registry/scene-api.js +111 -0
  60. package/dist/registry/scene-api.test.d.ts +2 -0
  61. package/dist/registry/scene-api.test.d.ts.map +1 -0
  62. package/dist/registry/scene-api.test.js +183 -0
  63. package/dist/registry/subtree.d.ts +67 -0
  64. package/dist/registry/subtree.d.ts.map +1 -0
  65. package/dist/registry/subtree.js +119 -0
  66. package/dist/registry/subtree.test.d.ts +2 -0
  67. package/dist/registry/subtree.test.d.ts.map +1 -0
  68. package/dist/registry/subtree.test.js +133 -0
  69. package/dist/registry/types.d.ts +1420 -0
  70. package/dist/registry/types.d.ts.map +1 -0
  71. package/dist/registry/types.js +1 -0
  72. package/dist/schema/index.d.ts +17 -2
  73. package/dist/schema/index.d.ts.map +1 -1
  74. package/dist/schema/index.js +17 -3
  75. package/dist/schema/material.d.ts +10 -0
  76. package/dist/schema/material.d.ts.map +1 -1
  77. package/dist/schema/material.js +10 -0
  78. package/dist/schema/nodes/box-vent.d.ts +74 -0
  79. package/dist/schema/nodes/box-vent.d.ts.map +1 -0
  80. package/dist/schema/nodes/box-vent.js +63 -0
  81. package/dist/schema/nodes/building.d.ts +1 -1
  82. package/dist/schema/nodes/building.d.ts.map +1 -1
  83. package/dist/schema/nodes/building.js +3 -2
  84. package/dist/schema/nodes/ceiling.d.ts +2 -0
  85. package/dist/schema/nodes/ceiling.d.ts.map +1 -1
  86. package/dist/schema/nodes/ceiling.js +1 -1
  87. package/dist/schema/nodes/chimney.d.ts +158 -0
  88. package/dist/schema/nodes/chimney.d.ts.map +1 -0
  89. package/dist/schema/nodes/chimney.js +67 -0
  90. package/dist/schema/nodes/column.d.ts +597 -5
  91. package/dist/schema/nodes/column.d.ts.map +1 -1
  92. package/dist/schema/nodes/column.js +586 -6
  93. package/dist/schema/nodes/cupola.d.ts +66 -0
  94. package/dist/schema/nodes/cupola.d.ts.map +1 -0
  95. package/dist/schema/nodes/cupola.js +28 -0
  96. package/dist/schema/nodes/dormer.d.ts +228 -0
  97. package/dist/schema/nodes/dormer.d.ts.map +1 -0
  98. package/dist/schema/nodes/dormer.js +119 -0
  99. package/dist/schema/nodes/downspout.d.ts +75 -0
  100. package/dist/schema/nodes/downspout.d.ts.map +1 -0
  101. package/dist/schema/nodes/downspout.js +63 -0
  102. package/dist/schema/nodes/elevator.d.ts +105 -0
  103. package/dist/schema/nodes/elevator.d.ts.map +1 -0
  104. package/dist/schema/nodes/elevator.js +54 -0
  105. package/dist/schema/nodes/eyebrow-vent.d.ts +68 -0
  106. package/dist/schema/nodes/eyebrow-vent.d.ts.map +1 -0
  107. package/dist/schema/nodes/eyebrow-vent.js +40 -0
  108. package/dist/schema/nodes/fence.d.ts +1 -0
  109. package/dist/schema/nodes/fence.d.ts.map +1 -1
  110. package/dist/schema/nodes/fence.js +2 -0
  111. package/dist/schema/nodes/gutter.d.ts +84 -0
  112. package/dist/schema/nodes/gutter.d.ts.map +1 -0
  113. package/dist/schema/nodes/gutter.js +76 -0
  114. package/dist/schema/nodes/item.d.ts +4 -0
  115. package/dist/schema/nodes/item.d.ts.map +1 -1
  116. package/dist/schema/nodes/item.js +9 -0
  117. package/dist/schema/nodes/level.d.ts +1 -1
  118. package/dist/schema/nodes/level.d.ts.map +1 -1
  119. package/dist/schema/nodes/level.js +2 -0
  120. package/dist/schema/nodes/ridge-vent.d.ts +67 -0
  121. package/dist/schema/nodes/ridge-vent.d.ts.map +1 -0
  122. package/dist/schema/nodes/ridge-vent.js +30 -0
  123. package/dist/schema/nodes/roof-segment.d.ts +195 -1
  124. package/dist/schema/nodes/roof-segment.d.ts.map +1 -1
  125. package/dist/schema/nodes/roof-segment.js +254 -3
  126. package/dist/schema/nodes/roof.d.ts.map +1 -1
  127. package/dist/schema/nodes/roof.js +4 -16
  128. package/dist/schema/nodes/shelf.d.ts +105 -0
  129. package/dist/schema/nodes/shelf.d.ts.map +1 -0
  130. package/dist/schema/nodes/shelf.js +84 -0
  131. package/dist/schema/nodes/site.d.ts +1 -122
  132. package/dist/schema/nodes/site.d.ts.map +1 -1
  133. package/dist/schema/nodes/site.js +2 -6
  134. package/dist/schema/nodes/skylight.d.ts +279 -0
  135. package/dist/schema/nodes/skylight.d.ts.map +1 -0
  136. package/dist/schema/nodes/skylight.js +152 -0
  137. package/dist/schema/nodes/slab.d.ts +2 -0
  138. package/dist/schema/nodes/slab.d.ts.map +1 -1
  139. package/dist/schema/nodes/slab.js +1 -1
  140. package/dist/schema/nodes/solar-panel.d.ts +117 -0
  141. package/dist/schema/nodes/solar-panel.d.ts.map +1 -0
  142. package/dist/schema/nodes/solar-panel.js +47 -0
  143. package/dist/schema/nodes/surface-hole-metadata.d.ts +2 -0
  144. package/dist/schema/nodes/surface-hole-metadata.d.ts.map +1 -1
  145. package/dist/schema/nodes/surface-hole-metadata.js +3 -1
  146. package/dist/schema/nodes/turbine-vent.d.ts +68 -0
  147. package/dist/schema/nodes/turbine-vent.d.ts.map +1 -0
  148. package/dist/schema/nodes/turbine-vent.js +41 -0
  149. package/dist/schema/types.d.ts +1325 -125
  150. package/dist/schema/types.d.ts.map +1 -1
  151. package/dist/schema/types.js +26 -0
  152. package/dist/services/alignment-anchors.d.ts +100 -0
  153. package/dist/services/alignment-anchors.d.ts.map +1 -0
  154. package/dist/services/alignment-anchors.js +283 -0
  155. package/dist/services/alignment-anchors.test.d.ts +2 -0
  156. package/dist/services/alignment-anchors.test.d.ts.map +1 -0
  157. package/dist/services/alignment-anchors.test.js +383 -0
  158. package/dist/services/alignment.d.ts +126 -0
  159. package/dist/services/alignment.d.ts.map +1 -0
  160. package/dist/services/alignment.js +169 -0
  161. package/dist/services/alignment.test.d.ts +2 -0
  162. package/dist/services/alignment.test.d.ts.map +1 -0
  163. package/dist/services/alignment.test.js +92 -0
  164. package/dist/services/drag-session.d.ts +47 -0
  165. package/dist/services/drag-session.d.ts.map +1 -0
  166. package/dist/services/drag-session.js +100 -0
  167. package/dist/services/drag-session.test.d.ts +2 -0
  168. package/dist/services/drag-session.test.d.ts.map +1 -0
  169. package/dist/services/drag-session.test.js +219 -0
  170. package/dist/services/hosting.d.ts +80 -0
  171. package/dist/services/hosting.d.ts.map +1 -0
  172. package/dist/services/hosting.js +122 -0
  173. package/dist/services/hosting.test.d.ts +2 -0
  174. package/dist/services/hosting.test.d.ts.map +1 -0
  175. package/dist/services/hosting.test.js +199 -0
  176. package/dist/services/index.d.ts +7 -0
  177. package/dist/services/index.d.ts.map +1 -0
  178. package/dist/services/index.js +6 -0
  179. package/dist/services/movement.d.ts +48 -0
  180. package/dist/services/movement.d.ts.map +1 -0
  181. package/dist/services/movement.js +65 -0
  182. package/dist/services/movement.test.d.ts +2 -0
  183. package/dist/services/movement.test.d.ts.map +1 -0
  184. package/dist/services/movement.test.js +133 -0
  185. package/dist/services/single-undo-dance.test.d.ts +2 -0
  186. package/dist/services/single-undo-dance.test.d.ts.map +1 -0
  187. package/dist/services/single-undo-dance.test.js +210 -0
  188. package/dist/services/snap.d.ts +72 -0
  189. package/dist/services/snap.d.ts.map +1 -0
  190. package/dist/services/snap.js +120 -0
  191. package/dist/services/snap.test.d.ts +2 -0
  192. package/dist/services/snap.test.d.ts.map +1 -0
  193. package/dist/services/snap.test.js +95 -0
  194. package/dist/solar-panel-presets.d.ts +17 -0
  195. package/dist/solar-panel-presets.d.ts.map +1 -0
  196. package/dist/solar-panel-presets.js +43 -0
  197. package/dist/store/actions/node-actions.d.ts +14 -2
  198. package/dist/store/actions/node-actions.d.ts.map +1 -1
  199. package/dist/store/actions/node-actions.js +492 -15
  200. package/dist/store/actions/node-mutation-sanitize.test.d.ts +2 -0
  201. package/dist/store/actions/node-mutation-sanitize.test.d.ts.map +1 -0
  202. package/dist/store/actions/node-mutation-sanitize.test.js +147 -0
  203. package/dist/store/actions/reparent.test.d.ts +2 -0
  204. package/dist/store/actions/reparent.test.d.ts.map +1 -0
  205. package/dist/store/actions/reparent.test.js +212 -0
  206. package/dist/store/use-interactive.d.ts +39 -0
  207. package/dist/store/use-interactive.d.ts.map +1 -1
  208. package/dist/store/use-interactive.js +75 -0
  209. package/dist/store/use-live-node-overrides.d.ts +22 -0
  210. package/dist/store/use-live-node-overrides.d.ts.map +1 -0
  211. package/dist/store/use-live-node-overrides.js +59 -0
  212. package/dist/store/use-scene-elevator-migration.test.d.ts +2 -0
  213. package/dist/store/use-scene-elevator-migration.test.d.ts.map +1 -0
  214. package/dist/store/use-scene-elevator-migration.test.js +109 -0
  215. package/dist/store/use-scene.d.ts +11 -0
  216. package/dist/store/use-scene.d.ts.map +1 -1
  217. package/dist/store/use-scene.js +196 -6
  218. package/dist/systems/elevator/elevator-dispatch.d.ts +12 -0
  219. package/dist/systems/elevator/elevator-dispatch.d.ts.map +1 -0
  220. package/dist/systems/elevator/elevator-dispatch.js +57 -0
  221. package/dist/systems/elevator/elevator-geometry.d.ts +15 -0
  222. package/dist/systems/elevator/elevator-geometry.d.ts.map +1 -0
  223. package/dist/systems/elevator/elevator-geometry.js +50 -0
  224. package/dist/systems/elevator/elevator-opening-sync.d.ts +6 -0
  225. package/dist/systems/elevator/elevator-opening-sync.d.ts.map +1 -0
  226. package/dist/systems/elevator/elevator-opening-sync.js +218 -0
  227. package/dist/systems/elevator/elevator-opening-sync.test.d.ts +2 -0
  228. package/dist/systems/elevator/elevator-opening-sync.test.d.ts.map +1 -0
  229. package/dist/systems/elevator/elevator-opening-sync.test.js +146 -0
  230. package/dist/systems/elevator/elevator-opening-system.d.ts +2 -0
  231. package/dist/systems/elevator/elevator-opening-system.d.ts.map +1 -0
  232. package/dist/systems/elevator/elevator-opening-system.js +48 -0
  233. package/dist/systems/elevator/elevator-runtime-system.d.ts +2 -0
  234. package/dist/systems/elevator/elevator-runtime-system.d.ts.map +1 -0
  235. package/dist/systems/elevator/elevator-runtime-system.js +8 -0
  236. package/dist/systems/elevator/elevator-runtime.d.ts +18 -0
  237. package/dist/systems/elevator/elevator-runtime.d.ts.map +1 -0
  238. package/dist/systems/elevator/elevator-runtime.js +225 -0
  239. package/dist/systems/elevator/elevator-runtime.test.d.ts +2 -0
  240. package/dist/systems/elevator/elevator-runtime.test.d.ts.map +1 -0
  241. package/dist/systems/elevator/elevator-runtime.test.js +64 -0
  242. package/dist/systems/elevator/elevator-service.d.ts +19 -0
  243. package/dist/systems/elevator/elevator-service.d.ts.map +1 -0
  244. package/dist/systems/elevator/elevator-service.js +111 -0
  245. package/dist/systems/stair/stair-footprint.d.ts +44 -0
  246. package/dist/systems/stair/stair-footprint.d.ts.map +1 -0
  247. package/dist/systems/stair/stair-footprint.js +168 -0
  248. package/dist/systems/stair/stair-opening-preview.d.ts +1777 -0
  249. package/dist/systems/stair/stair-opening-preview.d.ts.map +1 -0
  250. package/dist/systems/stair/stair-opening-preview.js +93 -0
  251. package/dist/systems/stair/stair-opening-preview.test.d.ts +2 -0
  252. package/dist/systems/stair/stair-opening-preview.test.d.ts.map +1 -0
  253. package/dist/systems/stair/stair-opening-preview.test.js +80 -0
  254. package/dist/systems/stair/stair-opening-sync.d.ts.map +1 -1
  255. package/dist/systems/stair/stair-opening-sync.js +172 -147
  256. package/dist/systems/stair/stair-opening-sync.test.js +385 -2
  257. package/dist/systems/stair/stair-opening-system.d.ts +2 -0
  258. package/dist/systems/stair/stair-opening-system.d.ts.map +1 -0
  259. package/dist/systems/stair/stair-opening-system.js +104 -0
  260. package/dist/systems/wall/wall-move.d.ts +48 -0
  261. package/dist/systems/wall/wall-move.d.ts.map +1 -0
  262. package/dist/systems/wall/wall-move.js +193 -0
  263. package/dist/utils/clone-scene-graph.d.ts +6 -4
  264. package/dist/utils/clone-scene-graph.d.ts.map +1 -1
  265. package/dist/utils/clone-scene-graph.js +6 -4
  266. package/dist/utils/clone-scene-graph.test.d.ts +2 -0
  267. package/dist/utils/clone-scene-graph.test.d.ts.map +1 -0
  268. package/dist/utils/clone-scene-graph.test.js +59 -0
  269. package/dist/validation/validate-build-json.d.ts +43 -0
  270. package/dist/validation/validate-build-json.d.ts.map +1 -0
  271. package/dist/validation/validate-build-json.js +227 -0
  272. package/package.json +9 -2
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stair-opening-preview.d.ts","sourceRoot":"","sources":["../../../src/systems/stair/stair-opening-preview.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAA;AAC7E,OAA6B,EAAE,KAAK,iBAAiB,EAAE,MAAM,qCAAqC,CAAA;AAClG,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iCAAiC,CAAA;AAGpE,KAAK,oBAAoB,GAAG;IAC1B,EAAE,EAAE,SAAS,CAAA;IACb,IAAI,EAAE,OAAO,CAAC,QAAQ,GAAG,WAAW,CAAC,CAAA;CACtC,CAAA;AAoBD,wBAAgB,yBAAyB,CACvC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9B,cAAc,EAAE,WAAW,CAAC,MAAM,EAAE,aAAa,CAAC,EAClD,aAAa,EAAE,WAAW,CAAC,MAAM,EAAE,iBAAiB,CAAC,EACrD,iBAAiB,EAAE,WAAW,CAAC,MAAM,CAAC,WAYvC;AAED,wBAAgB,kCAAkC,CAChD,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9B,cAAc,EAAE,WAAW,CAAC,MAAM,EAAE,aAAa,CAAC,EAClD,aAAa,EAAE,WAAW,CAAC,MAAM,EAAE,iBAAiB,CAAC,EACrD,iBAAiB,EAAE,WAAW,CAAC,MAAM,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwBvC;AAED,wBAAgB,qCAAqC;;mBAUlC,oBAAoB,EAAE;;EAiCxC"}
@@ -0,0 +1,93 @@
1
+ import useLiveNodeOverrides, {} from '../../store/use-live-node-overrides';
2
+ import useScene from '../../store/use-scene';
3
+ const SURFACE_OPENING_FIELDS = ['holes', 'holeMetadata'];
4
+ function isSurface(node) {
5
+ return node?.type === 'slab' || node?.type === 'ceiling';
6
+ }
7
+ function isStairOpeningInputNode(node) {
8
+ return node?.type === 'stair' || node?.type === 'stair-segment';
9
+ }
10
+ function omitPreviewSurfaceFields(override) {
11
+ const next = { ...override };
12
+ for (const field of SURFACE_OPENING_FIELDS) {
13
+ delete next[field];
14
+ }
15
+ return next;
16
+ }
17
+ export function hasLiveStairOpeningInputs(nodes, liveTransforms, liveOverrides, previewSurfaceIds) {
18
+ for (const nodeId of liveTransforms.keys()) {
19
+ if (nodes[nodeId]?.type === 'stair')
20
+ return true;
21
+ }
22
+ for (const [nodeId, override] of liveOverrides) {
23
+ if (previewSurfaceIds.has(nodeId))
24
+ continue;
25
+ if (Object.keys(override).length > 0 && isStairOpeningInputNode(nodes[nodeId]))
26
+ return true;
27
+ }
28
+ return false;
29
+ }
30
+ export function getNodesWithLiveStairOpeningInputs(nodes, liveTransforms, liveOverrides, previewSurfaceIds) {
31
+ const nextNodes = { ...nodes };
32
+ for (const [nodeId, override] of liveOverrides) {
33
+ const node = nextNodes[nodeId];
34
+ if (!node)
35
+ continue;
36
+ const values = previewSurfaceIds.has(nodeId) ? omitPreviewSurfaceFields(override) : override;
37
+ if (Object.keys(values).length === 0)
38
+ continue;
39
+ nextNodes[nodeId] = { ...node, ...values };
40
+ }
41
+ for (const [nodeId, transform] of liveTransforms) {
42
+ const node = nextNodes[nodeId];
43
+ if (node?.type !== 'stair')
44
+ continue;
45
+ nextNodes[nodeId] = {
46
+ ...node,
47
+ position: transform.position,
48
+ rotation: transform.rotation,
49
+ };
50
+ }
51
+ return nextNodes;
52
+ }
53
+ export function createSurfaceOpeningPreviewController() {
54
+ const previewSurfaceIds = new Set();
55
+ const clearSurface = (id) => {
56
+ useLiveNodeOverrides.getState().clearFields(id, SURFACE_OPENING_FIELDS);
57
+ useScene.getState().markDirty(id);
58
+ };
59
+ return {
60
+ previewSurfaceIds,
61
+ apply(updates) {
62
+ const scene = useScene.getState();
63
+ const nextSurfaceIds = new Set();
64
+ for (const update of updates) {
65
+ const node = scene.nodes[update.id];
66
+ if (!isSurface(node))
67
+ continue;
68
+ if (!('holes' in update.data || 'holeMetadata' in update.data))
69
+ continue;
70
+ nextSurfaceIds.add(update.id);
71
+ useLiveNodeOverrides.getState().set(update.id, {
72
+ holes: update.data.holes ?? [],
73
+ holeMetadata: update.data.holeMetadata ?? [],
74
+ });
75
+ scene.markDirty(update.id);
76
+ }
77
+ for (const id of previewSurfaceIds) {
78
+ if (!nextSurfaceIds.has(id))
79
+ clearSurface(id);
80
+ }
81
+ previewSurfaceIds.clear();
82
+ for (const id of nextSurfaceIds) {
83
+ previewSurfaceIds.add(id);
84
+ }
85
+ },
86
+ clear() {
87
+ for (const id of previewSurfaceIds) {
88
+ clearSurface(id);
89
+ }
90
+ previewSurfaceIds.clear();
91
+ },
92
+ };
93
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=stair-opening-preview.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stair-opening-preview.test.d.ts","sourceRoot":"","sources":["../../../src/systems/stair/stair-opening-preview.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,80 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+ import { BuildingNode, LevelNode, SlabNode, StairNode, StairSegmentNode } from '../../schema';
3
+ import { getNodesWithLiveStairOpeningInputs, hasLiveStairOpeningInputs, } from './stair-opening-preview';
4
+ import { syncAutoStairOpenings } from './stair-opening-sync';
5
+ describe('stair opening previews', () => {
6
+ test('computes auto openings from live stair transforms', () => {
7
+ const building = BuildingNode.parse({ name: 'Building' });
8
+ const ground = LevelNode.parse({ name: 'Ground', level: 0, parentId: building.id });
9
+ const upper = LevelNode.parse({ name: 'Upper', level: 1, parentId: building.id });
10
+ const slab = SlabNode.parse({
11
+ name: 'Upper Slab',
12
+ parentId: upper.id,
13
+ polygon: [
14
+ [0, 0],
15
+ [5, 0],
16
+ [5, 4],
17
+ [0, 4],
18
+ ],
19
+ });
20
+ const segment = StairSegmentNode.parse({
21
+ parentId: 'stair_live',
22
+ width: 1,
23
+ length: 3,
24
+ height: 2.5,
25
+ stepCount: 12,
26
+ });
27
+ const stair = StairNode.parse({
28
+ id: 'stair_live',
29
+ name: 'Live Stair',
30
+ parentId: ground.id,
31
+ position: [1, 0, 0.2],
32
+ stairType: 'straight',
33
+ fromLevelId: ground.id,
34
+ toLevelId: upper.id,
35
+ slabOpeningMode: 'destination',
36
+ children: [segment.id],
37
+ });
38
+ const nodes = Object.fromEntries([building, ground, upper, slab, stair, { ...segment, parentId: stair.id }].map((node) => [
39
+ node.id,
40
+ node,
41
+ ]));
42
+ const liveTransforms = new Map([
43
+ [stair.id, { position: [3, 0, 0.2], rotation: 0 }],
44
+ ]);
45
+ const liveOverrides = new Map();
46
+ expect(hasLiveStairOpeningInputs(nodes, liveTransforms, liveOverrides, new Set())).toBe(true);
47
+ const previewNodes = getNodesWithLiveStairOpeningInputs(nodes, liveTransforms, liveOverrides, new Set());
48
+ const updates = syncAutoStairOpenings(previewNodes);
49
+ const hole = updates.find((update) => update.id === slab.id)?.data.holes?.[0];
50
+ expect(hole).toBeDefined();
51
+ expect(Math.max(...hole.map(([x]) => x))).toBeGreaterThan(3.4);
52
+ });
53
+ test('ignores its own live surface overrides as preview inputs', () => {
54
+ const slab = SlabNode.parse({
55
+ polygon: [
56
+ [0, 0],
57
+ [4, 0],
58
+ [4, 4],
59
+ [0, 4],
60
+ ],
61
+ });
62
+ const nodes = { [slab.id]: slab };
63
+ const liveOverrides = new Map([
64
+ [
65
+ slab.id,
66
+ {
67
+ holes: [
68
+ [
69
+ [1, 1],
70
+ [2, 1],
71
+ [2, 2],
72
+ [1, 2],
73
+ ],
74
+ ],
75
+ },
76
+ ],
77
+ ]);
78
+ expect(hasLiveStairOpeningInputs(nodes, new Map(), liveOverrides, new Set([slab.id]))).toBe(false);
79
+ });
80
+ });
@@ -1 +1 @@
1
- {"version":3,"file":"stair-opening-sync.d.ts","sourceRoot":"","sources":["../../../src/systems/stair/stair-opening-sync.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,OAAO,EACP,SAAS,EACT,WAAW,EACX,QAAQ,EAGT,MAAM,cAAc,CAAA;AA6pBrB,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;QAQvC,SAAS;UAAQ,OAAO,CAAC,QAAQ,GAAG,WAAW,CAAC;IAuG5E"}
1
+ {"version":3,"file":"stair-opening-sync.d.ts","sourceRoot":"","sources":["../../../src/systems/stair/stair-opening-sync.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,OAAO,EACP,SAAS,EACT,WAAW,EACX,QAAQ,EAIT,MAAM,cAAc,CAAA;AA8rBrB,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;QAQvC,SAAS;UAAQ,OAAO,CAAC,QAAQ,GAAG,WAAW,CAAC;IAqG5E"}
@@ -1,5 +1,7 @@
1
- import { resolveLevelId } from '../../hooks/spatial-grid/spatial-grid-sync';
1
+ import { resolveBuildingForLevel, resolveLevelId } from '../../hooks/spatial-grid/spatial-grid-sync';
2
+ import { polygonContainsPolygon, polygonsOverlap } from '../../lib/polygon-relations';
2
3
  import { DEFAULT_WALL_HEIGHT } from '../wall/wall-footprint';
4
+ import { computeSegmentTransforms, rotateXZ } from './stair-footprint';
3
5
  const CURVED_STAIR_SLAB_OPENING_RATIO = 0.8;
4
6
  const STRAIGHT_STAIR_TARGET_THRESHOLD_MIN = 0.35;
5
7
  const STAIR_SLAB_OPENING_TIGHTENING = 0;
@@ -30,99 +32,100 @@ function metadataEqual(left, right) {
30
32
  if (left.length !== right.length)
31
33
  return false;
32
34
  return left.every((entry, index) => entry.source === right[index]?.source &&
35
+ (entry.elevatorId ?? null) === (right[index]?.elevatorId ?? null) &&
33
36
  (entry.stairId ?? null) === (right[index]?.stairId ?? null));
34
37
  }
35
38
  function normalizeExistingMetadata(holes, metadata) {
36
39
  return holes.map((_, index) => metadata?.[index] ?? { source: 'manual' });
37
40
  }
38
- function expandPolygonFromCentroid(polygon, offset) {
39
- if (Math.abs(offset) < 1e-6) {
40
- return polygon.map(([x, z]) => [x, z]);
41
- }
42
- const centroid = polygon.reduce((acc, [x, z]) => {
43
- acc.x += x;
44
- acc.z += z;
45
- return acc;
46
- }, { x: 0, z: 0 });
47
- centroid.x /= Math.max(polygon.length, 1);
48
- centroid.z /= Math.max(polygon.length, 1);
49
- return polygon.map(([x, z]) => {
50
- const dx = x - centroid.x;
51
- const dz = z - centroid.z;
52
- const length = Math.hypot(dx, dz);
53
- if (length < 1e-6) {
54
- return [x, z];
55
- }
56
- const scale = Math.max(0.1, (length + offset) / length);
57
- return [centroid.x + dx * scale, centroid.z + dz * scale];
58
- });
59
- }
60
- function rotateXZ(x, z, angle) {
61
- const cos = Math.cos(angle);
62
- const sin = Math.sin(angle);
63
- return [x * cos + z * sin, -x * sin + z * cos];
64
- }
65
- function computeSegmentTransforms(segments) {
66
- const transforms = [];
67
- let currentX = 0;
68
- let currentY = 0;
69
- let currentZ = 0;
70
- let currentRot = 0;
71
- for (let index = 0; index < segments.length; index++) {
72
- const segment = segments[index];
73
- if (!segment)
74
- continue;
75
- if (index === 0) {
76
- transforms.push({
77
- position: [currentX, currentY, currentZ],
78
- rotation: currentRot,
79
- });
80
- continue;
81
- }
82
- const previous = segments[index - 1];
83
- if (!previous)
84
- continue;
85
- let attachX = 0;
86
- let attachZ = 0;
87
- let rotationDelta = 0;
88
- switch (segment.attachmentSide) {
89
- case 'front':
90
- attachX = 0;
91
- attachZ = previous.length;
92
- break;
93
- case 'left':
94
- attachX = previous.width / 2;
95
- attachZ = previous.length / 2;
96
- rotationDelta = Math.PI / 2;
97
- break;
98
- case 'right':
99
- attachX = -previous.width / 2;
100
- attachZ = previous.length / 2;
101
- rotationDelta = -Math.PI / 2;
102
- break;
103
- }
104
- const [deltaX, deltaZ] = rotateXZ(attachX, attachZ, currentRot);
105
- currentX += deltaX;
106
- currentY += previous.height;
107
- currentZ += deltaZ;
108
- currentRot += rotationDelta;
109
- transforms.push({
110
- position: [currentX, currentY, currentZ],
111
- rotation: currentRot,
112
- });
113
- }
114
- return transforms;
115
- }
41
+ // (Removing expandPolygonRadially in favor of geometric expansion inside the polygon generators)
42
+ // `rotateXZ` + `computeSegmentTransforms` are shared with the alignment-anchor
43
+ // footprint via `./stair-footprint` so both derive the chain identically.
116
44
  function getLevelNumber(levelId, nodes) {
117
45
  if (!levelId)
118
46
  return undefined;
119
47
  const node = nodes[levelId];
120
48
  return node?.type === 'level' ? node.level : undefined;
121
49
  }
50
+ function getLevelBuildingId(levelId, nodes) {
51
+ if (!levelId)
52
+ return null;
53
+ return resolveBuildingForLevel(levelId, nodes);
54
+ }
55
+ function normalizeLevelId(levelId, nodes) {
56
+ if (!levelId)
57
+ return null;
58
+ return nodes[levelId]?.type === 'level' ? levelId : null;
59
+ }
60
+ function getBuildingLevels(buildingId, nodes) {
61
+ const building = buildingId ? nodes[buildingId] : null;
62
+ if (building?.type !== 'building')
63
+ return [];
64
+ const levels = new Map();
65
+ for (const childId of building.children ?? []) {
66
+ const child = nodes[childId];
67
+ if (child?.type === 'level')
68
+ levels.set(child.id, child);
69
+ }
70
+ for (const candidate of Object.values(nodes)) {
71
+ if (candidate?.type === 'level' && candidate.parentId === building.id) {
72
+ levels.set(candidate.id, candidate);
73
+ }
74
+ }
75
+ return Array.from(levels.values()).sort((left, right) => left.level - right.level);
76
+ }
77
+ function inferSourceLevelForDestination(destinationLevelId, nodes) {
78
+ if (!destinationLevelId)
79
+ return null;
80
+ const destination = nodes[destinationLevelId];
81
+ if (destination?.type !== 'level')
82
+ return null;
83
+ const buildingId = getLevelBuildingId(destinationLevelId, nodes);
84
+ return (getBuildingLevels(buildingId, nodes)
85
+ .filter((level) => level.level < destination.level)
86
+ .at(-1)?.id ?? null);
87
+ }
88
+ function inferDestinationLevelForSource(sourceLevelId, nodes) {
89
+ if (!sourceLevelId)
90
+ return null;
91
+ const source = nodes[sourceLevelId];
92
+ if (source?.type !== 'level')
93
+ return null;
94
+ const buildingId = getLevelBuildingId(sourceLevelId, nodes);
95
+ return (getBuildingLevels(buildingId, nodes).find((level) => level.level > source.level)?.id ?? null);
96
+ }
97
+ function levelsShareBuilding(leftLevelId, rightLevelId, nodes) {
98
+ if (!(leftLevelId && rightLevelId))
99
+ return true;
100
+ const leftBuildingId = getLevelBuildingId(leftLevelId, nodes);
101
+ const rightBuildingId = getLevelBuildingId(rightLevelId, nodes);
102
+ return !(leftBuildingId && rightBuildingId && leftBuildingId !== rightBuildingId);
103
+ }
104
+ function isInStairBuildingScope(stair, surfaceLevelId, nodes) {
105
+ const { fromLevelId, toLevelId } = getResolvedStairLevelIds(stair, nodes);
106
+ const fromBuildingId = getLevelBuildingId(fromLevelId, nodes);
107
+ const toBuildingId = getLevelBuildingId(toLevelId, nodes);
108
+ const surfaceBuildingId = getLevelBuildingId(surfaceLevelId, nodes);
109
+ if (fromBuildingId && toBuildingId && fromBuildingId !== toBuildingId)
110
+ return false;
111
+ if (fromBuildingId && surfaceBuildingId && fromBuildingId !== surfaceBuildingId)
112
+ return false;
113
+ if (toBuildingId && surfaceBuildingId && toBuildingId !== surfaceBuildingId)
114
+ return false;
115
+ return true;
116
+ }
122
117
  function getResolvedStairLevelIds(stair, nodes) {
123
- const parentLevelId = resolveLevelId(stair, nodes);
124
- const fromLevelId = stair.fromLevelId ?? parentLevelId;
125
- const toLevelId = stair.toLevelId ?? fromLevelId;
118
+ const parentLevelId = normalizeLevelId(resolveLevelId(stair, nodes), nodes);
119
+ const explicitToLevelId = normalizeLevelId(stair.toLevelId, nodes);
120
+ const fromLevelId = normalizeLevelId(stair.fromLevelId, nodes) ??
121
+ parentLevelId ??
122
+ inferSourceLevelForDestination(explicitToLevelId, nodes);
123
+ const explicitToLevelIsUsable = explicitToLevelId &&
124
+ explicitToLevelId !== fromLevelId &&
125
+ levelsShareBuilding(fromLevelId, explicitToLevelId, nodes);
126
+ const toLevelId = explicitToLevelIsUsable
127
+ ? explicitToLevelId
128
+ : inferDestinationLevelForSource(fromLevelId, nodes);
126
129
  return { fromLevelId, toLevelId };
127
130
  }
128
131
  function resolveStraightSegments(stair, nodes) {
@@ -179,7 +182,7 @@ function getStraightSegmentSlicePolygon(stair, layout, startAlong, endAlong) {
179
182
  }
180
183
  function getStraightFlightOpeningDepth(stair, segment) {
181
184
  const treadDepth = Math.max(0.2, segment.length / Math.max(segment.stepCount || stair.stepCount || 10, 1));
182
- return Math.min(segment.length, Math.max(treadDepth * 6, segment.length * 0.62, 1.8));
185
+ return Math.min(segment.length, Math.max(treadDepth * 10, segment.length * 0.8, 3.0));
183
186
  }
184
187
  function polygonArea(points) {
185
188
  let area = 0;
@@ -192,34 +195,8 @@ function polygonArea(points) {
192
195
  }
193
196
  return area / 2;
194
197
  }
195
- function pointOnSegment(point, a, b, tolerance = 1e-6) {
196
- const cross = (point[1] - a[1]) * (b[0] - a[0]) - (point[0] - a[0]) * (b[1] - a[1]);
197
- if (Math.abs(cross) > tolerance)
198
- return false;
199
- const dot = (point[0] - a[0]) * (b[0] - a[0]) + (point[1] - a[1]) * (b[1] - a[1]);
200
- if (dot < -tolerance)
201
- return false;
202
- const lenSq = (b[0] - a[0]) ** 2 + (b[1] - a[1]) ** 2;
203
- return dot <= lenSq + tolerance;
204
- }
205
- function pointInPolygon(point, polygon) {
206
- if (polygon.length < 3)
207
- return false;
208
- let inside = false;
209
- const [x, z] = point;
210
- for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
211
- const a = polygon[i];
212
- const b = polygon[j];
213
- if (pointOnSegment(point, a, b))
214
- return true;
215
- const intersects = a[1] > z !== b[1] > z && x < ((b[0] - a[0]) * (z - a[1])) / (b[1] - a[1]) + a[0];
216
- if (intersects)
217
- inside = !inside;
218
- }
219
- return inside;
220
- }
221
- function polygonContainsPolygon(outer, inner) {
222
- return inner.every((point) => pointInPolygon(point, outer));
198
+ function isCoveredByExistingHole(existingHoles, autoHole) {
199
+ return existingHoles.some((existingHole) => polygonContainsPolygon(existingHole, autoHole));
223
200
  }
224
201
  function getAxisAlignedRectFromPolygon(polygon) {
225
202
  if (polygon.length < 4)
@@ -318,13 +295,15 @@ function buildUnionPolygonsFromRects(rects) {
318
295
  }
319
296
  return polygons;
320
297
  }
321
- function getCurvedOpeningPolygon(stair) {
298
+ function getCurvedOpeningPolygon(stair, offset = 0) {
322
299
  const width = Math.max(stair.width ?? 1, 0.4);
323
- const innerRadius = Math.max(0.2, stair.innerRadius ?? 0.9);
324
- const outerRadius = innerRadius + width;
300
+ const innerRadius = Math.max(0.01, (stair.innerRadius ?? 0.9) - offset);
301
+ const outerRadius = (stair.innerRadius ?? 0.9) + width + offset;
325
302
  const totalSweep = stair.sweepAngle ?? Math.PI / 2;
326
- const openingSweep = Math.sign(totalSweep || 1) *
327
- Math.max(Math.abs(totalSweep) * CURVED_STAIR_SLAB_OPENING_RATIO, Math.abs(totalSweep) / Math.max(stair.stepCount ?? 1, 1));
303
+ const baseOpeningSweep = Math.abs(totalSweep) *
304
+ Math.max(CURVED_STAIR_SLAB_OPENING_RATIO, 1 / Math.max(stair.stepCount ?? 1, 1));
305
+ const angleOffset = offset / Math.max(innerRadius, 0.1);
306
+ const openingSweep = Math.sign(totalSweep || 1) * Math.min(Math.abs(totalSweep), baseOpeningSweep + angleOffset * 2);
328
307
  const startAngle = totalSweep / 2 - openingSweep;
329
308
  const endAngle = totalSweep / 2;
330
309
  const segmentCount = Math.max(10, Math.min(32, Math.ceil(Math.abs(openingSweep) / (Math.PI / 24) + Math.max(stair.stepCount ?? 1, 1) * 0.5)));
@@ -342,21 +321,34 @@ function getCurvedOpeningPolygon(stair) {
342
321
  }
343
322
  return [...outerPoints, ...innerPoints];
344
323
  }
345
- function getSpiralOpeningPolygon(stair) {
346
- const radius = Math.max(0.05, stair.innerRadius ?? 0.9) + Math.max(stair.width ?? 1, 0.4);
324
+ function getSpiralOpeningPolygon(stair, offset = 0) {
325
+ const radius = Math.max(0.05, stair.innerRadius ?? 0.9) + Math.max(stair.width ?? 1, 0.4) + offset;
347
326
  const segmentCount = 48;
348
327
  return Array.from({ length: segmentCount }).map((_, index) => {
349
328
  const angle = (index / segmentCount) * Math.PI * 2;
350
329
  return toWorldPlanPoint(stair, Math.cos(angle) * radius, Math.sin(angle) * radius);
351
330
  });
352
331
  }
353
- function getStraightOpeningPolygonsForSurface(stair, nodes, targetElevation) {
332
+ function getSpiralLandingPolygon(stair, offset = 0) {
333
+ const width = Math.max(stair.width ?? 1, 0.4);
334
+ const outerRadius = Math.max(0.05, (stair.innerRadius ?? 0.9) + width);
335
+ const depth = Math.max(stair.topLandingDepth ?? 0.9, 0.1);
336
+ const halfWidth = width / 2;
337
+ const localPoints = [
338
+ [outerRadius - offset, -halfWidth - offset],
339
+ [outerRadius + depth + offset, -halfWidth - offset],
340
+ [outerRadius + depth + offset, halfWidth + offset],
341
+ [outerRadius - offset, halfWidth + offset],
342
+ ];
343
+ return localPoints.map(([x, z]) => toWorldPlanPoint(stair, x, z));
344
+ }
345
+ function getStraightOpeningPolygonsForSurface(stair, nodes, targetElevation, openingOffsetOverride) {
354
346
  const layouts = getStraightStairLayouts(stair, nodes);
355
347
  if (layouts.length === 0)
356
348
  return [];
357
349
  const riserHeight = (stair.totalRise ?? 2.5) / Math.max(stair.stepCount ?? 10, 1);
358
350
  const targetThreshold = Math.max(riserHeight * 2, STRAIGHT_STAIR_TARGET_THRESHOLD_MIN);
359
- const openingOffset = Math.max(stair.openingOffset ?? 0, 0);
351
+ const openingOffset = Math.max(openingOffsetOverride ?? stair.openingOffset ?? 0, 0);
360
352
  const openingRects = [];
361
353
  for (let index = 0; index < layouts.length; index += 1) {
362
354
  const layout = layouts[index];
@@ -409,20 +401,41 @@ function getStraightOpeningPolygonsForSurface(stair, nodes, targetElevation) {
409
401
  }
410
402
  return fallbackLayout ? [getStraightSegmentFootprintPolygon(stair, fallbackLayout)] : [];
411
403
  }
412
- function getStairOpeningPolygons(stair, nodes, targetElevation) {
404
+ function getStairOpeningPolygons(stair, nodes, targetElevation, openingOffsetOverride) {
413
405
  if ((stair.slabOpeningMode ?? 'none') !== 'destination') {
414
406
  return [];
415
407
  }
408
+ const openingOffset = Math.max(openingOffsetOverride ?? stair.openingOffset ?? 0, 0);
416
409
  if (stair.stairType === 'curved') {
417
- return [getCurvedOpeningPolygon(stair)];
410
+ return [
411
+ getCurvedOpeningPolygon(stair, Math.max(openingOffset - STAIR_SLAB_OPENING_TIGHTENING, 0)),
412
+ ];
418
413
  }
419
414
  if (stair.stairType === 'spiral') {
420
- return [getSpiralOpeningPolygon(stair)];
415
+ const offset = Math.max(openingOffset - STAIR_SLAB_OPENING_TIGHTENING, 0);
416
+ const polygons = [getSpiralOpeningPolygon(stair, offset)];
417
+ if (stair.topLandingMode === 'integrated') {
418
+ polygons.push(getSpiralLandingPolygon(stair, offset));
419
+ }
420
+ return polygons;
421
421
  }
422
422
  if (typeof targetElevation === 'number') {
423
- return getStraightOpeningPolygonsForSurface(stair, nodes, targetElevation);
423
+ return getStraightOpeningPolygonsForSurface(stair, nodes, targetElevation, openingOffset);
424
424
  }
425
- return getStraightOpeningPolygonsForSurface(stair, nodes, Math.max(...getStraightStairLayouts(stair, nodes).map((layout) => layout.topElevation), 0));
425
+ return getStraightOpeningPolygonsForSurface(stair, nodes, Math.max(...getStraightStairLayouts(stair, nodes).map((layout) => layout.topElevation), 0), openingOffset);
426
+ }
427
+ function getApplicableStairOpeningPolygons(stair, nodes, targetElevation, surfacePolygon) {
428
+ const configuredOffset = Math.max(stair.openingOffset ?? 0, 0);
429
+ const polygons = getStairOpeningPolygons(stair, nodes, targetElevation, configuredOffset);
430
+ const overlappingPolygons = polygons.filter((polygon) => polygonsOverlap(surfacePolygon, polygon));
431
+ if (overlappingPolygons.length === polygons.length || configuredOffset <= 1e-6) {
432
+ return overlappingPolygons;
433
+ }
434
+ const fallbackPolygons = getStairOpeningPolygons(stair, nodes, targetElevation, 0);
435
+ const overlappingFallbackPolygons = fallbackPolygons.filter((polygon) => polygonsOverlap(surfacePolygon, polygon));
436
+ return overlappingFallbackPolygons.length === fallbackPolygons.length
437
+ ? overlappingFallbackPolygons
438
+ : overlappingPolygons;
426
439
  }
427
440
  function getTargetSlabElevationForStair(stair, slab, slabLevelId, nodes) {
428
441
  const { fromLevelId } = getResolvedStairLevelIds(stair, nodes);
@@ -451,6 +464,8 @@ function shouldApplyStairToSlab(stair, slabLevelId, nodes) {
451
464
  const fromLevel = getLevelNumber(fromLevelId, nodes);
452
465
  const toLevel = getLevelNumber(toLevelId, nodes);
453
466
  const slabLevel = getLevelNumber(slabLevelId, nodes);
467
+ if (!isInStairBuildingScope(stair, slabLevelId, nodes))
468
+ return false;
454
469
  if (slabLevel === undefined) {
455
470
  return toLevelId === slabLevelId;
456
471
  }
@@ -466,6 +481,8 @@ function shouldApplyStairToCeiling(stair, ceilingLevelId, nodes) {
466
481
  const fromLevel = getLevelNumber(fromLevelId, nodes);
467
482
  const toLevel = getLevelNumber(toLevelId, nodes);
468
483
  const ceilingLevel = getLevelNumber(ceilingLevelId, nodes);
484
+ if (!isInStairBuildingScope(stair, ceilingLevelId, nodes))
485
+ return false;
469
486
  if (ceilingLevel === undefined) {
470
487
  return fromLevelId === ceilingLevelId;
471
488
  }
@@ -485,24 +502,28 @@ export function syncAutoStairOpenings(nodes) {
485
502
  const slabLevelId = resolveLevelId(slab, nodes);
486
503
  const existingHoles = slab.holes ?? [];
487
504
  const existingMetadata = normalizeExistingMetadata(existingHoles, slab.holeMetadata);
488
- const manualHoles = existingHoles.filter((_hole, index) => existingMetadata[index]?.source !== 'stair');
489
- const manualMetadata = existingMetadata
490
- .filter((entry) => entry.source !== 'stair')
491
- .map((entry) => ({ ...entry }));
505
+ const preservedHoles = existingHoles
506
+ .map((polygon, index) => ({ metadata: existingMetadata[index], polygon }))
507
+ .filter((entry) => entry.metadata.source !== 'stair');
508
+ const preservedHolePolygons = preservedHoles.map((entry) => entry.polygon);
492
509
  const stairHoles = stairs
493
510
  .filter((stair) => shouldApplyStairToSlab(stair, slabLevelId, nodes))
494
- .flatMap((stair) => getStairOpeningPolygons(stair, nodes, getTargetSlabElevationForStair(stair, slab, slabLevelId, nodes)).map((polygon) => ({
495
- polygon: stair.stairType === 'straight'
496
- ? polygon
497
- : expandPolygonFromCentroid(polygon, Math.max((stair.openingOffset ?? 0) - STAIR_SLAB_OPENING_TIGHTENING, 0)),
511
+ .flatMap((stair) => getApplicableStairOpeningPolygons(stair, nodes, getTargetSlabElevationForStair(stair, slab, slabLevelId, nodes), slab.polygon).map((polygon) => ({
512
+ polygon,
498
513
  metadata: {
499
514
  source: 'stair',
500
515
  stairId: stair.id,
501
516
  },
502
517
  })))
503
- .filter((hole) => polygonContainsPolygon(slab.polygon, hole.polygon));
504
- const nextHoles = [...manualHoles, ...stairHoles.map((hole) => hole.polygon)];
505
- const nextMetadata = [...manualMetadata, ...stairHoles.map((hole) => hole.metadata)];
518
+ .filter((hole) => !isCoveredByExistingHole(preservedHolePolygons, hole.polygon));
519
+ const nextHoles = [
520
+ ...preservedHoles.map((hole) => hole.polygon),
521
+ ...stairHoles.map((hole) => hole.polygon),
522
+ ];
523
+ const nextMetadata = [
524
+ ...preservedHoles.map((hole) => ({ ...hole.metadata })),
525
+ ...stairHoles.map((hole) => hole.metadata),
526
+ ];
506
527
  if (!(polygonsEqual(existingHoles, nextHoles) && metadataEqual(existingMetadata, nextMetadata))) {
507
528
  updates.push({
508
529
  id: slab.id,
@@ -517,24 +538,28 @@ export function syncAutoStairOpenings(nodes) {
517
538
  const ceilingLevelId = resolveLevelId(ceiling, nodes);
518
539
  const existingHoles = ceiling.holes ?? [];
519
540
  const existingMetadata = normalizeExistingMetadata(existingHoles, ceiling.holeMetadata);
520
- const manualHoles = existingHoles.filter((_hole, index) => existingMetadata[index]?.source !== 'stair');
521
- const manualMetadata = existingMetadata
522
- .filter((entry) => entry.source !== 'stair')
523
- .map((entry) => ({ ...entry }));
541
+ const preservedHoles = existingHoles
542
+ .map((polygon, index) => ({ metadata: existingMetadata[index], polygon }))
543
+ .filter((entry) => entry.metadata.source !== 'stair');
544
+ const preservedHolePolygons = preservedHoles.map((entry) => entry.polygon);
524
545
  const stairHoles = stairs
525
546
  .filter((stair) => shouldApplyStairToCeiling(stair, ceilingLevelId, nodes))
526
- .flatMap((stair) => getStairOpeningPolygons(stair, nodes, getTargetCeilingElevationForStair(stair, ceiling, ceilingLevelId, nodes)).map((polygon) => ({
527
- polygon: stair.stairType === 'straight'
528
- ? polygon
529
- : expandPolygonFromCentroid(polygon, Math.max((stair.openingOffset ?? 0) - STAIR_SLAB_OPENING_TIGHTENING, 0)),
547
+ .flatMap((stair) => getApplicableStairOpeningPolygons(stair, nodes, getTargetCeilingElevationForStair(stair, ceiling, ceilingLevelId, nodes), ceiling.polygon).map((polygon) => ({
548
+ polygon,
530
549
  metadata: {
531
550
  source: 'stair',
532
551
  stairId: stair.id,
533
552
  },
534
553
  })))
535
- .filter((hole) => polygonContainsPolygon(ceiling.polygon, hole.polygon));
536
- const nextHoles = [...manualHoles, ...stairHoles.map((hole) => hole.polygon)];
537
- const nextMetadata = [...manualMetadata, ...stairHoles.map((hole) => hole.metadata)];
554
+ .filter((hole) => !isCoveredByExistingHole(preservedHolePolygons, hole.polygon));
555
+ const nextHoles = [
556
+ ...preservedHoles.map((hole) => hole.polygon),
557
+ ...stairHoles.map((hole) => hole.polygon),
558
+ ];
559
+ const nextMetadata = [
560
+ ...preservedHoles.map((hole) => ({ ...hole.metadata })),
561
+ ...stairHoles.map((hole) => hole.metadata),
562
+ ];
538
563
  if (!(polygonsEqual(existingHoles, nextHoles) && metadataEqual(existingMetadata, nextMetadata))) {
539
564
  updates.push({
540
565
  id: ceiling.id,