@pascal-app/core 0.7.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 (318) hide show
  1. package/dist/events/bus.d.ts +47 -18
  2. package/dist/events/bus.d.ts.map +1 -1
  3. package/dist/events/bus.js +1 -1
  4. package/dist/hooks/scene-registry/scene-registry.d.ts +6 -22
  5. package/dist/hooks/scene-registry/scene-registry.d.ts.map +1 -1
  6. package/dist/hooks/scene-registry/scene-registry.js +33 -26
  7. package/dist/hooks/spatial-grid/floor-placed-elevation.d.ts +14 -0
  8. package/dist/hooks/spatial-grid/floor-placed-elevation.d.ts.map +1 -0
  9. package/dist/hooks/spatial-grid/floor-placed-elevation.js +51 -0
  10. package/dist/hooks/spatial-grid/floor-placed-elevation.test.d.ts +2 -0
  11. package/dist/hooks/spatial-grid/floor-placed-elevation.test.d.ts.map +1 -0
  12. package/dist/hooks/spatial-grid/floor-placed-elevation.test.js +273 -0
  13. package/dist/hooks/spatial-grid/spatial-grid-manager.d.ts +13 -2
  14. package/dist/hooks/spatial-grid/spatial-grid-manager.d.ts.map +1 -1
  15. package/dist/hooks/spatial-grid/spatial-grid-manager.js +95 -9
  16. package/dist/hooks/spatial-grid/spatial-grid-sync.d.ts +28 -1
  17. package/dist/hooks/spatial-grid/spatial-grid-sync.d.ts.map +1 -1
  18. package/dist/hooks/spatial-grid/spatial-grid-sync.js +91 -18
  19. package/dist/index.d.ts +22 -4
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +19 -2
  22. package/dist/lib/polygon-geometry.d.ts.map +1 -1
  23. package/dist/lib/polygon-relations.d.ts +10 -0
  24. package/dist/lib/polygon-relations.d.ts.map +1 -0
  25. package/dist/lib/polygon-relations.js +73 -0
  26. package/dist/lib/space-detection-pause.test.d.ts +2 -0
  27. package/dist/lib/space-detection-pause.test.d.ts.map +1 -0
  28. package/dist/lib/space-detection-pause.test.js +42 -0
  29. package/dist/lib/space-detection.d.ts +30 -1
  30. package/dist/lib/space-detection.d.ts.map +1 -1
  31. package/dist/lib/space-detection.js +222 -41
  32. package/dist/lib/space-detection.test.d.ts +2 -0
  33. package/dist/lib/space-detection.test.d.ts.map +1 -0
  34. package/dist/lib/space-detection.test.js +77 -0
  35. package/dist/material-library.d.ts +1 -1
  36. package/dist/material-library.d.ts.map +1 -1
  37. package/dist/material-library.js +1957 -141
  38. package/dist/registry/__bench__/relations-resolver.bench.d.ts +15 -0
  39. package/dist/registry/__bench__/relations-resolver.bench.d.ts.map +1 -0
  40. package/dist/registry/__bench__/relations-resolver.bench.js +170 -0
  41. package/dist/registry/handles.d.ts +321 -0
  42. package/dist/registry/handles.d.ts.map +1 -0
  43. package/dist/registry/handles.js +24 -0
  44. package/dist/registry/index.d.ts +7 -0
  45. package/dist/registry/index.d.ts.map +1 -0
  46. package/dist/registry/index.js +4 -0
  47. package/dist/registry/registry.d.ts +93 -0
  48. package/dist/registry/registry.d.ts.map +1 -0
  49. package/dist/registry/registry.js +201 -0
  50. package/dist/registry/registry.test.d.ts +2 -0
  51. package/dist/registry/registry.test.d.ts.map +1 -0
  52. package/dist/registry/registry.test.js +183 -0
  53. package/dist/registry/relations-resolver.d.ts +42 -0
  54. package/dist/registry/relations-resolver.d.ts.map +1 -0
  55. package/dist/registry/relations-resolver.js +77 -0
  56. package/dist/registry/relations-resolver.test.d.ts +2 -0
  57. package/dist/registry/relations-resolver.test.d.ts.map +1 -0
  58. package/dist/registry/relations-resolver.test.js +183 -0
  59. package/dist/registry/scene-api.d.ts +44 -0
  60. package/dist/registry/scene-api.d.ts.map +1 -0
  61. package/dist/registry/scene-api.js +111 -0
  62. package/dist/registry/scene-api.test.d.ts +2 -0
  63. package/dist/registry/scene-api.test.d.ts.map +1 -0
  64. package/dist/registry/scene-api.test.js +183 -0
  65. package/dist/registry/subtree.d.ts +67 -0
  66. package/dist/registry/subtree.d.ts.map +1 -0
  67. package/dist/registry/subtree.js +119 -0
  68. package/dist/registry/subtree.test.d.ts +2 -0
  69. package/dist/registry/subtree.test.d.ts.map +1 -0
  70. package/dist/registry/subtree.test.js +133 -0
  71. package/dist/registry/types.d.ts +1420 -0
  72. package/dist/registry/types.d.ts.map +1 -0
  73. package/dist/registry/types.js +1 -0
  74. package/dist/schema/asset-url.test.js +0 -4
  75. package/dist/schema/index.d.ts +17 -2
  76. package/dist/schema/index.d.ts.map +1 -1
  77. package/dist/schema/index.js +17 -3
  78. package/dist/schema/material.d.ts +12 -2
  79. package/dist/schema/material.d.ts.map +1 -1
  80. package/dist/schema/material.js +10 -0
  81. package/dist/schema/nodes/box-vent.d.ts +74 -0
  82. package/dist/schema/nodes/box-vent.d.ts.map +1 -0
  83. package/dist/schema/nodes/box-vent.js +63 -0
  84. package/dist/schema/nodes/building.d.ts +1 -1
  85. package/dist/schema/nodes/building.d.ts.map +1 -1
  86. package/dist/schema/nodes/building.js +3 -2
  87. package/dist/schema/nodes/ceiling.d.ts +3 -1
  88. package/dist/schema/nodes/ceiling.d.ts.map +1 -1
  89. package/dist/schema/nodes/ceiling.js +1 -1
  90. package/dist/schema/nodes/chimney.d.ts +158 -0
  91. package/dist/schema/nodes/chimney.d.ts.map +1 -0
  92. package/dist/schema/nodes/chimney.js +67 -0
  93. package/dist/schema/nodes/column.d.ts +598 -6
  94. package/dist/schema/nodes/column.d.ts.map +1 -1
  95. package/dist/schema/nodes/column.js +586 -6
  96. package/dist/schema/nodes/cupola.d.ts +66 -0
  97. package/dist/schema/nodes/cupola.d.ts.map +1 -0
  98. package/dist/schema/nodes/cupola.js +28 -0
  99. package/dist/schema/nodes/door.d.ts +1 -1
  100. package/dist/schema/nodes/dormer.d.ts +228 -0
  101. package/dist/schema/nodes/dormer.d.ts.map +1 -0
  102. package/dist/schema/nodes/dormer.js +119 -0
  103. package/dist/schema/nodes/downspout.d.ts +75 -0
  104. package/dist/schema/nodes/downspout.d.ts.map +1 -0
  105. package/dist/schema/nodes/downspout.js +63 -0
  106. package/dist/schema/nodes/elevator.d.ts +105 -0
  107. package/dist/schema/nodes/elevator.d.ts.map +1 -0
  108. package/dist/schema/nodes/elevator.js +54 -0
  109. package/dist/schema/nodes/eyebrow-vent.d.ts +68 -0
  110. package/dist/schema/nodes/eyebrow-vent.d.ts.map +1 -0
  111. package/dist/schema/nodes/eyebrow-vent.js +40 -0
  112. package/dist/schema/nodes/fence.d.ts +3 -2
  113. package/dist/schema/nodes/fence.d.ts.map +1 -1
  114. package/dist/schema/nodes/fence.js +4 -2
  115. package/dist/schema/nodes/gutter.d.ts +84 -0
  116. package/dist/schema/nodes/gutter.d.ts.map +1 -0
  117. package/dist/schema/nodes/gutter.js +76 -0
  118. package/dist/schema/nodes/item.d.ts +16 -0
  119. package/dist/schema/nodes/item.d.ts.map +1 -1
  120. package/dist/schema/nodes/item.js +21 -0
  121. package/dist/schema/nodes/level.d.ts +1 -1
  122. package/dist/schema/nodes/level.d.ts.map +1 -1
  123. package/dist/schema/nodes/level.js +2 -0
  124. package/dist/schema/nodes/ridge-vent.d.ts +67 -0
  125. package/dist/schema/nodes/ridge-vent.d.ts.map +1 -0
  126. package/dist/schema/nodes/ridge-vent.js +30 -0
  127. package/dist/schema/nodes/roof-segment.d.ts +198 -4
  128. package/dist/schema/nodes/roof-segment.d.ts.map +1 -1
  129. package/dist/schema/nodes/roof-segment.js +254 -3
  130. package/dist/schema/nodes/roof.d.ts +6 -6
  131. package/dist/schema/nodes/roof.d.ts.map +1 -1
  132. package/dist/schema/nodes/roof.js +9 -21
  133. package/dist/schema/nodes/shelf.d.ts +105 -0
  134. package/dist/schema/nodes/shelf.d.ts.map +1 -0
  135. package/dist/schema/nodes/shelf.js +84 -0
  136. package/dist/schema/nodes/site.d.ts +1 -116
  137. package/dist/schema/nodes/site.d.ts.map +1 -1
  138. package/dist/schema/nodes/site.js +2 -6
  139. package/dist/schema/nodes/skylight.d.ts +279 -0
  140. package/dist/schema/nodes/skylight.d.ts.map +1 -0
  141. package/dist/schema/nodes/skylight.js +152 -0
  142. package/dist/schema/nodes/slab.d.ts +3 -1
  143. package/dist/schema/nodes/slab.d.ts.map +1 -1
  144. package/dist/schema/nodes/slab.js +1 -1
  145. package/dist/schema/nodes/solar-panel.d.ts +117 -0
  146. package/dist/schema/nodes/solar-panel.d.ts.map +1 -0
  147. package/dist/schema/nodes/solar-panel.js +47 -0
  148. package/dist/schema/nodes/stair-segment.d.ts +1 -1
  149. package/dist/schema/nodes/stair.d.ts +6 -6
  150. package/dist/schema/nodes/stair.d.ts.map +1 -1
  151. package/dist/schema/nodes/stair.js +9 -7
  152. package/dist/schema/nodes/surface-hole-metadata.d.ts +2 -0
  153. package/dist/schema/nodes/surface-hole-metadata.d.ts.map +1 -1
  154. package/dist/schema/nodes/surface-hole-metadata.js +3 -1
  155. package/dist/schema/nodes/turbine-vent.d.ts +68 -0
  156. package/dist/schema/nodes/turbine-vent.d.ts.map +1 -0
  157. package/dist/schema/nodes/turbine-vent.js +41 -0
  158. package/dist/schema/nodes/wall.d.ts +3 -3
  159. package/dist/schema/nodes/window.d.ts +1 -1
  160. package/dist/schema/types.d.ts +1448 -236
  161. package/dist/schema/types.d.ts.map +1 -1
  162. package/dist/schema/types.js +26 -0
  163. package/dist/services/alignment-anchors.d.ts +100 -0
  164. package/dist/services/alignment-anchors.d.ts.map +1 -0
  165. package/dist/services/alignment-anchors.js +283 -0
  166. package/dist/services/alignment-anchors.test.d.ts +2 -0
  167. package/dist/services/alignment-anchors.test.d.ts.map +1 -0
  168. package/dist/services/alignment-anchors.test.js +383 -0
  169. package/dist/services/alignment.d.ts +126 -0
  170. package/dist/services/alignment.d.ts.map +1 -0
  171. package/dist/services/alignment.js +169 -0
  172. package/dist/services/alignment.test.d.ts +2 -0
  173. package/dist/services/alignment.test.d.ts.map +1 -0
  174. package/dist/services/alignment.test.js +92 -0
  175. package/dist/services/drag-session.d.ts +47 -0
  176. package/dist/services/drag-session.d.ts.map +1 -0
  177. package/dist/services/drag-session.js +100 -0
  178. package/dist/services/drag-session.test.d.ts +2 -0
  179. package/dist/services/drag-session.test.d.ts.map +1 -0
  180. package/dist/services/drag-session.test.js +219 -0
  181. package/dist/services/hosting.d.ts +80 -0
  182. package/dist/services/hosting.d.ts.map +1 -0
  183. package/dist/services/hosting.js +122 -0
  184. package/dist/services/hosting.test.d.ts +2 -0
  185. package/dist/services/hosting.test.d.ts.map +1 -0
  186. package/dist/services/hosting.test.js +199 -0
  187. package/dist/services/index.d.ts +7 -0
  188. package/dist/services/index.d.ts.map +1 -0
  189. package/dist/services/index.js +6 -0
  190. package/dist/services/movement.d.ts +48 -0
  191. package/dist/services/movement.d.ts.map +1 -0
  192. package/dist/services/movement.js +65 -0
  193. package/dist/services/movement.test.d.ts +2 -0
  194. package/dist/services/movement.test.d.ts.map +1 -0
  195. package/dist/services/movement.test.js +133 -0
  196. package/dist/services/single-undo-dance.test.d.ts +2 -0
  197. package/dist/services/single-undo-dance.test.d.ts.map +1 -0
  198. package/dist/services/single-undo-dance.test.js +210 -0
  199. package/dist/services/snap.d.ts +72 -0
  200. package/dist/services/snap.d.ts.map +1 -0
  201. package/dist/services/snap.js +120 -0
  202. package/dist/services/snap.test.d.ts +2 -0
  203. package/dist/services/snap.test.d.ts.map +1 -0
  204. package/dist/services/snap.test.js +95 -0
  205. package/dist/solar-panel-presets.d.ts +17 -0
  206. package/dist/solar-panel-presets.d.ts.map +1 -0
  207. package/dist/solar-panel-presets.js +43 -0
  208. package/dist/store/actions/node-actions.d.ts +14 -2
  209. package/dist/store/actions/node-actions.d.ts.map +1 -1
  210. package/dist/store/actions/node-actions.js +499 -20
  211. package/dist/store/actions/node-mutation-sanitize.test.d.ts +2 -0
  212. package/dist/store/actions/node-mutation-sanitize.test.d.ts.map +1 -0
  213. package/dist/store/actions/node-mutation-sanitize.test.js +147 -0
  214. package/dist/store/actions/reparent.test.d.ts +2 -0
  215. package/dist/store/actions/reparent.test.d.ts.map +1 -0
  216. package/dist/store/actions/reparent.test.js +212 -0
  217. package/dist/store/use-interactive.d.ts +39 -0
  218. package/dist/store/use-interactive.d.ts.map +1 -1
  219. package/dist/store/use-interactive.js +75 -0
  220. package/dist/store/use-live-node-overrides.d.ts +22 -0
  221. package/dist/store/use-live-node-overrides.d.ts.map +1 -0
  222. package/dist/store/use-live-node-overrides.js +59 -0
  223. package/dist/store/use-scene-elevator-migration.test.d.ts +2 -0
  224. package/dist/store/use-scene-elevator-migration.test.d.ts.map +1 -0
  225. package/dist/store/use-scene-elevator-migration.test.js +109 -0
  226. package/dist/store/use-scene.d.ts +11 -0
  227. package/dist/store/use-scene.d.ts.map +1 -1
  228. package/dist/store/use-scene.js +207 -11
  229. package/dist/systems/elevator/elevator-dispatch.d.ts +12 -0
  230. package/dist/systems/elevator/elevator-dispatch.d.ts.map +1 -0
  231. package/dist/systems/elevator/elevator-dispatch.js +57 -0
  232. package/dist/systems/elevator/elevator-geometry.d.ts +15 -0
  233. package/dist/systems/elevator/elevator-geometry.d.ts.map +1 -0
  234. package/dist/systems/elevator/elevator-geometry.js +50 -0
  235. package/dist/systems/elevator/elevator-opening-sync.d.ts +6 -0
  236. package/dist/systems/elevator/elevator-opening-sync.d.ts.map +1 -0
  237. package/dist/systems/elevator/elevator-opening-sync.js +218 -0
  238. package/dist/systems/elevator/elevator-opening-sync.test.d.ts +2 -0
  239. package/dist/systems/elevator/elevator-opening-sync.test.d.ts.map +1 -0
  240. package/dist/systems/elevator/elevator-opening-sync.test.js +146 -0
  241. package/dist/systems/elevator/elevator-opening-system.d.ts +2 -0
  242. package/dist/systems/elevator/elevator-opening-system.d.ts.map +1 -0
  243. package/dist/systems/elevator/elevator-opening-system.js +48 -0
  244. package/dist/systems/elevator/elevator-runtime-system.d.ts +2 -0
  245. package/dist/systems/elevator/elevator-runtime-system.d.ts.map +1 -0
  246. package/dist/systems/elevator/elevator-runtime-system.js +8 -0
  247. package/dist/systems/elevator/elevator-runtime.d.ts +18 -0
  248. package/dist/systems/elevator/elevator-runtime.d.ts.map +1 -0
  249. package/dist/systems/elevator/elevator-runtime.js +225 -0
  250. package/dist/systems/elevator/elevator-runtime.test.d.ts +2 -0
  251. package/dist/systems/elevator/elevator-runtime.test.d.ts.map +1 -0
  252. package/dist/systems/elevator/elevator-runtime.test.js +64 -0
  253. package/dist/systems/elevator/elevator-service.d.ts +19 -0
  254. package/dist/systems/elevator/elevator-service.d.ts.map +1 -0
  255. package/dist/systems/elevator/elevator-service.js +111 -0
  256. package/dist/systems/stair/stair-footprint.d.ts +44 -0
  257. package/dist/systems/stair/stair-footprint.d.ts.map +1 -0
  258. package/dist/systems/stair/stair-footprint.js +168 -0
  259. package/dist/systems/stair/stair-opening-preview.d.ts +1777 -0
  260. package/dist/systems/stair/stair-opening-preview.d.ts.map +1 -0
  261. package/dist/systems/stair/stair-opening-preview.js +93 -0
  262. package/dist/systems/stair/stair-opening-preview.test.d.ts +2 -0
  263. package/dist/systems/stair/stair-opening-preview.test.d.ts.map +1 -0
  264. package/dist/systems/stair/stair-opening-preview.test.js +80 -0
  265. package/dist/systems/stair/stair-opening-sync.d.ts.map +1 -1
  266. package/dist/systems/stair/stair-opening-sync.js +183 -185
  267. package/dist/systems/stair/stair-opening-sync.test.js +385 -4
  268. package/dist/systems/stair/stair-opening-system.d.ts +2 -0
  269. package/dist/systems/stair/stair-opening-system.d.ts.map +1 -0
  270. package/dist/systems/stair/stair-opening-system.js +104 -0
  271. package/dist/systems/wall/wall-curve.d.ts +1 -1
  272. package/dist/systems/wall/wall-curve.d.ts.map +1 -1
  273. package/dist/systems/wall/wall-curve.js +1 -1
  274. package/dist/systems/wall/wall-mitering.d.ts.map +1 -1
  275. package/dist/systems/wall/wall-mitering.js +2 -6
  276. package/dist/systems/wall/wall-move.d.ts +48 -0
  277. package/dist/systems/wall/wall-move.d.ts.map +1 -0
  278. package/dist/systems/wall/wall-move.js +193 -0
  279. package/dist/utils/clone-scene-graph.d.ts +6 -4
  280. package/dist/utils/clone-scene-graph.d.ts.map +1 -1
  281. package/dist/utils/clone-scene-graph.js +6 -4
  282. package/dist/utils/clone-scene-graph.test.d.ts +2 -0
  283. package/dist/utils/clone-scene-graph.test.d.ts.map +1 -0
  284. package/dist/utils/clone-scene-graph.test.js +59 -0
  285. package/dist/validation/validate-build-json.d.ts +43 -0
  286. package/dist/validation/validate-build-json.d.ts.map +1 -0
  287. package/dist/validation/validate-build-json.js +227 -0
  288. package/package.json +11 -3
  289. package/dist/materials.d.ts +0 -10
  290. package/dist/materials.d.ts.map +0 -1
  291. package/dist/materials.js +0 -22
  292. package/dist/systems/ceiling/ceiling-system.d.ts +0 -8
  293. package/dist/systems/ceiling/ceiling-system.d.ts.map +0 -1
  294. package/dist/systems/ceiling/ceiling-system.js +0 -92
  295. package/dist/systems/door/door-system.d.ts +0 -2
  296. package/dist/systems/door/door-system.d.ts.map +0 -1
  297. package/dist/systems/door/door-system.js +0 -195
  298. package/dist/systems/fence/fence-system.d.ts +0 -2
  299. package/dist/systems/fence/fence-system.d.ts.map +0 -1
  300. package/dist/systems/fence/fence-system.js +0 -187
  301. package/dist/systems/item/item-system.d.ts +0 -2
  302. package/dist/systems/item/item-system.d.ts.map +0 -1
  303. package/dist/systems/item/item-system.js +0 -48
  304. package/dist/systems/roof/roof-system.d.ts +0 -16
  305. package/dist/systems/roof/roof-system.d.ts.map +0 -1
  306. package/dist/systems/roof/roof-system.js +0 -797
  307. package/dist/systems/slab/slab-system.d.ts +0 -8
  308. package/dist/systems/slab/slab-system.d.ts.map +0 -1
  309. package/dist/systems/slab/slab-system.js +0 -214
  310. package/dist/systems/stair/stair-system.d.ts +0 -2
  311. package/dist/systems/stair/stair-system.d.ts.map +0 -1
  312. package/dist/systems/stair/stair-system.js +0 -776
  313. package/dist/systems/wall/wall-system.d.ts +0 -12
  314. package/dist/systems/wall/wall-system.d.ts.map +0 -1
  315. package/dist/systems/wall/wall-system.js +0 -455
  316. package/dist/systems/window/window-system.d.ts +0 -2
  317. package/dist/systems/window/window-system.d.ts.map +0 -1
  318. package/dist/systems/window/window-system.js +0 -131
@@ -1,9 +1,10 @@
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';
3
- const CURVED_STAIR_SLAB_OPENING_RATIO = 0.9;
4
+ import { computeSegmentTransforms, rotateXZ } from './stair-footprint';
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;
6
- const CURVED_STAIR_OPENING_STEP_PADDING = 3;
7
8
  function clamp(value, min, max) {
8
9
  return Math.min(max, Math.max(min, value));
9
10
  }
@@ -31,99 +32,100 @@ function metadataEqual(left, right) {
31
32
  if (left.length !== right.length)
32
33
  return false;
33
34
  return left.every((entry, index) => entry.source === right[index]?.source &&
35
+ (entry.elevatorId ?? null) === (right[index]?.elevatorId ?? null) &&
34
36
  (entry.stairId ?? null) === (right[index]?.stairId ?? null));
35
37
  }
36
38
  function normalizeExistingMetadata(holes, metadata) {
37
39
  return holes.map((_, index) => metadata?.[index] ?? { source: 'manual' });
38
40
  }
39
- function expandPolygonFromCentroid(polygon, offset) {
40
- if (Math.abs(offset) < 1e-6) {
41
- return polygon.map(([x, z]) => [x, z]);
42
- }
43
- const centroid = polygon.reduce((acc, [x, z]) => {
44
- acc.x += x;
45
- acc.z += z;
46
- return acc;
47
- }, { x: 0, z: 0 });
48
- centroid.x /= Math.max(polygon.length, 1);
49
- centroid.z /= Math.max(polygon.length, 1);
50
- return polygon.map(([x, z]) => {
51
- const dx = x - centroid.x;
52
- const dz = z - centroid.z;
53
- const length = Math.hypot(dx, dz);
54
- if (length < 1e-6) {
55
- return [x, z];
56
- }
57
- const scale = Math.max(0.1, (length + offset) / length);
58
- return [centroid.x + dx * scale, centroid.z + dz * scale];
59
- });
60
- }
61
- function rotateXZ(x, z, angle) {
62
- const cos = Math.cos(angle);
63
- const sin = Math.sin(angle);
64
- return [x * cos + z * sin, -x * sin + z * cos];
65
- }
66
- function computeSegmentTransforms(segments) {
67
- const transforms = [];
68
- let currentX = 0;
69
- let currentY = 0;
70
- let currentZ = 0;
71
- let currentRot = 0;
72
- for (let index = 0; index < segments.length; index++) {
73
- const segment = segments[index];
74
- if (!segment)
75
- continue;
76
- if (index === 0) {
77
- transforms.push({
78
- position: [currentX, currentY, currentZ],
79
- rotation: currentRot,
80
- });
81
- continue;
82
- }
83
- const previous = segments[index - 1];
84
- if (!previous)
85
- continue;
86
- let attachX = 0;
87
- let attachZ = 0;
88
- let rotationDelta = 0;
89
- switch (segment.attachmentSide) {
90
- case 'front':
91
- attachX = 0;
92
- attachZ = previous.length;
93
- break;
94
- case 'left':
95
- attachX = previous.width / 2;
96
- attachZ = previous.length / 2;
97
- rotationDelta = Math.PI / 2;
98
- break;
99
- case 'right':
100
- attachX = -previous.width / 2;
101
- attachZ = previous.length / 2;
102
- rotationDelta = -Math.PI / 2;
103
- break;
104
- }
105
- const [deltaX, deltaZ] = rotateXZ(attachX, attachZ, currentRot);
106
- currentX += deltaX;
107
- currentY += previous.height;
108
- currentZ += deltaZ;
109
- currentRot += rotationDelta;
110
- transforms.push({
111
- position: [currentX, currentY, currentZ],
112
- rotation: currentRot,
113
- });
114
- }
115
- return transforms;
116
- }
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.
117
44
  function getLevelNumber(levelId, nodes) {
118
45
  if (!levelId)
119
46
  return undefined;
120
47
  const node = nodes[levelId];
121
48
  return node?.type === 'level' ? node.level : undefined;
122
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
+ }
123
117
  function getResolvedStairLevelIds(stair, nodes) {
124
- const parentLevelId = resolveLevelId(stair, nodes);
125
- const fromLevelId = stair.fromLevelId ?? parentLevelId;
126
- 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);
127
129
  return { fromLevelId, toLevelId };
128
130
  }
129
131
  function resolveStraightSegments(stair, nodes) {
@@ -180,47 +182,21 @@ function getStraightSegmentSlicePolygon(stair, layout, startAlong, endAlong) {
180
182
  }
181
183
  function getStraightFlightOpeningDepth(stair, segment) {
182
184
  const treadDepth = Math.max(0.2, segment.length / Math.max(segment.stepCount || stair.stepCount || 10, 1));
183
- 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));
184
186
  }
185
187
  function polygonArea(points) {
186
188
  let area = 0;
187
189
  for (let index = 0; index < points.length; index += 1) {
188
190
  const current = points[index];
189
191
  const next = points[(index + 1) % points.length];
190
- if (!current || !next)
192
+ if (!(current && next))
191
193
  continue;
192
194
  area += current[0] * next[1] - next[0] * current[1];
193
195
  }
194
196
  return area / 2;
195
197
  }
196
- function pointOnSegment(point, a, b, tolerance = 1e-6) {
197
- const cross = (point[1] - a[1]) * (b[0] - a[0]) - (point[0] - a[0]) * (b[1] - a[1]);
198
- if (Math.abs(cross) > tolerance)
199
- return false;
200
- const dot = (point[0] - a[0]) * (b[0] - a[0]) + (point[1] - a[1]) * (b[1] - a[1]);
201
- if (dot < -tolerance)
202
- return false;
203
- const lenSq = (b[0] - a[0]) ** 2 + (b[1] - a[1]) ** 2;
204
- return dot <= lenSq + tolerance;
205
- }
206
- function pointInPolygon(point, polygon) {
207
- if (polygon.length < 3)
208
- return false;
209
- let inside = false;
210
- const [x, z] = point;
211
- for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
212
- const a = polygon[i];
213
- const b = polygon[j];
214
- if (pointOnSegment(point, a, b))
215
- return true;
216
- const intersects = a[1] > z !== b[1] > z && x < ((b[0] - a[0]) * (z - a[1])) / (b[1] - a[1]) + a[0];
217
- if (intersects)
218
- inside = !inside;
219
- }
220
- return inside;
221
- }
222
- function polygonContainsPolygon(outer, inner) {
223
- return inner.every((point) => pointInPolygon(point, outer));
198
+ function isCoveredByExistingHole(existingHoles, autoHole) {
199
+ return existingHoles.some((existingHole) => polygonContainsPolygon(existingHole, autoHole));
224
200
  }
225
201
  function getAxisAlignedRectFromPolygon(polygon) {
226
202
  if (polygon.length < 4)
@@ -319,69 +295,60 @@ function buildUnionPolygonsFromRects(rects) {
319
295
  }
320
296
  return polygons;
321
297
  }
322
- function getCurvedOpeningStepCount(stair, innerRadius, outerRadius, totalSweep) {
323
- const stepCount = Math.max(2, Math.round(stair.stepCount ?? 10));
324
- const stepSweep = Math.abs(totalSweep) / stepCount;
325
- const midRadius = Math.max((innerRadius + outerRadius) * 0.5, 0.01);
326
- const treadDepth = Math.max(stepSweep * midRadius, 0.2);
327
- return Math.min(stepCount, Math.max(1, Math.ceil(1.8 / treadDepth), Math.ceil(stepCount * CURVED_STAIR_SLAB_OPENING_RATIO)));
328
- }
329
- function buildArcOpeningPolygon(stair, innerRadius, outerRadius, startAngle, endAngle) {
330
- const sweep = endAngle - startAngle;
331
- const segmentCount = Math.max(10, Math.min(32, Math.ceil(Math.abs(sweep) / (Math.PI / 24) + Math.max(stair.stepCount ?? 1, 1) * 0.5)));
298
+ function getCurvedOpeningPolygon(stair, offset = 0) {
299
+ const width = Math.max(stair.width ?? 1, 0.4);
300
+ const innerRadius = Math.max(0.01, (stair.innerRadius ?? 0.9) - offset);
301
+ const outerRadius = (stair.innerRadius ?? 0.9) + width + offset;
302
+ const totalSweep = stair.sweepAngle ?? Math.PI / 2;
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);
307
+ const startAngle = totalSweep / 2 - openingSweep;
308
+ const endAngle = totalSweep / 2;
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)));
332
310
  const outerPoints = [];
333
311
  const innerPoints = [];
334
312
  for (let index = 0; index <= segmentCount; index++) {
335
313
  const t = index / segmentCount;
336
- const angle = startAngle + sweep * t;
314
+ const angle = startAngle + (endAngle - startAngle) * t;
337
315
  outerPoints.push(toWorldPlanPoint(stair, Math.cos(angle) * outerRadius, Math.sin(angle) * outerRadius));
338
316
  }
339
317
  for (let index = segmentCount; index >= 0; index--) {
340
318
  const t = index / segmentCount;
341
- const angle = startAngle + sweep * t;
319
+ const angle = startAngle + (endAngle - startAngle) * t;
342
320
  innerPoints.push(toWorldPlanPoint(stair, Math.cos(angle) * innerRadius, Math.sin(angle) * innerRadius));
343
321
  }
344
322
  return [...outerPoints, ...innerPoints];
345
323
  }
346
- function getCurvedOpeningPolygon(stair, targetElevation) {
347
- const width = Math.max(stair.width ?? 1, 0.4);
348
- const innerRadius = Math.max(0.2, stair.innerRadius ?? 0.9);
349
- const outerRadius = innerRadius + width;
350
- const totalSweep = stair.sweepAngle ?? Math.PI / 2;
351
- const stepCount = Math.max(2, Math.round(stair.stepCount ?? 10));
352
- const stepHeight = Math.max(stair.totalRise ?? 2.5, 0.1) / stepCount;
353
- const stepSweep = totalSweep / stepCount;
354
- const targetThreshold = Math.max(stepHeight * 2, STRAIGHT_STAIR_TARGET_THRESHOLD_MIN);
355
- const endAngle = totalSweep / 2;
356
- const fallbackStartStepIndex = Math.max(0, stepCount - getCurvedOpeningStepCount(stair, innerRadius, outerRadius, totalSweep));
357
- let startStepIndex = fallbackStartStepIndex;
358
- if (typeof targetElevation === 'number') {
359
- for (let index = 0; index < stepCount; index += 1) {
360
- const stepTopElevation = stepHeight * (index + 1);
361
- if (stepTopElevation >= targetElevation - targetThreshold) {
362
- startStepIndex = Math.max(0, Math.min(fallbackStartStepIndex, index - CURVED_STAIR_OPENING_STEP_PADDING));
363
- break;
364
- }
365
- }
366
- }
367
- const startAngle = -totalSweep / 2 + stepSweep * startStepIndex;
368
- return buildArcOpeningPolygon(stair, innerRadius, outerRadius, startAngle, endAngle);
369
- }
370
- function getSpiralOpeningPolygon(stair) {
371
- 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;
372
326
  const segmentCount = 48;
373
327
  return Array.from({ length: segmentCount }).map((_, index) => {
374
328
  const angle = (index / segmentCount) * Math.PI * 2;
375
329
  return toWorldPlanPoint(stair, Math.cos(angle) * radius, Math.sin(angle) * radius);
376
330
  });
377
331
  }
378
- 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) {
379
346
  const layouts = getStraightStairLayouts(stair, nodes);
380
347
  if (layouts.length === 0)
381
348
  return [];
382
349
  const riserHeight = (stair.totalRise ?? 2.5) / Math.max(stair.stepCount ?? 10, 1);
383
350
  const targetThreshold = Math.max(riserHeight * 2, STRAIGHT_STAIR_TARGET_THRESHOLD_MIN);
384
- const openingOffset = Math.max(stair.openingOffset ?? 0, 0);
351
+ const openingOffset = Math.max(openingOffsetOverride ?? stair.openingOffset ?? 0, 0);
385
352
  const openingRects = [];
386
353
  for (let index = 0; index < layouts.length; index += 1) {
387
354
  const layout = layouts[index];
@@ -434,20 +401,41 @@ function getStraightOpeningPolygonsForSurface(stair, nodes, targetElevation) {
434
401
  }
435
402
  return fallbackLayout ? [getStraightSegmentFootprintPolygon(stair, fallbackLayout)] : [];
436
403
  }
437
- function getStairOpeningPolygons(stair, nodes, targetElevation) {
404
+ function getStairOpeningPolygons(stair, nodes, targetElevation, openingOffsetOverride) {
438
405
  if ((stair.slabOpeningMode ?? 'none') !== 'destination') {
439
406
  return [];
440
407
  }
408
+ const openingOffset = Math.max(openingOffsetOverride ?? stair.openingOffset ?? 0, 0);
441
409
  if (stair.stairType === 'curved') {
442
- return [getCurvedOpeningPolygon(stair, targetElevation)];
410
+ return [
411
+ getCurvedOpeningPolygon(stair, Math.max(openingOffset - STAIR_SLAB_OPENING_TIGHTENING, 0)),
412
+ ];
443
413
  }
444
414
  if (stair.stairType === 'spiral') {
445
- 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;
446
421
  }
447
422
  if (typeof targetElevation === 'number') {
448
- return getStraightOpeningPolygonsForSurface(stair, nodes, targetElevation);
423
+ return getStraightOpeningPolygonsForSurface(stair, nodes, targetElevation, openingOffset);
424
+ }
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;
449
433
  }
450
- return getStraightOpeningPolygonsForSurface(stair, nodes, Math.max(...getStraightStairLayouts(stair, nodes).map((layout) => layout.topElevation), 0));
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;
451
439
  }
452
440
  function getTargetSlabElevationForStair(stair, slab, slabLevelId, nodes) {
453
441
  const { fromLevelId } = getResolvedStairLevelIds(stair, nodes);
@@ -476,6 +464,8 @@ function shouldApplyStairToSlab(stair, slabLevelId, nodes) {
476
464
  const fromLevel = getLevelNumber(fromLevelId, nodes);
477
465
  const toLevel = getLevelNumber(toLevelId, nodes);
478
466
  const slabLevel = getLevelNumber(slabLevelId, nodes);
467
+ if (!isInStairBuildingScope(stair, slabLevelId, nodes))
468
+ return false;
479
469
  if (slabLevel === undefined) {
480
470
  return toLevelId === slabLevelId;
481
471
  }
@@ -491,6 +481,8 @@ function shouldApplyStairToCeiling(stair, ceilingLevelId, nodes) {
491
481
  const fromLevel = getLevelNumber(fromLevelId, nodes);
492
482
  const toLevel = getLevelNumber(toLevelId, nodes);
493
483
  const ceilingLevel = getLevelNumber(ceilingLevelId, nodes);
484
+ if (!isInStairBuildingScope(stair, ceilingLevelId, nodes))
485
+ return false;
494
486
  if (ceilingLevel === undefined) {
495
487
  return fromLevelId === ceilingLevelId;
496
488
  }
@@ -510,26 +502,29 @@ export function syncAutoStairOpenings(nodes) {
510
502
  const slabLevelId = resolveLevelId(slab, nodes);
511
503
  const existingHoles = slab.holes ?? [];
512
504
  const existingMetadata = normalizeExistingMetadata(existingHoles, slab.holeMetadata);
513
- const manualHoles = existingHoles.filter((_hole, index) => existingMetadata[index]?.source !== 'stair');
514
- const manualMetadata = existingMetadata
515
- .filter((entry) => entry.source !== 'stair')
516
- .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);
517
509
  const stairHoles = stairs
518
510
  .filter((stair) => shouldApplyStairToSlab(stair, slabLevelId, nodes))
519
- .flatMap((stair) => getStairOpeningPolygons(stair, nodes, getTargetSlabElevationForStair(stair, slab, slabLevelId, nodes)).map((polygon) => ({
520
- polygon: stair.stairType === 'straight'
521
- ? polygon
522
- : 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,
523
513
  metadata: {
524
514
  source: 'stair',
525
515
  stairId: stair.id,
526
516
  },
527
517
  })))
528
- .filter((hole) => polygonContainsPolygon(slab.polygon, hole.polygon));
529
- const nextHoles = [...manualHoles, ...stairHoles.map((hole) => hole.polygon)];
530
- const nextMetadata = [...manualMetadata, ...stairHoles.map((hole) => hole.metadata)];
531
- if (!polygonsEqual(existingHoles, nextHoles) ||
532
- !metadataEqual(existingMetadata, nextMetadata)) {
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
+ ];
527
+ if (!(polygonsEqual(existingHoles, nextHoles) && metadataEqual(existingMetadata, nextMetadata))) {
533
528
  updates.push({
534
529
  id: slab.id,
535
530
  data: {
@@ -543,26 +538,29 @@ export function syncAutoStairOpenings(nodes) {
543
538
  const ceilingLevelId = resolveLevelId(ceiling, nodes);
544
539
  const existingHoles = ceiling.holes ?? [];
545
540
  const existingMetadata = normalizeExistingMetadata(existingHoles, ceiling.holeMetadata);
546
- const manualHoles = existingHoles.filter((_hole, index) => existingMetadata[index]?.source !== 'stair');
547
- const manualMetadata = existingMetadata
548
- .filter((entry) => entry.source !== 'stair')
549
- .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);
550
545
  const stairHoles = stairs
551
546
  .filter((stair) => shouldApplyStairToCeiling(stair, ceilingLevelId, nodes))
552
- .flatMap((stair) => getStairOpeningPolygons(stair, nodes, getTargetCeilingElevationForStair(stair, ceiling, ceilingLevelId, nodes)).map((polygon) => ({
553
- polygon: stair.stairType === 'straight'
554
- ? polygon
555
- : 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,
556
549
  metadata: {
557
550
  source: 'stair',
558
551
  stairId: stair.id,
559
552
  },
560
553
  })))
561
- .filter((hole) => polygonContainsPolygon(ceiling.polygon, hole.polygon));
562
- const nextHoles = [...manualHoles, ...stairHoles.map((hole) => hole.polygon)];
563
- const nextMetadata = [...manualMetadata, ...stairHoles.map((hole) => hole.metadata)];
564
- if (!polygonsEqual(existingHoles, nextHoles) ||
565
- !metadataEqual(existingMetadata, nextMetadata)) {
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
+ ];
563
+ if (!(polygonsEqual(existingHoles, nextHoles) && metadataEqual(existingMetadata, nextMetadata))) {
566
564
  updates.push({
567
565
  id: ceiling.id,
568
566
  data: {