@pascal-app/editor 1.0.1 → 1.0.4

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 (498) hide show
  1. package/package.json +16 -9
  2. package/src/components/editor/action-menu-button.tsx +83 -0
  3. package/src/components/editor/bake-thumbnail.tsx +315 -4
  4. package/src/components/editor/capture-level-picker.tsx +72 -0
  5. package/src/components/editor/custom-camera-controls.tsx +221 -128
  6. package/src/components/editor/delete-confirmation-dialog.tsx +297 -7
  7. package/src/components/editor/duplicate-node.ts +244 -0
  8. package/src/components/editor/editor-layout-mobile.tsx +37 -25
  9. package/src/components/editor/editor-layout-v2.tsx +131 -58
  10. package/src/components/editor/export-manager.tsx +34 -8
  11. package/src/components/editor/first-person/build-collider-world.test.ts +51 -0
  12. package/src/components/editor/first-person/build-collider-world.ts +6 -2
  13. package/src/components/editor/first-person/mechanism-targets.test.ts +75 -0
  14. package/src/components/editor/first-person/mechanism-targets.ts +36 -0
  15. package/src/components/editor/first-person-controls.tsx +205 -2
  16. package/src/components/editor/floating-action-menu.tsx +63 -207
  17. package/src/components/editor/floor-aware-camera-controls.test.ts +442 -0
  18. package/src/components/editor/floor-aware-camera-controls.ts +324 -0
  19. package/src/components/editor/floor-openings-overlay.tsx +243 -0
  20. package/src/components/editor/floor-region-controls.tsx +846 -0
  21. package/src/components/editor/floor-region-webgpu-materials.test.ts +12 -0
  22. package/src/components/editor/floor-surface.test.ts +165 -0
  23. package/src/components/editor/floor-surface.ts +182 -0
  24. package/src/components/editor/floorplan-background-selection.test.ts +11 -2
  25. package/src/components/editor/floorplan-background-selection.ts +1 -3
  26. package/src/components/editor/floorplan-panel.tsx +536 -487
  27. package/src/components/editor/footprint-height-handle.tsx +204 -0
  28. package/src/components/editor/grid.tsx +31 -7
  29. package/src/components/editor/group-actions.ts +59 -11
  30. package/src/components/editor/group-floating-action-menu.tsx +19 -9
  31. package/src/components/editor/group-move-3d.ts +4 -6
  32. package/src/components/editor/group-rotate-handle.tsx +81 -35
  33. package/src/components/editor/group-selection-box-3d.tsx +25 -17
  34. package/src/components/editor/group-transform-shared.test.ts +126 -1
  35. package/src/components/editor/group-transform-shared.ts +79 -73
  36. package/src/components/editor/handles/handle-arrow.tsx +72 -2
  37. package/src/components/editor/handles/handle-hit-priority.test.tsx +63 -0
  38. package/src/components/editor/handles/handle-hit-priority.tsx +37 -0
  39. package/src/components/editor/handles/swallow-next-click.test.ts +28 -0
  40. package/src/components/editor/handles/use-handle-drag.ts +272 -131
  41. package/src/components/editor/index.tsx +316 -176
  42. package/src/components/editor/join-walls-pill.tsx +44 -0
  43. package/src/components/editor/node-action-menu.tsx +70 -70
  44. package/src/components/editor/node-arrow-handles.tsx +167 -14
  45. package/src/components/editor/open-wall-ends-3d-layer.tsx +273 -0
  46. package/src/components/editor/open-wall-ends-hint.tsx +89 -0
  47. package/src/components/editor/quick-measurement-hud.tsx +3 -2
  48. package/src/components/editor/registry-action-contributions.test.tsx +63 -0
  49. package/src/components/editor/registry-action-contributions.tsx +132 -0
  50. package/src/components/editor/room-controls.tsx +847 -0
  51. package/src/components/editor/room-floor-highlight.tsx +128 -0
  52. package/src/components/editor/room-handles-2d.tsx +354 -0
  53. package/src/components/editor/room-handles.tsx +675 -0
  54. package/src/components/editor/room-highlight.tsx +225 -0
  55. package/src/components/editor/room-labels.test.ts +357 -0
  56. package/src/components/editor/room-labels.tsx +990 -0
  57. package/src/components/editor/room-transform-ghost.tsx +256 -0
  58. package/src/components/editor/room-webgpu-materials.test.ts +27 -0
  59. package/src/components/editor/selection-manager.tsx +451 -317
  60. package/src/components/editor/slab-hole-highlights.tsx +3 -0
  61. package/src/components/editor/snapshot-capture-overlay.tsx +4 -0
  62. package/src/components/editor/snapshot-capture.test.ts +52 -4
  63. package/src/components/editor/snapshot-capture.ts +30 -5
  64. package/src/components/editor/thumbnail-generator.test.ts +308 -0
  65. package/src/components/editor/thumbnail-generator.tsx +1035 -229
  66. package/src/components/editor/use-floorplan-background-placement.ts +31 -11
  67. package/src/components/editor/vector-edge-extractor.tsx +121 -0
  68. package/src/components/editor/views/host-view.tsx +61 -0
  69. package/src/components/editor/views/plugins-view.tsx +670 -0
  70. package/src/components/editor/views/scene-region.tsx +59 -0
  71. package/src/components/editor/views/use-editor-views.ts +57 -0
  72. package/src/components/editor/views/view-bar.tsx +220 -0
  73. package/src/components/editor/wall-justification.test.ts +140 -0
  74. package/src/components/editor/wall-move-side-handles.tsx +114 -163
  75. package/src/components/editor/wall-opening-highlights.tsx +2 -3
  76. package/src/components/editor/wall-push-arrow.tsx +92 -0
  77. package/src/components/editor/wall-region-handles.tsx +509 -0
  78. package/src/components/editor/wall-snap-beacon-layer.tsx +3 -0
  79. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +8 -4
  80. package/src/components/editor-2d/floorplan-group-move.tsx +17 -24
  81. package/src/components/editor-2d/floorplan-open-wall-ends-layer.test.tsx +86 -0
  82. package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +290 -0
  83. package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +33 -12
  84. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +11 -1
  85. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +242 -126
  86. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.test.ts +156 -0
  87. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +278 -0
  88. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +77 -0
  89. package/src/components/editor-2d/renderers/floorplan-draft-measurement.tsx +166 -0
  90. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +4 -1
  91. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +87 -8
  92. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +355 -245
  93. package/src/components/editor-2d/svg-paths.ts +12 -15
  94. package/src/components/procedural-items/inspector.tsx +40 -15
  95. package/src/components/systems/ceiling/ceiling-selection-affordance-system.test.ts +97 -14
  96. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +18 -18
  97. package/src/components/systems/zone/zone-system.test.ts +47 -0
  98. package/src/components/systems/zone/zone-system.tsx +31 -66
  99. package/src/components/tools/__fixtures__/hosting.ts +10 -0
  100. package/src/components/tools/item/item-surface-placement.test.ts +3 -9
  101. package/src/components/tools/item/item-surface-strategy-resolver.test.ts +3 -9
  102. package/src/components/tools/item/placement-history.test.tsx +474 -0
  103. package/src/components/tools/item/placement-strategies.ts +63 -47
  104. package/src/components/tools/item/shelf-surface-strategy-resolver.test.ts +3 -3
  105. package/src/components/tools/item/use-draft-node.test.tsx +432 -1
  106. package/src/components/tools/item/use-draft-node.ts +207 -97
  107. package/src/components/tools/item/use-placement-coordinator.tsx +251 -111
  108. package/src/components/tools/paint-region/use-paint-tint.ts +37 -0
  109. package/src/components/tools/paint-region/wall-paint-region-tool.tsx +466 -0
  110. package/src/components/tools/registry/item-surface-move.test.ts +10 -2
  111. package/src/components/tools/registry/item-surface-move.ts +46 -24
  112. package/src/components/tools/registry/item-surface-resolver.test.ts +2 -2
  113. package/src/components/tools/registry/move-registry-node-tool.tsx +26 -10
  114. package/src/components/tools/select/box-select-tool.tsx +2 -12
  115. package/src/components/tools/select/plane-box-select-tool.test.tsx +79 -0
  116. package/src/components/tools/select/plane-box-select-tool.tsx +35 -22
  117. package/src/components/tools/select/select-candidates.test.ts +37 -4
  118. package/src/components/tools/select/select-candidates.ts +15 -11
  119. package/src/components/tools/shared/affordance-dispatch.test.ts +24 -0
  120. package/src/components/tools/shared/affordance-dispatch.ts +13 -0
  121. package/src/components/tools/shared/cursor-sphere.tsx +36 -12
  122. package/src/components/tools/shared/draft-measurement-label.tsx +38 -0
  123. package/src/components/tools/shared/pointer-support-cap.test.ts +27 -0
  124. package/src/components/tools/shared/pointer-support-cap.ts +17 -5
  125. package/src/components/tools/shared/surface-rejection.test.tsx +5 -4
  126. package/src/components/tools/shared/surface-rejection.tsx +14 -0
  127. package/src/components/tools/site/site-boundary-editor.tsx +1 -2
  128. package/src/components/tools/stair/stair-defaults.ts +0 -3
  129. package/src/components/tools/stair/stair-tool.tsx +90 -42
  130. package/src/components/tools/tool-manager.tsx +82 -23
  131. package/src/components/tools/wall/wall-drafting.test.ts +19 -4
  132. package/src/components/tools/zone/zone-tool.tsx +4 -12
  133. package/src/components/ui/action-menu/action-button.tsx +3 -0
  134. package/src/components/ui/action-menu/camera-actions.tsx +2 -1
  135. package/src/components/ui/action-menu/control-modes.tsx +2 -35
  136. package/src/components/ui/action-menu/measurement-control.tsx +3 -10
  137. package/src/components/ui/build-panel/build-panel.tsx +235 -0
  138. package/src/components/ui/command-palette/editor-commands.tsx +4 -30
  139. package/src/components/ui/command-palette/index.tsx +5 -24
  140. package/src/components/ui/controls/finish-swatch.tsx +110 -0
  141. package/src/components/ui/controls/material-paint-panel.tsx +101 -91
  142. package/src/components/ui/controls/material-picker.tsx +8 -67
  143. package/src/components/ui/controls/scene-material-list.tsx +4 -32
  144. package/src/components/ui/controls/select-control.tsx +65 -0
  145. package/src/components/ui/controls/slider-control.tsx +76 -31
  146. package/src/components/ui/floating-level-selector.tsx +9 -14
  147. package/src/components/ui/helpers/building-helper.tsx +4 -1
  148. package/src/components/ui/helpers/contextual-helper-panel.tsx +154 -70
  149. package/src/components/ui/helpers/helper-manager.test.tsx +69 -0
  150. package/src/components/ui/helpers/helper-manager.tsx +348 -19
  151. package/src/components/ui/helpers/item-helper.tsx +8 -0
  152. package/src/components/ui/helpers/registered-tool-helper.tsx +10 -33
  153. package/src/components/ui/icon-ref.tsx +45 -1
  154. package/src/components/ui/item-catalog/item-catalog.tsx +5 -30
  155. package/src/components/ui/level-duplicate-dialog.tsx +1 -1
  156. package/src/components/ui/node-icon-image.tsx +16 -0
  157. package/src/components/ui/panels/collections/collections-popover.tsx +20 -14
  158. package/src/components/ui/panels/floor-foundation-panel.tsx +551 -0
  159. package/src/components/ui/panels/floor-opening-panel.tsx +143 -0
  160. package/src/components/ui/panels/mobile-panel-sheet.tsx +2 -2
  161. package/src/components/ui/panels/mobile-selection-bar.tsx +2 -2
  162. package/src/components/ui/panels/multi-field-value.test.ts +26 -3
  163. package/src/components/ui/panels/multi-field-value.ts +6 -3
  164. package/src/components/ui/panels/multi-height-mode.tsx +27 -21
  165. package/src/components/ui/panels/multi-parametric-inspector.tsx +52 -2
  166. package/src/components/ui/panels/multi-selection-panel.tsx +43 -4
  167. package/src/components/ui/panels/node-display.ts +8 -3
  168. package/src/components/ui/panels/panel-manager.tsx +103 -68
  169. package/src/components/ui/panels/panel-wrapper.tsx +78 -36
  170. package/src/components/ui/panels/parametric-field-control.tsx +1 -1
  171. package/src/components/ui/panels/parametric-inspector.tsx +7 -19
  172. package/src/components/ui/panels/reference-panel.tsx +2 -3
  173. package/src/components/ui/panels/room-construction-rows.tsx +774 -0
  174. package/src/components/ui/panels/room-name-options.test.ts +55 -0
  175. package/src/components/ui/panels/room-name-options.ts +47 -0
  176. package/src/components/ui/panels/room-openings-field.tsx +110 -0
  177. package/src/components/ui/panels/room-panel.tsx +725 -0
  178. package/src/components/ui/panels/shape-choice.tsx +58 -0
  179. package/src/components/ui/panels/wall-region-list.tsx +109 -0
  180. package/src/components/ui/primitives/color-dot.tsx +36 -24
  181. package/src/components/ui/primitives/shortcut-token.tsx +4 -0
  182. package/src/components/ui/primitives/sidebar.tsx +2 -0
  183. package/src/components/ui/right-stack.test.ts +29 -0
  184. package/src/components/ui/right-stack.tsx +145 -0
  185. package/src/components/ui/sidebar/icon-rail.tsx +5 -0
  186. package/src/components/ui/sidebar/panels/settings-panel/audio-settings-dialog.tsx +2 -2
  187. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +615 -372
  188. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +7 -7
  189. package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +1 -1
  190. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -4
  191. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +1 -5
  192. package/src/components/ui/sidebar/panels/site-panel/collection-tree-node.tsx +197 -0
  193. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -5
  194. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +1 -5
  195. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +1 -5
  196. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -5
  197. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +24 -11
  198. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +1 -5
  199. package/src/components/ui/sidebar/panels/site-panel/index.tsx +176 -211
  200. package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +30 -8
  201. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +1 -3
  202. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +4 -1
  203. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +1 -3
  204. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +1 -4
  205. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +1 -3
  206. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +21 -13
  207. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +1 -5
  208. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -5
  209. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +1 -5
  210. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +0 -27
  211. package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +2 -3
  212. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -5
  213. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +1 -5
  214. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +9 -4
  215. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +0 -2
  216. package/src/components/ui/sidebar/tab-bar.tsx +10 -0
  217. package/src/components/ui/sidebar/use-plugin-panels.tsx +15 -5
  218. package/src/components/viewer/floorplan-compass-button.tsx +9 -1
  219. package/src/components/viewer/floorplan-preview-visibility.test.ts +35 -0
  220. package/src/components/viewer/floorplan-preview-visibility.ts +19 -0
  221. package/src/components/viewer/floorplan-preview.tsx +6 -18
  222. package/src/components/viewer/viewer-hover-label.tsx +34 -0
  223. package/src/components/viewer/viewer-rooms.tsx +175 -0
  224. package/src/components/viewer/viewer-scene-header.tsx +181 -145
  225. package/src/components/viewer/viewer-selection-manager.tsx +124 -0
  226. package/src/components/viewer/viewer-stage.tsx +2 -2
  227. package/src/components/viewer-zone-system.tsx +9 -9
  228. package/src/hooks/use-auto-save.integration.test.tsx +311 -0
  229. package/src/hooks/use-auto-save.ts +55 -14
  230. package/src/hooks/use-ceiling-events.test.ts +1 -0
  231. package/src/hooks/use-ceiling-events.ts +2 -0
  232. package/src/hooks/use-grid-events.test.ts +2 -0
  233. package/src/hooks/use-grid-events.ts +16 -11
  234. package/src/hooks/use-keyboard.test.ts +182 -0
  235. package/src/hooks/use-keyboard.ts +122 -91
  236. package/src/hooks/use-open-wall-ends.test.ts +99 -0
  237. package/src/hooks/use-open-wall-ends.ts +238 -0
  238. package/src/hooks/use-placement-notice.test.ts +48 -0
  239. package/src/hooks/use-placement-notice.ts +42 -0
  240. package/src/hooks/use-selected-room.ts +143 -0
  241. package/src/index.tsx +306 -39
  242. package/src/lib/catalog-panel-model.ts +39 -0
  243. package/src/lib/catalog-selection.test.ts +27 -0
  244. package/src/lib/catalog-selection.ts +8 -0
  245. package/src/lib/ceiling-edit-session.test.ts +321 -0
  246. package/src/lib/ceiling-edit-session.ts +206 -0
  247. package/src/lib/ceiling-region-session.test.ts +179 -0
  248. package/src/lib/contextual-help.test.ts +27 -1
  249. package/src/lib/contextual-help.ts +16 -9
  250. package/src/lib/continuation.test.ts +19 -1
  251. package/src/lib/continuation.ts +25 -4
  252. package/src/lib/door-interaction.ts +14 -0
  253. package/src/lib/editor-level-display.test.ts +96 -0
  254. package/src/lib/editor-level-display.ts +48 -0
  255. package/src/lib/editor-views.ts +144 -0
  256. package/src/lib/export-geometry-wait.test.ts +90 -0
  257. package/src/lib/floor-edit-session.ts +153 -0
  258. package/src/lib/floor-footprints.test.ts +563 -0
  259. package/src/lib/floor-footprints.ts +395 -0
  260. package/src/lib/floor-foundation-support.test.ts +283 -0
  261. package/src/lib/floor-opening-draft.ts +218 -0
  262. package/src/lib/floor-openings.test.ts +216 -0
  263. package/src/lib/floor-region-geometry.ts +191 -0
  264. package/src/lib/floor-region-session.test.ts +485 -0
  265. package/src/lib/floor-region-session.ts +589 -0
  266. package/src/lib/floor-region-snap.ts +101 -0
  267. package/src/lib/floor-step-hover.test.ts +32 -0
  268. package/src/lib/floorplan/annotation-visibility.ts +17 -0
  269. package/src/lib/floorplan/dimension-drive.test.ts +360 -0
  270. package/src/lib/floorplan/dimension-drive.ts +479 -0
  271. package/src/lib/floorplan/floorplan-export.test.ts +15 -9
  272. package/src/lib/floorplan/floorplan-export.tsx +32 -8
  273. package/src/lib/floorplan/floorplan-extension.ts +48 -0
  274. package/src/lib/floorplan/floorplan-mode.test.ts +4 -0
  275. package/src/lib/floorplan/floorplan-mode.ts +2 -0
  276. package/src/lib/floorplan/floorplan-pdfkit-document.ts +339 -18
  277. package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +138 -8
  278. package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +56 -25
  279. package/src/lib/floorplan/floorplan-plugin-install.test.tsx +138 -0
  280. package/src/lib/floorplan/floorplan-readonly.ts +6 -0
  281. package/src/lib/floorplan/geometry.ts +13 -6
  282. package/src/lib/floorplan/index.ts +0 -9
  283. package/src/lib/floorplan/items.ts +5 -2
  284. package/src/lib/floorplan/marks.test.ts +213 -0
  285. package/src/lib/floorplan/marks.ts +238 -0
  286. package/src/lib/floorplan/open-wall-ends.test.ts +268 -0
  287. package/src/lib/floorplan/open-wall-ends.ts +162 -0
  288. package/src/lib/floorplan/pdf-fonts/GeistMono-Bold.ttf +0 -0
  289. package/src/lib/floorplan/pdf-fonts/GeistMono-Regular.ttf +0 -0
  290. package/src/lib/floorplan/pdf-fonts/LICENSE-GeistMono.txt +92 -0
  291. package/src/lib/floorplan/pdf-fonts/LICENSE-LiberationSans.txt +102 -0
  292. package/src/lib/floorplan/pdf-fonts/LiberationSans-Bold.ttf +0 -0
  293. package/src/lib/floorplan/pdf-fonts/LiberationSans-Regular.ttf +0 -0
  294. package/src/lib/floorplan/schedules.test.ts +189 -0
  295. package/src/lib/floorplan/schedules.ts +339 -0
  296. package/src/lib/floorplan/sheet-export.test.ts +100 -0
  297. package/src/lib/floorplan/sheet-export.ts +318 -0
  298. package/src/lib/floorplan/site-plan/build-site-plan-drawing.ts +779 -0
  299. package/src/lib/floorplan/site-plan/contours.test.ts +43 -0
  300. package/src/lib/floorplan/site-plan/contributors.ts +97 -0
  301. package/src/lib/floorplan/site-plan/coverage.ts +232 -0
  302. package/src/lib/floorplan/site-plan/drawing-type-switch.test.ts +15 -0
  303. package/src/lib/floorplan/site-plan/drawing-type-switch.tsx +86 -0
  304. package/src/lib/floorplan/site-plan/front-edge.test.ts +103 -0
  305. package/src/lib/floorplan/site-plan/front-edge.ts +156 -0
  306. package/src/lib/floorplan/site-plan/geometry.test.ts +275 -0
  307. package/src/lib/floorplan/site-plan/geometry.ts +557 -0
  308. package/src/lib/floorplan/site-plan/index.ts +71 -0
  309. package/src/lib/floorplan/site-plan/site-annotations.test.ts +366 -0
  310. package/src/lib/floorplan/site-plan/site-annotations.ts +1083 -0
  311. package/src/lib/floorplan/site-plan/site-parts.ts +379 -0
  312. package/src/lib/floorplan/site-plan/site-plan-layer.tsx +138 -0
  313. package/src/lib/floorplan/site-plan/street-name.test.ts +69 -0
  314. package/src/lib/floorplan/site-plan/street-name.ts +302 -0
  315. package/src/lib/floorplan/site-plan/yards.test.ts +69 -0
  316. package/src/lib/floorplan/types.ts +1 -27
  317. package/src/lib/fresh-planar-placement.test.ts +36 -0
  318. package/src/lib/fresh-planar-placement.ts +191 -36
  319. package/src/lib/geometry-script/author.ts +152 -0
  320. package/src/lib/geometry-script/client.ts +152 -0
  321. package/src/lib/geometry-script/protocol.ts +12 -0
  322. package/src/lib/gesture-lifecycle.test.ts +595 -0
  323. package/src/lib/gesture-lifecycle.ts +290 -0
  324. package/src/lib/glb-export-zone-holes.test.ts +41 -0
  325. package/src/lib/glb-export.test.ts +544 -16
  326. package/src/lib/glb-export.ts +256 -125
  327. package/src/lib/grid-frame.test.ts +19 -0
  328. package/src/lib/grid-frame.ts +18 -0
  329. package/src/lib/history.test.ts +6 -0
  330. package/src/lib/history.ts +7 -0
  331. package/src/lib/hud-title.ts +109 -0
  332. package/src/lib/hydration-gated-storage.ts +49 -0
  333. package/src/lib/ifc-export.test.ts +174 -0
  334. package/src/lib/ifc-export.ts +201 -0
  335. package/src/lib/ifc-parts.ts +15 -0
  336. package/src/lib/inspector-expanded.test.ts +44 -0
  337. package/src/lib/inspector-expanded.ts +36 -0
  338. package/src/lib/interaction/scope.ts +27 -1
  339. package/src/lib/keyboard-pan.test.ts +184 -0
  340. package/src/lib/keyboard-pan.ts +238 -0
  341. package/src/lib/live-gesture-round-trip.test.ts +360 -0
  342. package/src/lib/lot/__fixtures__/map-dossier-miami-shores.json +1 -0
  343. package/src/lib/lot/__fixtures__/map-dossier-stpete.json +1 -0
  344. package/src/lib/lot/clean-ring.test.ts +186 -0
  345. package/src/lib/lot/clean-ring.ts +217 -0
  346. package/src/lib/lot/dossier.test.ts +202 -0
  347. package/src/lib/lot/dossier.ts +498 -0
  348. package/src/lib/lot/drop-in.test.ts +326 -0
  349. package/src/lib/lot/drop-in.ts +407 -0
  350. package/src/lib/lot/index.ts +43 -0
  351. package/src/lib/lot/lot-patch.test.ts +340 -0
  352. package/src/lib/lot/lot-patch.ts +394 -0
  353. package/src/lib/lot/parcel-provider.ts +43 -0
  354. package/src/lib/lot/terrain.test.ts +161 -0
  355. package/src/lib/lot/terrain.ts +266 -0
  356. package/src/lib/material-catalog-model.ts +74 -0
  357. package/src/lib/material-paint-panel-model.ts +80 -0
  358. package/src/lib/material-paint.ts +4 -0
  359. package/src/lib/mezzanine-actions.test.ts +342 -0
  360. package/src/lib/mezzanine-draft.test.ts +501 -0
  361. package/src/lib/mezzanine-draft.ts +278 -0
  362. package/src/lib/mezzanine-messages.ts +26 -0
  363. package/src/lib/mezzanine-selection.test.ts +207 -0
  364. package/src/lib/mezzanine-stairs.ts +46 -0
  365. package/src/lib/model-export.ts +3 -1
  366. package/src/lib/node-action-movement.ts +17 -6
  367. package/src/lib/node-display-label.ts +37 -0
  368. package/src/lib/orbit-floor-clearance.test.ts +165 -0
  369. package/src/lib/orbit-floor-clearance.ts +106 -0
  370. package/src/lib/overlay-transform.test.ts +28 -0
  371. package/src/lib/overlay-transform.ts +12 -0
  372. package/src/lib/paint-commit-route.test.ts +37 -0
  373. package/src/lib/paint-commit-route.ts +17 -0
  374. package/src/lib/paint-eyedropper.test.tsx +165 -0
  375. package/src/lib/paint-eyedropper.ts +99 -0
  376. package/src/lib/paint-pass-through.test.ts +111 -0
  377. package/src/lib/paint-pass-through.ts +76 -0
  378. package/src/lib/paint-region-hover.ts +25 -0
  379. package/src/lib/paint-region-mode.ts +154 -0
  380. package/src/lib/paint-regions.test.ts +166 -0
  381. package/src/lib/paint-regions.ts +192 -0
  382. package/src/lib/paint-scope.test.ts +317 -35
  383. package/src/lib/paint-scope.ts +266 -25
  384. package/src/lib/panel-rows.ts +96 -0
  385. package/src/lib/panel-tool-options.test.ts +34 -0
  386. package/src/lib/panel-tool-options.ts +46 -0
  387. package/src/lib/parametric-node-update.ts +37 -0
  388. package/src/lib/plate-paint-affected.test.ts +300 -0
  389. package/src/lib/plate-paint-affected.ts +177 -0
  390. package/src/lib/plugin-chat.ts +50 -0
  391. package/src/lib/plugin-panels.test.ts +25 -0
  392. package/src/lib/plugin-panels.ts +94 -0
  393. package/src/lib/print-shell-compiler.ts +1 -0
  394. package/src/lib/registered-draft-snap.ts +12 -0
  395. package/src/lib/roof-duplication.ts +10 -2
  396. package/src/lib/room-assembly-overlay.test.ts +275 -0
  397. package/src/lib/room-assembly-overlay.ts +266 -0
  398. package/src/lib/room-built-on.test.ts +379 -0
  399. package/src/lib/room-built-on.ts +138 -0
  400. package/src/lib/room-construction-commands.ts +712 -0
  401. package/src/lib/room-construction.test.ts +746 -0
  402. package/src/lib/room-construction.ts +385 -0
  403. package/src/lib/room-controls-phase6.test.ts +630 -0
  404. package/src/lib/room-divide-session.ts +523 -0
  405. package/src/lib/room-handle-drag.ts +1068 -0
  406. package/src/lib/room-name-catalog.test.ts +90 -0
  407. package/src/lib/room-name-catalog.ts +168 -0
  408. package/src/lib/room-push-dimensions.test.ts +311 -0
  409. package/src/lib/room-push-dimensions.ts +251 -0
  410. package/src/lib/room-selection-commands.ts +62 -0
  411. package/src/lib/room-selection.test.ts +973 -0
  412. package/src/lib/room-selection.ts +417 -0
  413. package/src/lib/room-structure-commands.ts +94 -0
  414. package/src/lib/room-structure.test.ts +921 -0
  415. package/src/lib/room-transform-session.ts +570 -0
  416. package/src/lib/room-zone-routing.test.ts +294 -0
  417. package/src/lib/room-zone-routing.ts +197 -0
  418. package/src/lib/scene-clipboard.test.ts +364 -0
  419. package/src/lib/scene-clipboard.ts +229 -48
  420. package/src/lib/scene-material-refs.test.ts +79 -0
  421. package/src/lib/scene-material-refs.ts +79 -0
  422. package/src/lib/scene.test.ts +87 -0
  423. package/src/lib/scene.ts +39 -45
  424. package/src/lib/selection-routing.test.ts +129 -45
  425. package/src/lib/selection-routing.ts +53 -74
  426. package/src/lib/session-writes.test.ts +120 -0
  427. package/src/lib/session-writes.ts +205 -0
  428. package/src/lib/sidebar-panel.ts +62 -0
  429. package/src/lib/site-boundary.test.ts +4 -6
  430. package/src/lib/site-boundary.ts +1 -1
  431. package/src/lib/snapping-mode.test.ts +17 -2
  432. package/src/lib/snapping-mode.ts +19 -7
  433. package/src/lib/spatial-drag-plane.test.ts +53 -0
  434. package/src/lib/spatial-drag-plane.ts +19 -0
  435. package/src/lib/spatial-pointer-input.test.ts +72 -0
  436. package/src/lib/spatial-pointer-input.ts +62 -0
  437. package/src/lib/spatial-snap.test.ts +47 -0
  438. package/src/lib/terrace-draft.test.ts +145 -0
  439. package/src/lib/terrace-draft.ts +213 -0
  440. package/src/lib/terrain-panel-model.ts +117 -0
  441. package/src/lib/tool-cursor-icon.test.ts +16 -0
  442. package/src/lib/tool-cursor-icon.ts +11 -0
  443. package/src/lib/typing-target.ts +9 -0
  444. package/src/lib/units.ts +77 -39
  445. package/src/lib/vector-edges.test.ts +58 -0
  446. package/src/lib/vector-edges.ts +400 -0
  447. package/src/lib/viewer-selection.test.ts +577 -0
  448. package/src/lib/viewer-selection.ts +569 -0
  449. package/src/lib/wall-draw-variant.test.ts +71 -0
  450. package/src/lib/wall-draw-variant.ts +91 -0
  451. package/src/lib/wall-paint-region-session.ts +246 -0
  452. package/src/lib/wall-paint-region.test.ts +373 -0
  453. package/src/lib/wall-polygon-draft.test.ts +262 -0
  454. package/src/lib/wall-polygon-draft.ts +286 -0
  455. package/src/lib/wall-push.test.ts +453 -0
  456. package/src/lib/wall-region-face.ts +352 -0
  457. package/src/lib/wall-region-handles.test.ts +304 -0
  458. package/src/lib/wall-region-handles.ts +368 -0
  459. package/src/lib/wall-region-list.test.ts +119 -0
  460. package/src/lib/wall-region-list.ts +136 -0
  461. package/src/lib/wall-region-snap.ts +110 -0
  462. package/src/lib/window-interaction.ts +14 -0
  463. package/src/lib/world-grid-snap.test.ts +48 -0
  464. package/src/lib/world-grid-snap.ts +9 -5
  465. package/src/lib/zone-content.ts +4 -1
  466. package/src/lib/zone-name.test.ts +17 -0
  467. package/src/lib/zone-name.ts +17 -0
  468. package/src/store/opens-in-select-mode.test.ts +223 -0
  469. package/src/store/paint-mode.test.ts +188 -0
  470. package/src/store/rotation-snapping.test.ts +53 -0
  471. package/src/store/terrain-sculpt-mode.test.ts +19 -21
  472. package/src/store/tool-mode.test.ts +137 -6
  473. package/src/store/use-delete-confirmation.ts +28 -1
  474. package/src/store/use-drawing-view.test.ts +24 -3
  475. package/src/store/use-drawing-view.ts +29 -1
  476. package/src/store/use-editor.tsx +442 -323
  477. package/src/store/use-floorplan-draft-preview.ts +23 -1
  478. package/src/store/use-floorplan-marquee.ts +12 -2
  479. package/src/store/use-interaction-scope.ts +155 -6
  480. package/src/store/use-quick-measurement-hud.ts +3 -3
  481. package/src/store/use-stair-build-preview.ts +13 -5
  482. package/src/store/use-wall-move-ghosts.ts +4 -2
  483. package/src/store/use-wall-region-handles-selection.test.ts +222 -0
  484. package/src/store/use-wall-region-handles-selection.ts +139 -0
  485. package/src/store/view-layout.test.ts +175 -0
  486. package/src/store/view-layout.ts +196 -0
  487. package/src/test-utils/immediate-animation-frames.ts +16 -0
  488. package/src/test-utils/live-gesture-round-trip.ts +224 -0
  489. package/src/test-utils/selection-harness.ts +82 -0
  490. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +0 -499
  491. package/src/components/systems/zone/zone-label-editor-system.tsx +0 -333
  492. package/src/components/tools/fence/fence-drafting.test.ts +0 -100
  493. package/src/components/tools/fence/fence-drafting.ts +0 -266
  494. package/src/components/ui/sidebar/panels/plugins-panel.tsx +0 -218
  495. package/src/components/viewer/viewer-units-panel.tsx +0 -116
  496. package/src/lib/floorplan/stairs.ts +0 -489
  497. package/src/lib/level-duplication.test.ts +0 -144
  498. package/src/lib/level-duplication.ts +0 -195
@@ -0,0 +1,213 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+ import type { AnyNode, AnyNodeId } from '@pascal-app/core'
3
+ import { persistResolvedMarks, resolveMarkDetail, resolveMarks } from './marks'
4
+
5
+ /**
6
+ * Fixture: a rectangular level, walls running clockwise on screen from the
7
+ * north-west corner.
8
+ *
9
+ * north [0,0] → [6,0] doors d_n1 @1, d_n2 @4
10
+ * east [6,0] → [6,4] window w_e1 @2
11
+ * south [6,4] → [0,4] door d_s1 @2
12
+ * west [0,4] → [0,0] (interior partition, no openings)
13
+ */
14
+ function fixture(levelOrdinal = 0): Record<string, AnyNode> {
15
+ const nodes: Record<string, AnyNode> = {}
16
+ const node = (value: Record<string, unknown>) => {
17
+ nodes[value.id as string] = {
18
+ object: 'node',
19
+ parentId: null,
20
+ visible: true,
21
+ metadata: {},
22
+ children: [],
23
+ ...value,
24
+ } as unknown as AnyNode
25
+ }
26
+
27
+ node({
28
+ id: 'level_1',
29
+ type: 'level',
30
+ level: levelOrdinal,
31
+ children: ['w_n', 'w_e', 'w_s', 'w_w'],
32
+ })
33
+ node({
34
+ id: 'w_n',
35
+ type: 'wall',
36
+ parentId: 'level_1',
37
+ start: [0, 0],
38
+ end: [6, 0],
39
+ thickness: 0.15,
40
+ frontSide: 'exterior',
41
+ backSide: 'interior',
42
+ children: ['d_n1', 'd_n2'],
43
+ })
44
+ node({
45
+ id: 'w_e',
46
+ type: 'wall',
47
+ parentId: 'level_1',
48
+ start: [6, 0],
49
+ end: [6, 4],
50
+ thickness: 0.15,
51
+ frontSide: 'exterior',
52
+ backSide: 'interior',
53
+ children: ['win_e1'],
54
+ })
55
+ node({
56
+ id: 'w_s',
57
+ type: 'wall',
58
+ parentId: 'level_1',
59
+ start: [6, 4],
60
+ end: [0, 4],
61
+ thickness: 0.15,
62
+ frontSide: 'exterior',
63
+ backSide: 'interior',
64
+ children: ['d_s1'],
65
+ })
66
+ node({
67
+ id: 'w_w',
68
+ type: 'wall',
69
+ parentId: 'level_1',
70
+ start: [0, 4],
71
+ end: [0, 0],
72
+ thickness: 0.1,
73
+ frontSide: 'interior',
74
+ backSide: 'interior',
75
+ children: [],
76
+ })
77
+
78
+ const door = (id: string, wallId: string, along: number) =>
79
+ node({
80
+ id,
81
+ type: 'door',
82
+ parentId: wallId,
83
+ wallId,
84
+ width: 0.9,
85
+ height: 2.1,
86
+ position: [along, 1.05, 0],
87
+ })
88
+ door('d_n1', 'w_n', 1)
89
+ door('d_n2', 'w_n', 4)
90
+ door('d_s1', 'w_s', 2)
91
+ node({
92
+ id: 'win_e1',
93
+ type: 'window',
94
+ parentId: 'w_e',
95
+ wallId: 'w_e',
96
+ width: 1.2,
97
+ height: 1.2,
98
+ position: [2, 1.1, 0],
99
+ })
100
+ return nodes
101
+ }
102
+
103
+ describe('resolveMarks', () => {
104
+ test('doors are D101…, windows W101… on level 0', () => {
105
+ const marks = resolveMarks(fixture(), 'level_1' as AnyNodeId)
106
+ expect(marks.get('d_n1')).toBe('D101')
107
+ expect(marks.get('d_n2')).toBe('D102')
108
+ expect(marks.get('d_s1')).toBe('D103')
109
+ expect(marks.get('win_e1')).toBe('W101')
110
+ })
111
+
112
+ test('level ordinal shifts the base by 100', () => {
113
+ const marks = resolveMarks(fixture(1), 'level_1' as AnyNodeId)
114
+ expect(marks.get('d_n1')).toBe('D201')
115
+ expect(marks.get('win_e1')).toBe('W201')
116
+ })
117
+
118
+ test('openings are ordered clockwise from the north-west, exterior walls first', () => {
119
+ const nodes = fixture()
120
+ // Move the west partition's opening onto it to prove interior comes last.
121
+ ;(nodes.w_w as unknown as { children: string[] }).children = ['d_w1']
122
+ nodes.d_w1 = {
123
+ object: 'node',
124
+ id: 'd_w1',
125
+ type: 'door',
126
+ parentId: 'w_w',
127
+ wallId: 'w_w',
128
+ visible: true,
129
+ metadata: {},
130
+ width: 0.8,
131
+ height: 2.1,
132
+ position: [2, 1.05, 0],
133
+ children: [],
134
+ } as unknown as AnyNode
135
+ const marks = resolveMarks(nodes, 'level_1' as AnyNodeId)
136
+ // North (exterior) first, then the other exterior walls, interior last.
137
+ expect(marks.get('d_w1')).toBe('D104')
138
+ })
139
+
140
+ test('an explicit mark wins and is never reassigned', () => {
141
+ const nodes = fixture()
142
+ ;(nodes.d_n2 as unknown as { mark: string }).mark = 'D-GARAGE'
143
+ const marks = resolveMarks(nodes, 'level_1' as AnyNodeId)
144
+ expect(marks.get('d_n2')).toBe('D-GARAGE')
145
+ expect(marks.get('d_n1')).toBe('D101')
146
+ expect(marks.get('d_s1')).toBe('D102')
147
+ })
148
+
149
+ test('duplicate explicit marks are reported, not silently renumbered', () => {
150
+ const nodes = fixture()
151
+ ;(nodes.d_n1 as unknown as { mark: string }).mark = 'D101'
152
+ ;(nodes.d_n2 as unknown as { mark: string }).mark = 'D101'
153
+ const resolution = resolveMarkDetail(nodes, 'level_1' as AnyNodeId)
154
+ expect(resolution.issues).toEqual(['Duplicate door mark D101 (2 instances)'])
155
+ })
156
+
157
+ test('numbering is stable under insertion once persisted', () => {
158
+ const nodes = fixture()
159
+ const first = resolveMarkDetail(nodes, 'level_1' as AnyNodeId)
160
+ expect([...first.assignments.values()]).toEqual(['D101', 'D102', 'D103', 'W101'])
161
+
162
+ // Persist, the way an edit session does the first time it resolves.
163
+ const written = persistResolvedMarks(first, (id, data) => {
164
+ ;(nodes[id] as unknown as Record<string, unknown>).mark = data.mark
165
+ })
166
+ expect(written).toBe(4)
167
+
168
+ // Insert a door BETWEEN d_n1 and d_n2 on the same wall.
169
+ ;(nodes.w_n as unknown as { children: string[] }).children = ['d_n1', 'd_new', 'd_n2']
170
+ nodes.d_new = {
171
+ object: 'node',
172
+ id: 'd_new',
173
+ type: 'door',
174
+ parentId: 'w_n',
175
+ wallId: 'w_n',
176
+ visible: true,
177
+ metadata: {},
178
+ width: 0.8,
179
+ height: 2.1,
180
+ position: [2.5, 1.05, 0],
181
+ children: [],
182
+ } as unknown as AnyNode
183
+
184
+ const second = resolveMarkDetail(nodes, 'level_1' as AnyNodeId)
185
+ // Existing doors keep their marks; the new one takes the next free number.
186
+ expect(second.marks.get('d_n1')).toBe('D101')
187
+ expect(second.marks.get('d_n2')).toBe('D102')
188
+ expect(second.marks.get('d_s1')).toBe('D103')
189
+ expect(second.marks.get('d_new')).toBe('D104')
190
+ expect([...second.assignments.keys()]).toEqual(['d_new'])
191
+ })
192
+
193
+ test('WITHOUT persisting, inserting mid-wall renumbers — which is why we persist', () => {
194
+ const nodes = fixture()
195
+ ;(nodes.w_n as unknown as { children: string[] }).children = ['d_n1', 'd_new', 'd_n2']
196
+ nodes.d_new = {
197
+ object: 'node',
198
+ id: 'd_new',
199
+ type: 'door',
200
+ parentId: 'w_n',
201
+ wallId: 'w_n',
202
+ visible: true,
203
+ metadata: {},
204
+ width: 0.8,
205
+ height: 2.1,
206
+ position: [2.5, 1.05, 0],
207
+ children: [],
208
+ } as unknown as AnyNode
209
+ const marks = resolveMarks(nodes, 'level_1' as AnyNodeId)
210
+ expect(marks.get('d_new')).toBe('D102')
211
+ expect(marks.get('d_n2')).toBe('D103')
212
+ })
213
+ })
@@ -0,0 +1,238 @@
1
+ import type {
2
+ AnyNode,
3
+ AnyNodeId,
4
+ DoorNode,
5
+ LevelNode,
6
+ WallNode,
7
+ WindowNode,
8
+ } from '@pascal-app/core'
9
+
10
+ /**
11
+ * Deterministic door / window marks per level — WS3.
12
+ *
13
+ * `packages/core/src/schema/nodes/door.ts:75-79` promises "the deterministic
14
+ * level fallback (101, 102, ...)"; this is the implementation.
15
+ *
16
+ * Numbering
17
+ * ---------
18
+ * base = (level.level + 1) * 100 — level 0 → 100, level 1 → 200
19
+ * doors = `D${base + n}` → D101, D102 …
20
+ * windows= `W${base + n}` → W101, W102 …
21
+ *
22
+ * Order
23
+ * -----
24
+ * Openings are visited **clockwise** around the level, starting from the
25
+ * north-west-most exterior wall, exterior walls first and interior walls
26
+ * after. Clockwise is the ascending-angle sweep of the wall midpoint about
27
+ * the level centroid, anchored on the north-west diagonal (plan axes are
28
+ * x east, z south — z grows down the screen — so a screen-clockwise sweep
29
+ * is increasing atan2(z, x)). Openings on the same wall are ordered along
30
+ * the wall direction.
31
+ *
32
+ * Stability
33
+ * ---------
34
+ * An explicit `mark` on the node always wins and is never reassigned.
35
+ * `resolveMarks` also reports, in `assignments`, the marks it invented for
36
+ * nodes that had none — the caller is expected to PERSIST those back onto
37
+ * `node.mark` the first time they are resolved in an edit session
38
+ * (`persistResolvedMarks`). That is what makes numbering stable under
39
+ * insertion: once written, an existing opening keeps its number forever and
40
+ * a newly inserted opening only ever takes the next free number. Without
41
+ * the write-back, inserting a door in the middle of a wall would silently
42
+ * renumber every door after it.
43
+ */
44
+
45
+ export type OpeningMarkKind = 'door' | 'window'
46
+
47
+ export type MarkResolution = {
48
+ /** Every door and window on the level → its mark. */
49
+ marks: ReadonlyMap<string, string>
50
+ /** Only the marks that were invented (node.mark was empty). */
51
+ assignments: ReadonlyMap<string, string>
52
+ /** Duplicate explicit marks and other reportable defects. */
53
+ issues: readonly string[]
54
+ }
55
+
56
+ export type MarkSceneInput = {
57
+ nodes: Readonly<Record<string, AnyNode>>
58
+ }
59
+
60
+ /**
61
+ * Resolve marks for every door and window under `levelId`.
62
+ *
63
+ * `scene` may be a `{ nodes }` snapshot or the raw node map.
64
+ */
65
+ export function resolveMarks(
66
+ scene: MarkSceneInput | Readonly<Record<string, AnyNode>>,
67
+ levelId: AnyNodeId,
68
+ ): Map<string, string> {
69
+ return new Map(resolveMarkDetail(scene, levelId).marks)
70
+ }
71
+
72
+ export function resolveMarkDetail(
73
+ scene: MarkSceneInput | Readonly<Record<string, AnyNode>>,
74
+ levelId: AnyNodeId,
75
+ ): MarkResolution {
76
+ const nodes = normalizeNodes(scene)
77
+ const level = nodes[levelId]
78
+ const ordinal = level && level.type === 'level' ? ((level as LevelNode).level ?? 0) : 0
79
+ const base = (Math.max(0, ordinal) + 1) * 100
80
+
81
+ const openings = orderedOpenings(nodes, levelId)
82
+ const marks = new Map<string, string>()
83
+ const assignments = new Map<string, string>()
84
+ const issues: string[] = []
85
+
86
+ for (const kind of ['door', 'window'] as const) {
87
+ const prefix = kind === 'door' ? 'D' : 'W'
88
+ const list = openings.filter((opening) => opening.type === kind)
89
+ const used = new Set<string>()
90
+ const explicitOwners = new Map<string, string[]>()
91
+
92
+ for (const opening of list) {
93
+ const explicit = opening.mark?.trim()
94
+ if (!explicit) continue
95
+ marks.set(opening.id, explicit)
96
+ const normalized = explicit.toLocaleUpperCase()
97
+ used.add(normalized)
98
+ const owners = explicitOwners.get(normalized)
99
+ if (owners) owners.push(opening.id)
100
+ else explicitOwners.set(normalized, [opening.id])
101
+ }
102
+
103
+ let sequence = 1
104
+ for (const opening of list) {
105
+ if (marks.has(opening.id)) continue
106
+ let candidate = `${prefix}${base + sequence}`
107
+ while (used.has(candidate.toLocaleUpperCase())) {
108
+ sequence++
109
+ candidate = `${prefix}${base + sequence}`
110
+ }
111
+ marks.set(opening.id, candidate)
112
+ assignments.set(opening.id, candidate)
113
+ used.add(candidate.toLocaleUpperCase())
114
+ sequence++
115
+ }
116
+
117
+ for (const [mark, owners] of explicitOwners) {
118
+ if (owners.length > 1) {
119
+ issues.push(`Duplicate ${kind} mark ${mark} (${owners.length} instances)`)
120
+ }
121
+ }
122
+ }
123
+
124
+ return { marks, assignments, issues }
125
+ }
126
+
127
+ /**
128
+ * Write invented marks back onto the nodes so numbering never drifts.
129
+ *
130
+ * Call once per level per edit session (the caller decides when — this
131
+ * module stays pure). `update` is typically `sceneApi.update`.
132
+ */
133
+ export function persistResolvedMarks(
134
+ resolution: MarkResolution,
135
+ update: (id: AnyNodeId, data: Record<string, unknown>) => void,
136
+ ): number {
137
+ let written = 0
138
+ for (const [id, mark] of resolution.assignments) {
139
+ update(id as AnyNodeId, { mark })
140
+ written++
141
+ }
142
+ return written
143
+ }
144
+
145
+ // ── Ordering ─────────────────────────────────────────────────────────
146
+
147
+ type Opening = (DoorNode | WindowNode) & { type: 'door' | 'window' }
148
+
149
+ /**
150
+ * Every door and window under the level, clockwise from the NW-most
151
+ * exterior wall, exterior walls first.
152
+ */
153
+ export function orderedOpenings(
154
+ nodes: Readonly<Record<string, AnyNode>>,
155
+ levelId: AnyNodeId,
156
+ ): Opening[] {
157
+ const walls: WallNode[] = []
158
+ const orphanOpenings: Opening[] = []
159
+
160
+ const visit = (id: string) => {
161
+ const node = nodes[id]
162
+ if (!node) return
163
+ if (node.visible === false) return
164
+ if (node.type === 'wall') walls.push(node as WallNode)
165
+ else if (node.type === 'door' || node.type === 'window') {
166
+ const opening = node as Opening
167
+ if (!opening.wallId || !nodes[opening.wallId]) orphanOpenings.push(opening)
168
+ }
169
+ for (const childId of (node as { children?: string[] }).children ?? []) visit(childId)
170
+ }
171
+ visit(levelId)
172
+
173
+ const centroid = wallCentroid(walls)
174
+ const ordered = [...walls].sort((left, right) => {
175
+ const leftExterior = isExteriorWall(left) ? 0 : 1
176
+ const rightExterior = isExteriorWall(right) ? 0 : 1
177
+ if (leftExterior !== rightExterior) return leftExterior - rightExterior
178
+ return clockwiseKey(left, centroid) - clockwiseKey(right, centroid)
179
+ })
180
+
181
+ const result: Opening[] = []
182
+ for (const wall of ordered) {
183
+ const wallOpenings: Opening[] = []
184
+ for (const childId of wall.children ?? []) {
185
+ const child = nodes[childId]
186
+ if (child && child.visible !== false && (child.type === 'door' || child.type === 'window')) {
187
+ wallOpenings.push(child as Opening)
188
+ }
189
+ }
190
+ wallOpenings.sort((left, right) => left.position[0] - right.position[0])
191
+ result.push(...wallOpenings)
192
+ }
193
+ result.push(...orphanOpenings)
194
+ return result
195
+ }
196
+
197
+ function wallCentroid(walls: readonly WallNode[]): [number, number] {
198
+ if (walls.length === 0) return [0, 0]
199
+ let x = 0
200
+ let z = 0
201
+ for (const wall of walls) {
202
+ x += (wall.start[0] + wall.end[0]) / 2
203
+ z += (wall.start[1] + wall.end[1]) / 2
204
+ }
205
+ return [x / walls.length, z / walls.length]
206
+ }
207
+
208
+ /**
209
+ * Sweep key: 0 at the north-west direction, increasing clockwise on screen.
210
+ *
211
+ * Plan axes are x east, z south — z grows DOWN the screen — so a clockwise
212
+ * screen sweep (up → right → down → left) is the INCREASING atan2(z, x)
213
+ * direction. The north-west diagonal sits at atan2(-1, -1) = -3π/4, so
214
+ * anchoring there and wrapping into [0, 2π) yields north, east, south, west.
215
+ */
216
+ const NORTH_WEST_ANGLE = (-3 * Math.PI) / 4
217
+
218
+ function clockwiseKey(wall: WallNode, centroid: readonly [number, number]): number {
219
+ const midX = (wall.start[0] + wall.end[0]) / 2 - centroid[0]
220
+ const midZ = (wall.start[1] + wall.end[1]) / 2 - centroid[1]
221
+ let sweep = Math.atan2(midZ, midX) - NORTH_WEST_ANGLE
222
+ while (sweep < 0) sweep += Math.PI * 2
223
+ while (sweep >= Math.PI * 2) sweep -= Math.PI * 2
224
+ return sweep
225
+ }
226
+
227
+ function isExteriorWall(wall: WallNode): boolean {
228
+ return wall.frontSide === 'exterior' || wall.backSide === 'exterior'
229
+ }
230
+
231
+ function normalizeNodes(
232
+ scene: MarkSceneInput | Readonly<Record<string, AnyNode>>,
233
+ ): Readonly<Record<string, AnyNode>> {
234
+ const candidate = scene as MarkSceneInput
235
+ return candidate && typeof candidate === 'object' && 'nodes' in candidate
236
+ ? candidate.nodes
237
+ : (scene as Readonly<Record<string, AnyNode>>)
238
+ }