@pascal-app/editor 1.0.3 → 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 (440) hide show
  1. package/package.json +11 -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 +202 -57
  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 +36 -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 +344 -342
  27. package/src/components/editor/footprint-height-handle.tsx +204 -0
  28. package/src/components/editor/group-actions.ts +54 -2
  29. package/src/components/editor/handles/handle-hit-priority.test.tsx +63 -0
  30. package/src/components/editor/handles/handle-hit-priority.tsx +37 -0
  31. package/src/components/editor/handles/swallow-next-click.test.ts +28 -0
  32. package/src/components/editor/handles/use-handle-drag.ts +269 -197
  33. package/src/components/editor/index.tsx +294 -169
  34. package/src/components/editor/join-walls-pill.tsx +44 -0
  35. package/src/components/editor/node-action-menu.tsx +68 -70
  36. package/src/components/editor/node-arrow-handles.tsx +25 -6
  37. package/src/components/editor/open-wall-ends-3d-layer.tsx +273 -0
  38. package/src/components/editor/open-wall-ends-hint.tsx +89 -0
  39. package/src/components/editor/quick-measurement-hud.tsx +3 -2
  40. package/src/components/editor/registry-action-contributions.test.tsx +63 -0
  41. package/src/components/editor/registry-action-contributions.tsx +80 -1
  42. package/src/components/editor/room-controls.tsx +847 -0
  43. package/src/components/editor/room-floor-highlight.tsx +128 -0
  44. package/src/components/editor/room-handles-2d.tsx +354 -0
  45. package/src/components/editor/room-handles.tsx +675 -0
  46. package/src/components/editor/room-highlight.tsx +225 -0
  47. package/src/components/editor/room-labels.test.ts +357 -0
  48. package/src/components/editor/room-labels.tsx +990 -0
  49. package/src/components/editor/room-transform-ghost.tsx +256 -0
  50. package/src/components/editor/room-webgpu-materials.test.ts +27 -0
  51. package/src/components/editor/selection-manager.tsx +451 -317
  52. package/src/components/editor/slab-hole-highlights.tsx +3 -0
  53. package/src/components/editor/snapshot-capture-overlay.tsx +4 -0
  54. package/src/components/editor/snapshot-capture.test.ts +52 -4
  55. package/src/components/editor/snapshot-capture.ts +30 -5
  56. package/src/components/editor/thumbnail-generator.test.ts +308 -0
  57. package/src/components/editor/thumbnail-generator.tsx +1035 -229
  58. package/src/components/editor/use-floorplan-background-placement.ts +31 -11
  59. package/src/components/editor/vector-edge-extractor.tsx +121 -0
  60. package/src/components/editor/views/host-view.tsx +61 -0
  61. package/src/components/editor/views/plugins-view.tsx +670 -0
  62. package/src/components/editor/views/scene-region.tsx +59 -0
  63. package/src/components/editor/views/use-editor-views.ts +57 -0
  64. package/src/components/editor/views/view-bar.tsx +220 -0
  65. package/src/components/editor/wall-justification.test.ts +140 -0
  66. package/src/components/editor/wall-move-side-handles.tsx +43 -142
  67. package/src/components/editor/wall-push-arrow.tsx +92 -0
  68. package/src/components/editor/wall-region-handles.tsx +509 -0
  69. package/src/components/editor/wall-snap-beacon-layer.tsx +3 -0
  70. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +8 -4
  71. package/src/components/editor-2d/floorplan-open-wall-ends-layer.test.tsx +86 -0
  72. package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +290 -0
  73. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +11 -1
  74. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +113 -102
  75. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.test.ts +156 -0
  76. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +278 -0
  77. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +77 -0
  78. package/src/components/editor-2d/renderers/{floorplan-draft-wall-measurement.tsx → floorplan-draft-measurement.tsx} +8 -8
  79. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +4 -1
  80. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +38 -8
  81. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +291 -227
  82. package/src/components/editor-2d/svg-paths.ts +12 -15
  83. package/src/components/procedural-items/inspector.tsx +40 -15
  84. package/src/components/systems/ceiling/ceiling-selection-affordance-system.test.ts +97 -14
  85. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +18 -18
  86. package/src/components/systems/zone/zone-system.test.ts +47 -0
  87. package/src/components/systems/zone/zone-system.tsx +29 -68
  88. package/src/components/tools/item/placement-history.test.tsx +474 -0
  89. package/src/components/tools/item/placement-strategies.ts +8 -4
  90. package/src/components/tools/item/use-draft-node.test.tsx +432 -1
  91. package/src/components/tools/item/use-draft-node.ts +166 -95
  92. package/src/components/tools/item/use-placement-coordinator.tsx +93 -46
  93. package/src/components/tools/paint-region/use-paint-tint.ts +37 -0
  94. package/src/components/tools/paint-region/wall-paint-region-tool.tsx +466 -0
  95. package/src/components/tools/registry/move-registry-node-tool.tsx +3 -1
  96. package/src/components/tools/select/box-select-tool.tsx +2 -12
  97. package/src/components/tools/select/plane-box-select-tool.test.tsx +79 -0
  98. package/src/components/tools/select/plane-box-select-tool.tsx +35 -22
  99. package/src/components/tools/select/select-candidates.test.ts +37 -4
  100. package/src/components/tools/select/select-candidates.ts +15 -11
  101. package/src/components/tools/shared/cursor-sphere.tsx +36 -12
  102. package/src/components/tools/site/site-boundary-editor.tsx +1 -2
  103. package/src/components/tools/stair/stair-defaults.ts +0 -3
  104. package/src/components/tools/stair/stair-tool.tsx +90 -42
  105. package/src/components/tools/tool-manager.tsx +46 -20
  106. package/src/components/tools/wall/wall-drafting.test.ts +19 -4
  107. package/src/components/tools/zone/zone-tool.tsx +4 -12
  108. package/src/components/ui/action-menu/action-button.tsx +3 -0
  109. package/src/components/ui/action-menu/camera-actions.tsx +2 -1
  110. package/src/components/ui/action-menu/control-modes.tsx +2 -35
  111. package/src/components/ui/action-menu/measurement-control.tsx +3 -10
  112. package/src/components/ui/build-panel/build-panel.tsx +235 -0
  113. package/src/components/ui/command-palette/editor-commands.tsx +4 -30
  114. package/src/components/ui/command-palette/index.tsx +5 -24
  115. package/src/components/ui/controls/finish-swatch.tsx +110 -0
  116. package/src/components/ui/controls/material-paint-panel.tsx +97 -26
  117. package/src/components/ui/controls/material-picker.tsx +4 -1
  118. package/src/components/ui/controls/scene-material-list.tsx +4 -32
  119. package/src/components/ui/controls/select-control.tsx +65 -0
  120. package/src/components/ui/floating-level-selector.tsx +9 -14
  121. package/src/components/ui/helpers/building-helper.tsx +4 -1
  122. package/src/components/ui/helpers/contextual-helper-panel.tsx +154 -70
  123. package/src/components/ui/helpers/helper-manager.test.tsx +69 -0
  124. package/src/components/ui/helpers/helper-manager.tsx +325 -14
  125. package/src/components/ui/helpers/item-helper.tsx +8 -0
  126. package/src/components/ui/helpers/registered-tool-helper.tsx +8 -1
  127. package/src/components/ui/icon-ref.tsx +45 -1
  128. package/src/components/ui/level-duplicate-dialog.tsx +1 -1
  129. package/src/components/ui/node-icon-image.tsx +16 -0
  130. package/src/components/ui/panels/collections/collections-popover.tsx +20 -14
  131. package/src/components/ui/panels/floor-foundation-panel.tsx +551 -0
  132. package/src/components/ui/panels/floor-opening-panel.tsx +143 -0
  133. package/src/components/ui/panels/mobile-panel-sheet.tsx +2 -2
  134. package/src/components/ui/panels/mobile-selection-bar.tsx +2 -2
  135. package/src/components/ui/panels/multi-field-value.test.ts +26 -3
  136. package/src/components/ui/panels/multi-field-value.ts +6 -3
  137. package/src/components/ui/panels/multi-parametric-inspector.tsx +52 -2
  138. package/src/components/ui/panels/multi-selection-panel.tsx +43 -4
  139. package/src/components/ui/panels/node-display.ts +8 -3
  140. package/src/components/ui/panels/panel-manager.tsx +103 -68
  141. package/src/components/ui/panels/panel-wrapper.tsx +78 -36
  142. package/src/components/ui/panels/parametric-field-control.tsx +1 -1
  143. package/src/components/ui/panels/parametric-inspector.tsx +4 -4
  144. package/src/components/ui/panels/reference-panel.tsx +2 -3
  145. package/src/components/ui/panels/room-construction-rows.tsx +774 -0
  146. package/src/components/ui/panels/room-name-options.test.ts +55 -0
  147. package/src/components/ui/panels/room-name-options.ts +47 -0
  148. package/src/components/ui/panels/room-openings-field.tsx +110 -0
  149. package/src/components/ui/panels/room-panel.tsx +725 -0
  150. package/src/components/ui/panels/shape-choice.tsx +58 -0
  151. package/src/components/ui/panels/wall-region-list.tsx +109 -0
  152. package/src/components/ui/primitives/color-dot.tsx +36 -24
  153. package/src/components/ui/primitives/sidebar.tsx +2 -0
  154. package/src/components/ui/right-stack.test.ts +29 -0
  155. package/src/components/ui/right-stack.tsx +145 -0
  156. package/src/components/ui/sidebar/icon-rail.tsx +5 -0
  157. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +31 -4
  158. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +5 -5
  159. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -4
  160. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +1 -5
  161. package/src/components/ui/sidebar/panels/site-panel/collection-tree-node.tsx +197 -0
  162. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -5
  163. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +1 -5
  164. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +1 -5
  165. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -5
  166. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +24 -11
  167. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +1 -5
  168. package/src/components/ui/sidebar/panels/site-panel/index.tsx +176 -211
  169. package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +30 -8
  170. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +1 -3
  171. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +4 -1
  172. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +1 -3
  173. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +1 -4
  174. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +1 -3
  175. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +21 -13
  176. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +1 -5
  177. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -5
  178. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +1 -5
  179. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +0 -27
  180. package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +2 -3
  181. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -5
  182. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +1 -5
  183. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +9 -4
  184. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +0 -2
  185. package/src/components/ui/sidebar/tab-bar.tsx +10 -0
  186. package/src/components/ui/sidebar/use-plugin-panels.tsx +15 -5
  187. package/src/components/viewer/floorplan-compass-button.tsx +9 -1
  188. package/src/components/viewer/floorplan-preview.tsx +5 -6
  189. package/src/components/viewer/viewer-hover-label.tsx +34 -0
  190. package/src/components/viewer/viewer-rooms.tsx +175 -0
  191. package/src/components/viewer/viewer-scene-header.tsx +181 -145
  192. package/src/components/viewer/viewer-selection-manager.tsx +124 -0
  193. package/src/components/viewer/viewer-stage.tsx +2 -2
  194. package/src/components/viewer-zone-system.tsx +3 -8
  195. package/src/hooks/use-auto-save.integration.test.tsx +123 -9
  196. package/src/hooks/use-auto-save.ts +34 -8
  197. package/src/hooks/use-keyboard.test.ts +178 -3
  198. package/src/hooks/use-keyboard.ts +89 -71
  199. package/src/hooks/use-open-wall-ends.test.ts +99 -0
  200. package/src/hooks/use-open-wall-ends.ts +238 -0
  201. package/src/hooks/use-placement-notice.test.ts +48 -0
  202. package/src/hooks/use-placement-notice.ts +42 -0
  203. package/src/hooks/use-selected-room.ts +143 -0
  204. package/src/index.tsx +246 -39
  205. package/src/lib/ceiling-edit-session.test.ts +321 -0
  206. package/src/lib/ceiling-edit-session.ts +206 -0
  207. package/src/lib/ceiling-region-session.test.ts +179 -0
  208. package/src/lib/continuation.test.ts +19 -1
  209. package/src/lib/continuation.ts +25 -4
  210. package/src/lib/door-interaction.ts +14 -0
  211. package/src/lib/editor-level-display.test.ts +96 -0
  212. package/src/lib/editor-level-display.ts +48 -0
  213. package/src/lib/editor-views.ts +144 -0
  214. package/src/lib/export-geometry-wait.test.ts +90 -0
  215. package/src/lib/floor-edit-session.ts +153 -0
  216. package/src/lib/floor-footprints.test.ts +563 -0
  217. package/src/lib/floor-footprints.ts +395 -0
  218. package/src/lib/floor-foundation-support.test.ts +283 -0
  219. package/src/lib/floor-opening-draft.ts +218 -0
  220. package/src/lib/floor-openings.test.ts +216 -0
  221. package/src/lib/floor-region-geometry.ts +191 -0
  222. package/src/lib/floor-region-session.test.ts +485 -0
  223. package/src/lib/floor-region-session.ts +589 -0
  224. package/src/lib/floor-region-snap.ts +101 -0
  225. package/src/lib/floor-step-hover.test.ts +32 -0
  226. package/src/lib/floorplan/annotation-visibility.ts +17 -0
  227. package/src/lib/floorplan/dimension-drive.test.ts +360 -0
  228. package/src/lib/floorplan/dimension-drive.ts +479 -0
  229. package/src/lib/floorplan/floorplan-export.test.ts +12 -8
  230. package/src/lib/floorplan/floorplan-export.tsx +32 -8
  231. package/src/lib/floorplan/floorplan-extension.ts +32 -0
  232. package/src/lib/floorplan/floorplan-mode.test.ts +4 -0
  233. package/src/lib/floorplan/floorplan-mode.ts +2 -0
  234. package/src/lib/floorplan/floorplan-pdfkit-document.ts +339 -18
  235. package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +138 -8
  236. package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +56 -25
  237. package/src/lib/floorplan/floorplan-plugin-install.test.tsx +138 -0
  238. package/src/lib/floorplan/floorplan-readonly.ts +6 -0
  239. package/src/lib/floorplan/geometry.ts +13 -6
  240. package/src/lib/floorplan/index.ts +0 -9
  241. package/src/lib/floorplan/items.ts +5 -2
  242. package/src/lib/floorplan/marks.test.ts +213 -0
  243. package/src/lib/floorplan/marks.ts +238 -0
  244. package/src/lib/floorplan/open-wall-ends.test.ts +268 -0
  245. package/src/lib/floorplan/open-wall-ends.ts +162 -0
  246. package/src/lib/floorplan/pdf-fonts/GeistMono-Bold.ttf +0 -0
  247. package/src/lib/floorplan/pdf-fonts/GeistMono-Regular.ttf +0 -0
  248. package/src/lib/floorplan/pdf-fonts/LICENSE-GeistMono.txt +92 -0
  249. package/src/lib/floorplan/pdf-fonts/LICENSE-LiberationSans.txt +102 -0
  250. package/src/lib/floorplan/pdf-fonts/LiberationSans-Bold.ttf +0 -0
  251. package/src/lib/floorplan/pdf-fonts/LiberationSans-Regular.ttf +0 -0
  252. package/src/lib/floorplan/schedules.test.ts +189 -0
  253. package/src/lib/floorplan/schedules.ts +339 -0
  254. package/src/lib/floorplan/sheet-export.test.ts +100 -0
  255. package/src/lib/floorplan/sheet-export.ts +318 -0
  256. package/src/lib/floorplan/site-plan/build-site-plan-drawing.ts +779 -0
  257. package/src/lib/floorplan/site-plan/contours.test.ts +43 -0
  258. package/src/lib/floorplan/site-plan/contributors.ts +97 -0
  259. package/src/lib/floorplan/site-plan/coverage.ts +232 -0
  260. package/src/lib/floorplan/site-plan/drawing-type-switch.test.ts +15 -0
  261. package/src/lib/floorplan/site-plan/drawing-type-switch.tsx +86 -0
  262. package/src/lib/floorplan/site-plan/front-edge.test.ts +103 -0
  263. package/src/lib/floorplan/site-plan/front-edge.ts +156 -0
  264. package/src/lib/floorplan/site-plan/geometry.test.ts +275 -0
  265. package/src/lib/floorplan/site-plan/geometry.ts +557 -0
  266. package/src/lib/floorplan/site-plan/index.ts +71 -0
  267. package/src/lib/floorplan/site-plan/site-annotations.test.ts +366 -0
  268. package/src/lib/floorplan/site-plan/site-annotations.ts +1083 -0
  269. package/src/lib/floorplan/site-plan/site-parts.ts +379 -0
  270. package/src/lib/floorplan/site-plan/site-plan-layer.tsx +138 -0
  271. package/src/lib/floorplan/site-plan/street-name.test.ts +69 -0
  272. package/src/lib/floorplan/site-plan/street-name.ts +302 -0
  273. package/src/lib/floorplan/site-plan/yards.test.ts +69 -0
  274. package/src/lib/floorplan/types.ts +1 -27
  275. package/src/lib/fresh-planar-placement.test.ts +36 -0
  276. package/src/lib/fresh-planar-placement.ts +59 -36
  277. package/src/lib/geometry-script/author.ts +152 -0
  278. package/src/lib/geometry-script/client.ts +152 -0
  279. package/src/lib/geometry-script/protocol.ts +12 -0
  280. package/src/lib/gesture-lifecycle.test.ts +595 -0
  281. package/src/lib/gesture-lifecycle.ts +290 -0
  282. package/src/lib/glb-export-zone-holes.test.ts +41 -0
  283. package/src/lib/glb-export.test.ts +485 -0
  284. package/src/lib/glb-export.ts +237 -119
  285. package/src/lib/history.test.ts +6 -0
  286. package/src/lib/history.ts +7 -0
  287. package/src/lib/hud-title.ts +109 -0
  288. package/src/lib/hydration-gated-storage.ts +49 -0
  289. package/src/lib/ifc-export.test.ts +174 -0
  290. package/src/lib/ifc-export.ts +201 -0
  291. package/src/lib/ifc-parts.ts +15 -0
  292. package/src/lib/inspector-expanded.test.ts +44 -0
  293. package/src/lib/inspector-expanded.ts +36 -0
  294. package/src/lib/interaction/scope.ts +18 -0
  295. package/src/lib/keyboard-pan.test.ts +129 -7
  296. package/src/lib/keyboard-pan.ts +161 -12
  297. package/src/lib/live-gesture-round-trip.test.ts +360 -0
  298. package/src/lib/lot/__fixtures__/map-dossier-miami-shores.json +1 -0
  299. package/src/lib/lot/__fixtures__/map-dossier-stpete.json +1 -0
  300. package/src/lib/lot/clean-ring.test.ts +186 -0
  301. package/src/lib/lot/clean-ring.ts +217 -0
  302. package/src/lib/lot/dossier.test.ts +202 -0
  303. package/src/lib/lot/dossier.ts +498 -0
  304. package/src/lib/lot/drop-in.test.ts +326 -0
  305. package/src/lib/lot/drop-in.ts +407 -0
  306. package/src/lib/lot/index.ts +43 -0
  307. package/src/lib/lot/lot-patch.test.ts +340 -0
  308. package/src/lib/lot/lot-patch.ts +394 -0
  309. package/src/lib/lot/parcel-provider.ts +43 -0
  310. package/src/lib/lot/terrain.test.ts +161 -0
  311. package/src/lib/lot/terrain.ts +266 -0
  312. package/src/lib/material-catalog-model.ts +3 -1
  313. package/src/lib/material-paint-panel-model.ts +9 -5
  314. package/src/lib/material-paint.ts +4 -0
  315. package/src/lib/mezzanine-actions.test.ts +342 -0
  316. package/src/lib/mezzanine-draft.test.ts +501 -0
  317. package/src/lib/mezzanine-draft.ts +278 -0
  318. package/src/lib/mezzanine-messages.ts +26 -0
  319. package/src/lib/mezzanine-selection.test.ts +207 -0
  320. package/src/lib/mezzanine-stairs.ts +46 -0
  321. package/src/lib/model-export.ts +3 -1
  322. package/src/lib/node-action-movement.ts +17 -6
  323. package/src/lib/node-display-label.ts +37 -0
  324. package/src/lib/orbit-floor-clearance.test.ts +165 -0
  325. package/src/lib/orbit-floor-clearance.ts +106 -0
  326. package/src/lib/paint-commit-route.test.ts +37 -0
  327. package/src/lib/paint-commit-route.ts +17 -0
  328. package/src/lib/paint-eyedropper.test.tsx +165 -0
  329. package/src/lib/paint-eyedropper.ts +99 -0
  330. package/src/lib/paint-pass-through.test.ts +111 -0
  331. package/src/lib/paint-pass-through.ts +76 -0
  332. package/src/lib/paint-region-hover.ts +25 -0
  333. package/src/lib/paint-region-mode.ts +154 -0
  334. package/src/lib/paint-regions.test.ts +166 -0
  335. package/src/lib/paint-regions.ts +192 -0
  336. package/src/lib/paint-scope.test.ts +317 -35
  337. package/src/lib/paint-scope.ts +266 -25
  338. package/src/lib/panel-rows.ts +21 -0
  339. package/src/lib/parametric-node-update.ts +1 -1
  340. package/src/lib/plate-paint-affected.test.ts +300 -0
  341. package/src/lib/plate-paint-affected.ts +177 -0
  342. package/src/lib/plugin-chat.ts +50 -0
  343. package/src/lib/plugin-panels.test.ts +25 -0
  344. package/src/lib/plugin-panels.ts +94 -0
  345. package/src/lib/print-shell-compiler.ts +1 -0
  346. package/src/lib/registered-draft-snap.ts +12 -0
  347. package/src/lib/roof-duplication.ts +10 -2
  348. package/src/lib/room-assembly-overlay.test.ts +275 -0
  349. package/src/lib/room-assembly-overlay.ts +266 -0
  350. package/src/lib/room-built-on.test.ts +379 -0
  351. package/src/lib/room-built-on.ts +138 -0
  352. package/src/lib/room-construction-commands.ts +712 -0
  353. package/src/lib/room-construction.test.ts +746 -0
  354. package/src/lib/room-construction.ts +385 -0
  355. package/src/lib/room-controls-phase6.test.ts +630 -0
  356. package/src/lib/room-divide-session.ts +523 -0
  357. package/src/lib/room-handle-drag.ts +1068 -0
  358. package/src/lib/room-name-catalog.test.ts +90 -0
  359. package/src/lib/room-name-catalog.ts +168 -0
  360. package/src/lib/room-push-dimensions.test.ts +311 -0
  361. package/src/lib/room-push-dimensions.ts +251 -0
  362. package/src/lib/room-selection-commands.ts +62 -0
  363. package/src/lib/room-selection.test.ts +973 -0
  364. package/src/lib/room-selection.ts +417 -0
  365. package/src/lib/room-structure-commands.ts +94 -0
  366. package/src/lib/room-structure.test.ts +921 -0
  367. package/src/lib/room-transform-session.ts +570 -0
  368. package/src/lib/room-zone-routing.test.ts +294 -0
  369. package/src/lib/room-zone-routing.ts +197 -0
  370. package/src/lib/scene-clipboard.test.ts +364 -0
  371. package/src/lib/scene-clipboard.ts +229 -48
  372. package/src/lib/scene-material-refs.test.ts +79 -0
  373. package/src/lib/scene-material-refs.ts +79 -0
  374. package/src/lib/scene.test.ts +24 -36
  375. package/src/lib/scene.ts +33 -57
  376. package/src/lib/selection-routing.test.ts +129 -45
  377. package/src/lib/selection-routing.ts +53 -74
  378. package/src/lib/session-writes.test.ts +120 -0
  379. package/src/lib/session-writes.ts +205 -0
  380. package/src/lib/sidebar-panel.ts +62 -0
  381. package/src/lib/site-boundary.test.ts +4 -6
  382. package/src/lib/site-boundary.ts +1 -1
  383. package/src/lib/snapping-mode.test.ts +4 -0
  384. package/src/lib/snapping-mode.ts +8 -1
  385. package/src/lib/terrace-draft.test.ts +145 -0
  386. package/src/lib/terrace-draft.ts +213 -0
  387. package/src/lib/tool-cursor-icon.test.ts +16 -0
  388. package/src/lib/tool-cursor-icon.ts +11 -0
  389. package/src/lib/typing-target.ts +9 -0
  390. package/src/lib/units.ts +77 -39
  391. package/src/lib/vector-edges.test.ts +58 -0
  392. package/src/lib/vector-edges.ts +400 -0
  393. package/src/lib/viewer-selection.test.ts +577 -0
  394. package/src/lib/viewer-selection.ts +569 -0
  395. package/src/lib/wall-draw-variant.test.ts +71 -0
  396. package/src/lib/wall-draw-variant.ts +91 -0
  397. package/src/lib/wall-paint-region-session.ts +246 -0
  398. package/src/lib/wall-paint-region.test.ts +373 -0
  399. package/src/lib/wall-polygon-draft.test.ts +262 -0
  400. package/src/lib/wall-polygon-draft.ts +286 -0
  401. package/src/lib/wall-push.test.ts +453 -0
  402. package/src/lib/wall-region-face.ts +352 -0
  403. package/src/lib/wall-region-handles.test.ts +304 -0
  404. package/src/lib/wall-region-handles.ts +368 -0
  405. package/src/lib/wall-region-list.test.ts +119 -0
  406. package/src/lib/wall-region-list.ts +136 -0
  407. package/src/lib/wall-region-snap.ts +110 -0
  408. package/src/lib/window-interaction.ts +14 -0
  409. package/src/lib/zone-content.ts +4 -1
  410. package/src/lib/zone-name.test.ts +17 -0
  411. package/src/lib/zone-name.ts +17 -0
  412. package/src/store/opens-in-select-mode.test.ts +223 -0
  413. package/src/store/paint-mode.test.ts +188 -0
  414. package/src/store/terrain-sculpt-mode.test.ts +19 -21
  415. package/src/store/tool-mode.test.ts +137 -6
  416. package/src/store/use-delete-confirmation.ts +28 -1
  417. package/src/store/use-drawing-view.test.ts +24 -3
  418. package/src/store/use-drawing-view.ts +29 -1
  419. package/src/store/use-editor.tsx +432 -321
  420. package/src/store/use-floorplan-draft-preview.ts +13 -0
  421. package/src/store/use-floorplan-marquee.ts +12 -2
  422. package/src/store/use-quick-measurement-hud.ts +3 -3
  423. package/src/store/use-stair-build-preview.ts +13 -5
  424. package/src/store/use-wall-move-ghosts.ts +4 -2
  425. package/src/store/use-wall-region-handles-selection.test.ts +222 -0
  426. package/src/store/use-wall-region-handles-selection.ts +139 -0
  427. package/src/store/view-layout.test.ts +175 -0
  428. package/src/store/view-layout.ts +196 -0
  429. package/src/test-utils/immediate-animation-frames.ts +16 -0
  430. package/src/test-utils/live-gesture-round-trip.ts +224 -0
  431. package/src/test-utils/selection-harness.ts +82 -0
  432. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +0 -499
  433. package/src/components/systems/zone/zone-label-editor-system.tsx +0 -333
  434. package/src/components/tools/fence/fence-drafting.test.ts +0 -100
  435. package/src/components/tools/fence/fence-drafting.ts +0 -266
  436. package/src/components/ui/sidebar/panels/plugins-panel.tsx +0 -218
  437. package/src/components/viewer/viewer-units-panel.tsx +0 -116
  438. package/src/lib/floorplan/stairs.ts +0 -489
  439. package/src/lib/level-duplication.test.ts +0 -144
  440. package/src/lib/level-duplication.ts +0 -195
@@ -0,0 +1,90 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+ import { type AnyNode, ZoneNode } from '@pascal-app/core'
3
+ import {
4
+ MEZZANINE_NAME_SECTION,
5
+ numberedRoomName,
6
+ ROOM_NAME_SECTIONS,
7
+ roomNameContext,
8
+ roomNameSections,
9
+ roomNameUseCount,
10
+ } from './room-name-catalog'
11
+
12
+ const square: [number, number][] = [
13
+ [0, 0],
14
+ [4, 0],
15
+ [4, 4],
16
+ [0, 4],
17
+ ]
18
+ function zone(id: string, patch: Record<string, unknown> = {}) {
19
+ return ZoneNode.parse({
20
+ id,
21
+ name: '',
22
+ spaceRole: 'room',
23
+ parentId: 'level_names',
24
+ polygon: square,
25
+ boundaryWallIds: ['wall_a'],
26
+ ...patch,
27
+ }) as AnyNode
28
+ }
29
+ const graph = (...zones: AnyNode[]) => Object.fromEntries(zones.map((z) => [z.id, z]))
30
+ const ids = (nodes: Record<string, AnyNode>, zoneId: string) =>
31
+ roomNameSections(nodes, zoneId).map((section) => section.id)
32
+
33
+ describe('room name catalog', () => {
34
+ test('every catalog name is sentence case and listed once', () => {
35
+ const names = ROOM_NAME_SECTIONS.flatMap((section) => section.names)
36
+ expect(new Set(names).size).toBe(names.length)
37
+ for (const name of [...names, ...MEZZANINE_NAME_SECTION.names])
38
+ expect(name[0]).toBe(name[0]!.toUpperCase())
39
+ })
40
+
41
+ test('an indoor room lists the catalog in the usual order: no project names, no mezzanine names', () => {
42
+ const nodes = graph(
43
+ zone('zone_a', { name: 'Kitchen' }),
44
+ zone('zone_c', { name: 'Snug' }),
45
+ zone('zone_d', { name: '' }),
46
+ )
47
+ expect(ids(nodes, 'zone_d')).toEqual(ROOM_NAME_SECTIONS.map((s) => s.id))
48
+ })
49
+
50
+ test('a picked name that is taken gets the next free number; counts ignore the number', () => {
51
+ const nodes = graph(
52
+ zone('zone_a', { name: 'Bedroom' }),
53
+ zone('zone_b', { name: 'Bedroom 2' }),
54
+ zone('zone_c', { name: 'Kitchen' }),
55
+ zone('zone_self', { name: 'Bedroom 3' }),
56
+ )
57
+ expect(roomNameUseCount(nodes, 'zone_self', 'Bedroom')).toBe(2)
58
+ expect(roomNameUseCount(nodes, 'zone_self', 'Office')).toBe(0)
59
+ expect(numberedRoomName(nodes, 'zone_self', 'Bedroom')).toBe('Bedroom 3')
60
+ expect(numberedRoomName(nodes, 'zone_self', 'Office')).toBe('Office')
61
+ expect(numberedRoomName(nodes, 'zone_c', 'Kitchen')).toBe('Kitchen')
62
+ // Only the level counts: the same name upstairs is not a repeat.
63
+ const upstairs = zone('zone_up', { name: '', parentId: 'level_upstairs' })
64
+ expect(numberedRoomName({ ...nodes, [upstairs.id]: upstairs }, 'zone_up', 'Bedroom')).toBe(
65
+ 'Bedroom',
66
+ )
67
+ })
68
+
69
+ test('outdoor-looking rooms list Outdoor first', () => {
70
+ const cases = [
71
+ zone('zone_open_sky', { hasCeiling: false }),
72
+ zone('zone_terrace', { boundaryWallIds: [] }),
73
+ zone('zone_outside', { spaceRole: 'generic' }),
74
+ ]
75
+ for (const node of cases) {
76
+ expect(roomNameContext(graph(node), node.id)).toBe('outdoor')
77
+ expect(ids(graph(node), node.id)[0]).toBe('outdoor')
78
+ expect(ids(graph(node), node.id).filter((id) => id === 'outdoor')).toHaveLength(1)
79
+ }
80
+ })
81
+
82
+ test('a mezzanine gets the Mezzanine names first; other rooms never see them', () => {
83
+ const mezzanine = zone('zone_mezz', {
84
+ floor: { support: 'open', elevation: 1.4, thickness: 0.2 },
85
+ })
86
+ expect(roomNameContext(graph(mezzanine), mezzanine.id)).toBe('mezzanine')
87
+ expect(ids(graph(mezzanine), mezzanine.id)[0]).toBe('mezzanine')
88
+ expect(ids(graph(zone('zone_room')), 'zone_room')).not.toContain('mezzanine')
89
+ })
90
+ })
@@ -0,0 +1,168 @@
1
+ import type { AnyNode } from '@pascal-app/core'
2
+
3
+ /** A titled group of the room-name combobox. */
4
+ export type RoomNameSection = { id: string; title: string; names: readonly string[] }
5
+
6
+ /** What the room reads as, which decides the section shown first. */
7
+ export type RoomNameContext = 'room' | 'outdoor' | 'mezzanine'
8
+
9
+ /** Names for any room, in the order the dropdown lists their sections. */
10
+ export const ROOM_NAME_SECTIONS: readonly RoomNameSection[] = [
11
+ {
12
+ id: 'living',
13
+ title: 'Living',
14
+ names: [
15
+ 'Living room',
16
+ 'Family room',
17
+ 'Dining room',
18
+ 'Kitchen',
19
+ 'Kitchenette',
20
+ 'Pantry',
21
+ 'Lounge',
22
+ 'Den',
23
+ 'Library',
24
+ 'Playroom',
25
+ 'Home cinema',
26
+ 'Games room',
27
+ 'Sunroom',
28
+ ],
29
+ },
30
+ {
31
+ id: 'sleeping',
32
+ title: 'Sleeping',
33
+ names: [
34
+ 'Bedroom',
35
+ 'Primary bedroom',
36
+ 'Guest room',
37
+ "Kids' room",
38
+ 'Nursery',
39
+ 'Dressing room',
40
+ 'Walk-in closet',
41
+ ],
42
+ },
43
+ {
44
+ id: 'bathrooms',
45
+ title: 'Bathrooms',
46
+ names: ['Bathroom', 'En-suite', 'Shower room', 'Toilet', 'Powder room'],
47
+ },
48
+ {
49
+ id: 'work',
50
+ title: 'Work & hobbies',
51
+ names: ['Office', 'Study', 'Studio', 'Workshop', 'Gym'],
52
+ },
53
+ {
54
+ id: 'circulation',
55
+ title: 'Circulation',
56
+ names: ['Entrance', 'Hallway', 'Corridor', 'Landing', 'Stairwell', 'Mudroom'],
57
+ },
58
+ {
59
+ id: 'utility',
60
+ title: 'Utility & storage',
61
+ names: [
62
+ 'Laundry',
63
+ 'Utility room',
64
+ 'Storage',
65
+ 'Closet',
66
+ 'Boiler room',
67
+ 'Cellar',
68
+ 'Basement',
69
+ 'Attic',
70
+ 'Garage',
71
+ ],
72
+ },
73
+ {
74
+ id: 'outdoor',
75
+ title: 'Outdoor',
76
+ names: [
77
+ 'Garden',
78
+ 'Patio',
79
+ 'Terrace',
80
+ 'Balcony',
81
+ 'Porch',
82
+ 'Veranda',
83
+ 'Deck',
84
+ 'Courtyard',
85
+ 'Pool area',
86
+ 'Carport',
87
+ 'Garden shed',
88
+ ],
89
+ },
90
+ ]
91
+
92
+ /** Only offered, and first, when the selection is a mezzanine. */
93
+ export const MEZZANINE_NAME_SECTION: RoomNameSection = {
94
+ id: 'mezzanine',
95
+ title: 'Mezzanine',
96
+ names: ['Mezzanine', 'Loft', 'Reading nook', 'Gallery'],
97
+ }
98
+
99
+ /**
100
+ * A mezzanine is one; a room reads as outdoor with no ceiling, without a wall
101
+ * of its own (a terrace closed by separators) or when it is not a room inside
102
+ * walls at all. Everything else is an indoor room.
103
+ */
104
+ export function roomNameContext(nodes: Record<string, AnyNode>, zoneId: string): RoomNameContext {
105
+ const zone = nodes[zoneId]
106
+ if (zone?.type !== 'zone') return 'room'
107
+ if (zone.floor?.support === 'open') return 'mezzanine'
108
+ if (zone.spaceRole !== 'room' || zone.hasCeiling === false || !zone.boundaryWallIds.length)
109
+ return 'outdoor'
110
+ return 'room'
111
+ }
112
+
113
+ /** A name without the number the picker adds to a repeat: "Bedroom 2" → "Bedroom". */
114
+ export function baseRoomName(name: string) {
115
+ return name.trim().replace(/\s+\d+$/, '')
116
+ }
117
+
118
+ /** The names the level's other rooms go by (a mezzanine's level is its host's). */
119
+ function otherRoomNames(nodes: Record<string, AnyNode>, zoneId: string) {
120
+ const levelId = nodes[zoneId]?.parentId
121
+ const names: string[] = []
122
+ for (const node of Object.values(nodes)) {
123
+ if (node.type !== 'zone' || node.id === zoneId || node.parentId !== levelId) continue
124
+ const name = node.name?.trim()
125
+ if (name) names.push(name)
126
+ }
127
+ return names
128
+ }
129
+
130
+ /** How many of the level's other rooms already go by `name` (numbered or not). */
131
+ export function roomNameUseCount(nodes: Record<string, AnyNode>, zoneId: string, name: string) {
132
+ const wanted = name.trim().toLowerCase()
133
+ return otherRoomNames(nodes, zoneId).filter(
134
+ (other) => baseRoomName(other).toLowerCase() === wanted,
135
+ ).length
136
+ }
137
+
138
+ /**
139
+ * The name a picked suggestion becomes: itself while no other room on the
140
+ * level has it, otherwise the first free "Name 2", "Name 3"…
141
+ */
142
+ export function numberedRoomName(nodes: Record<string, AnyNode>, zoneId: string, name: string) {
143
+ const taken = new Set(otherRoomNames(nodes, zoneId).map((other) => other.toLowerCase()))
144
+ if (!taken.has(name.toLowerCase())) return name
145
+ let index = 2
146
+ while (taken.has(`${name} ${index}`.toLowerCase())) index++
147
+ return `${name} ${index}`
148
+ }
149
+
150
+ /**
151
+ * The combobox's sections for one room: the catalog, with the section its
152
+ * context calls for first (Mezzanine, only for a mezzanine; Outdoor for an
153
+ * outdoor room), then the rest in their usual order. Other rooms' names are
154
+ * not offered — a name already used on the level is numbered when picked.
155
+ */
156
+ export function roomNameSections(
157
+ nodes: Record<string, AnyNode>,
158
+ zoneId: string,
159
+ ): RoomNameSection[] {
160
+ const context = roomNameContext(nodes, zoneId)
161
+ const first =
162
+ context === 'mezzanine'
163
+ ? [MEZZANINE_NAME_SECTION]
164
+ : context === 'outdoor'
165
+ ? ROOM_NAME_SECTIONS.filter((section) => section.id === 'outdoor')
166
+ : []
167
+ return [...first, ...ROOM_NAME_SECTIONS.filter((section) => !first.includes(section))]
168
+ }
@@ -0,0 +1,311 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+ import {
3
+ type AnyNode,
4
+ createRoomTopologyIndex,
5
+ LevelNode,
6
+ type Point,
7
+ WallNode,
8
+ } from '@pascal-app/core'
9
+ import { formatLinearMeasurement } from './measurements'
10
+ import {
11
+ DIMENSION_FLOOR_LIFT,
12
+ mezzanineEdgeDimensions,
13
+ type RoomDimension,
14
+ wallPushDimensionRooms,
15
+ wallPushDimensions,
16
+ } from './room-push-dimensions'
17
+
18
+ // Rooms are real: the walls go through core's room topology, so the faces the
19
+ // dimensions read are the ones the push arrows and the room outline use. Walls
20
+ // are 0.2 m thick and centred, so a clear width is the centreline span − 0.2.
21
+
22
+ const LEVEL = 'level_push_dimensions'
23
+ const THICKNESS = 0.2
24
+
25
+ type Scene = { nodes: Record<string, AnyNode>; walls: WallNode[] }
26
+
27
+ function scene(segments: Array<[Point, Point]>, extra: Partial<WallNode> = {}): Scene {
28
+ const walls = segments.map(([start, end], i) =>
29
+ WallNode.parse({
30
+ id: `wall_d${i}`,
31
+ parentId: LEVEL,
32
+ start,
33
+ end,
34
+ thickness: THICKNESS,
35
+ ...extra,
36
+ }),
37
+ )
38
+ const level = LevelNode.parse({ id: LEVEL, children: walls.map((wall) => wall.id) })
39
+ return {
40
+ walls,
41
+ nodes: Object.fromEntries([level, ...walls].map((node) => [node.id, node as AnyNode])),
42
+ }
43
+ }
44
+
45
+ function ring(points: Point[]): Array<[Point, Point]> {
46
+ return points.map((p, i) => [p, points[(i + 1) % points.length]!])
47
+ }
48
+
49
+ function measure(
50
+ { nodes }: Scene,
51
+ wall: WallNode,
52
+ outward: Point,
53
+ distance: number,
54
+ span: { t0: number; t1: number } = { t0: 0, t1: 1 },
55
+ elevationOf: (roomId: string) => number = () => 0,
56
+ ): RoomDimension[] {
57
+ const index = createRoomTopologyIndex()
58
+ index.rebuildLevel(LEVEL, nodes)
59
+ const rooms = index.getLevelTopology(LEVEL)?.rooms ?? []
60
+ return wallPushDimensions({
61
+ rooms: wallPushDimensionRooms(rooms, { wallId: wall.id, ...span }, elevationOf),
62
+ wall,
63
+ outward,
64
+ distance,
65
+ resolve: (id) => nodes[id],
66
+ })
67
+ }
68
+
69
+ const distances = (dimensions: RoomDimension[]) =>
70
+ dimensions.map((dimension) => Number(dimension.distance.toFixed(4)))
71
+
72
+ const wallAt = ({ walls }: Scene, start: Point, end: Point) =>
73
+ walls.find(
74
+ (wall) =>
75
+ Math.hypot(wall.start[0] - start[0], wall.start[1] - start[1]) < 1e-9 &&
76
+ Math.hypot(wall.end[0] - end[0], wall.end[1] - end[1]) < 1e-9,
77
+ )!
78
+
79
+ describe('a rectangular room', () => {
80
+ const room = scene(
81
+ ring([
82
+ [0, 0],
83
+ [6, 0],
84
+ [6, 4],
85
+ [0, 4],
86
+ ]),
87
+ )
88
+ const east = wallAt(room, [6, 0], [6, 4])
89
+
90
+ test('at rest: the clear width, east face to west face, across the room', () => {
91
+ const [dimension, ...rest] = measure(room, east, [1, 0], 0)
92
+ expect(rest).toEqual([])
93
+ expect(dimension!.distance).toBeCloseTo(6 - THICKNESS)
94
+ // Face to face along the push axis, at the middle of the shared stretch.
95
+ expect(dimension!.from[0]).toBeCloseTo(6 - THICKNESS / 2)
96
+ expect(dimension!.to[0]).toBeCloseTo(THICKNESS / 2)
97
+ expect(dimension!.from[1]).toBeCloseTo(dimension!.to[1])
98
+ expect(dimension!.from[1]).toBeGreaterThan(0)
99
+ expect(dimension!.from[1]).toBeLessThan(4)
100
+ })
101
+
102
+ test('follows the push both ways; only the room side is measured', () => {
103
+ expect(distances(measure(room, east, [1, 0], 1.5))).toEqual([7.3])
104
+ expect(distances(measure(room, east, [1, 0], -2))).toEqual([3.8])
105
+ })
106
+
107
+ test('the room on a raised floor measures just above that floor', () => {
108
+ const [dimension] = measure(room, east, [1, 0], 0, undefined, () => 0.45)
109
+ expect(dimension!.elevation).toBeCloseTo(0.45 + DIMENSION_FLOOR_LIFT)
110
+ })
111
+
112
+ test('a far wall split in two collinear pieces is one dimension', () => {
113
+ const split = scene([
114
+ [
115
+ [0, 0],
116
+ [6, 0],
117
+ ],
118
+ [
119
+ [6, 0],
120
+ [6, 4],
121
+ ],
122
+ [
123
+ [6, 4],
124
+ [0, 4],
125
+ ],
126
+ [
127
+ [0, 4],
128
+ [0, 2],
129
+ ],
130
+ [
131
+ [0, 2],
132
+ [0, 0],
133
+ ],
134
+ ])
135
+ expect(distances(measure(split, wallAt(split, [6, 0], [6, 4]), [1, 0], 0))).toEqual([5.8])
136
+ })
137
+
138
+ test('a curved moving wall gets no dimension', () => {
139
+ const curved = scene(
140
+ ring([
141
+ [0, 0],
142
+ [6, 0],
143
+ [6, 4],
144
+ [0, 4],
145
+ ]),
146
+ )
147
+ const wall = wallAt(curved, [6, 0], [6, 4])
148
+ expect(measure(curved, { ...wall, curveOffset: 0.8 }, [1, 0], 0)).toEqual([])
149
+ })
150
+ })
151
+
152
+ describe('an L-shaped room', () => {
153
+ // The notch is the top right: the south wall faces the notch wall (y = 2)
154
+ // and the north wall (y = 4), two distinct widths, nearest first.
155
+ const room = scene(
156
+ ring([
157
+ [0, 0],
158
+ [6, 0],
159
+ [6, 2],
160
+ [3, 2],
161
+ [3, 4],
162
+ [0, 4],
163
+ ]),
164
+ )
165
+ const south = wallAt(room, [0, 0], [6, 0])
166
+
167
+ test('every facing parallel wall, nearest first', () => {
168
+ const dimensions = measure(room, south, [0, -1], 0)
169
+ expect(distances(dimensions)).toEqual([1.8, 3.8])
170
+ // Each is measured where the two faces overlap: the notch over x 3..6, the north wall over 0..3.
171
+ expect(dimensions[0]!.from[0]).toBeGreaterThan(3)
172
+ expect(dimensions[1]!.from[0]).toBeLessThan(3)
173
+ expect(new Set(dimensions.map((dimension) => dimension.key)).size).toBe(2)
174
+ })
175
+
176
+ test('a wall that no longer faces the moving one drops out', () => {
177
+ // Pulled north past the notch wall (y = 2), only the north wall is ahead.
178
+ expect(distances(measure(room, south, [0, -1], -2.5))).toEqual([1.3])
179
+ })
180
+ })
181
+
182
+ describe('two rooms sharing the moving wall', () => {
183
+ const rooms = scene([
184
+ ...ring([
185
+ [0, 0],
186
+ [8, 0],
187
+ [8, 4],
188
+ [0, 4],
189
+ ]),
190
+ [
191
+ [4, 0],
192
+ [4, 4],
193
+ ],
194
+ ])
195
+ const middle = wallAt(rooms, [4, 0], [4, 4])
196
+
197
+ test('one width per side, each from its own face of the wall', () => {
198
+ const dimensions = measure(rooms, middle, [1, 0], 1)
199
+ expect(dimensions).toHaveLength(2)
200
+ const west = dimensions.find((dimension) => dimension.to[0] < 4)!
201
+ const east = dimensions.find((dimension) => dimension.to[0] > 4)!
202
+ expect(west.distance).toBeCloseTo(5 - THICKNESS)
203
+ expect(east.distance).toBeCloseTo(3 - THICKNESS)
204
+ expect(west.from[0]).toBeCloseTo(5 - THICKNESS / 2)
205
+ expect(east.from[0]).toBeCloseTo(5 + THICKNESS / 2)
206
+ expect(west.key).not.toBe(east.key)
207
+ })
208
+
209
+ test('a push over part of the wall measures only the rooms along that part', () => {
210
+ const dimensions = measure(rooms, middle, [1, 0], 0, { t0: 0, t1: 0.5 })
211
+ expect(dimensions).toHaveLength(2)
212
+ for (const dimension of dimensions) {
213
+ expect(dimension.from[1]).toBeGreaterThan(0)
214
+ expect(dimension.from[1]).toBeLessThan(2)
215
+ }
216
+ })
217
+ })
218
+
219
+ describe('a rotated room', () => {
220
+ const angle = Math.PI / 6
221
+ const rotate = ([x, z]: Point): Point => [
222
+ x * Math.cos(angle) - z * Math.sin(angle),
223
+ x * Math.sin(angle) + z * Math.cos(angle),
224
+ ]
225
+ const corners = (
226
+ [
227
+ [0, 0],
228
+ [6, 0],
229
+ [6, 4],
230
+ [0, 4],
231
+ ] as Point[]
232
+ ).map(rotate)
233
+ const room = scene(ring(corners))
234
+ const east = wallAt(room, corners[1]!, corners[2]!)
235
+ const outward = rotate([1, 0])
236
+
237
+ test('measures along the rotated axis, the same widths', () => {
238
+ const [dimension] = measure(room, east, outward, 0.5)
239
+ expect(dimension!.distance).toBeCloseTo(6.5 - THICKNESS)
240
+ const { from, to, distance } = dimension!
241
+ expect((to[0] - from[0]) / distance).toBeCloseTo(-outward[0])
242
+ expect((to[1] - from[1]) / distance).toBeCloseTo(-outward[1])
243
+ })
244
+
245
+ test('a wall about 2° off parallel is not measured', () => {
246
+ const skewed = scene(
247
+ ring([
248
+ [0, 0],
249
+ [6, 0],
250
+ [6, 4],
251
+ [0.15, 4],
252
+ ]),
253
+ )
254
+ expect(measure(skewed, wallAt(skewed, [6, 0], [6, 4]), [1, 0], 0)).toEqual([])
255
+ })
256
+ })
257
+
258
+ describe('a mezzanine edge', () => {
259
+ const rest: Point[] = [
260
+ [0, 0],
261
+ [5, 0],
262
+ [5, 3],
263
+ [2, 3],
264
+ [2, 2],
265
+ [0, 2],
266
+ ]
267
+
268
+ test('measures to each facing parallel edge of the outline it would leave', () => {
269
+ // Edge 0 (south, y = 0) pushed 0.5 m south: the outline follows.
270
+ const outline = rest.map(([x, z], i): Point => (i < 2 ? [x, z - 0.5] : [x, z]))
271
+ const dimensions = mezzanineEdgeDimensions({
272
+ rest,
273
+ outline,
274
+ edgeIndex: 0,
275
+ outward: [0, -1],
276
+ distance: 0.5,
277
+ elevation: 1.2,
278
+ })
279
+ expect(distances(dimensions)).toEqual([2.5, 3.5])
280
+ expect(dimensions[0]!.elevation).toBeCloseTo(1.2 + DIMENSION_FLOOR_LIFT)
281
+ })
282
+
283
+ test('a refused push still measures, from the edge where the arrow is', () => {
284
+ const dimensions = mezzanineEdgeDimensions({
285
+ rest,
286
+ outline: rest,
287
+ edgeIndex: 0,
288
+ outward: [0, -1],
289
+ distance: -1,
290
+ elevation: 1.2,
291
+ })
292
+ expect(distances(dimensions)).toEqual([1, 2])
293
+ })
294
+ })
295
+
296
+ describe('units', () => {
297
+ test('a dimension reads in the viewer’s units', () => {
298
+ const room = scene(
299
+ ring([
300
+ [0, 0],
301
+ [6, 0],
302
+ [6, 4],
303
+ [0, 4],
304
+ ]),
305
+ )
306
+ const [dimension] = measure(room, wallAt(room, [6, 0], [6, 4]), [1, 0], 0)
307
+ expect(formatLinearMeasurement(dimension!.distance, 'metric')).toBe('5.8m')
308
+ expect(formatLinearMeasurement(dimension!.distance, 'metric', 'millimeters')).toBe('5800mm')
309
+ expect(formatLinearMeasurement(dimension!.distance, 'imperial')).toBe(`19'0"`)
310
+ })
311
+ })