@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,72 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+ import { Ray, Vector3 } from 'three'
3
+ import { getSpatialPointerId, SpatialPointerInput } from './spatial-pointer-input'
4
+
5
+ describe('SpatialPointerInput', () => {
6
+ test('recognizes current and legacy XR native event shapes', () => {
7
+ const source = {}
8
+ expect(getSpatialPointerId({ inputSource: source })).toBe(source)
9
+ expect(getSpatialPointerId({ pointerState: { inputSource: source } })).toBe(source)
10
+ expect(getSpatialPointerId({ pointerType: 'mouse' })).toBeNull()
11
+ })
12
+
13
+ test('keeps move and release bound to the pointer that captured a handle', () => {
14
+ const input = new SpatialPointerInput()
15
+ const moves: Ray[] = []
16
+ let releases = 0
17
+ input.capture(42, {
18
+ onMove: (ray) => moves.push(ray.clone()),
19
+ onRelease: () => releases++,
20
+ onCancel: () => undefined,
21
+ })
22
+ const ray = new Ray(new Vector3(1, 2, 3), new Vector3(0, 1, 0))
23
+ expect(input.move(7, ray)).toBe(false)
24
+ expect(input.move(42, ray)).toBe(true)
25
+ expect(input.release(7)).toBe(false)
26
+ expect(input.release(42)).toBe(true)
27
+ expect(moves).toHaveLength(1)
28
+ expect(moves[0]?.origin.toArray()).toEqual([1, 2, 3])
29
+ expect(releases).toBe(1)
30
+ expect(input.move(42, ray)).toBe(false)
31
+ })
32
+
33
+ test('cancels a captured handle without releasing it', () => {
34
+ const input = new SpatialPointerInput()
35
+ let cancels = 0
36
+ let releases = 0
37
+
38
+ input.capture(9, {
39
+ onMove: () => undefined,
40
+ onRelease: () => releases++,
41
+ onCancel: () => cancels++,
42
+ })
43
+
44
+ expect(input.cancel(9)).toBe(true)
45
+ expect(cancels).toBe(1)
46
+ expect(releases).toBe(0)
47
+ })
48
+
49
+ test('cleans up a replaced capture without letting its stale disposer clear the replacement', () => {
50
+ const input = new SpatialPointerInput()
51
+ const ray = new Ray(new Vector3(), new Vector3(0, 0, -1))
52
+ let replacements = 0
53
+ let replacementMoves = 0
54
+
55
+ const disposeOriginal = input.capture(12, {
56
+ onMove: () => undefined,
57
+ onRelease: () => undefined,
58
+ onCancel: () => undefined,
59
+ onReplace: () => replacements++,
60
+ })
61
+ input.capture(12, {
62
+ onMove: () => replacementMoves++,
63
+ onRelease: () => undefined,
64
+ onCancel: () => undefined,
65
+ })
66
+
67
+ expect(replacements).toBe(1)
68
+ disposeOriginal()
69
+ expect(input.move(12, ray)).toBe(true)
70
+ expect(replacementMoves).toBe(1)
71
+ })
72
+ })
@@ -0,0 +1,62 @@
1
+ import type { Ray } from 'three'
2
+
3
+ type SpatialPointerCapture = {
4
+ onMove: (ray: Ray) => void
5
+ onRelease: () => void
6
+ onCancel: () => void
7
+ onReplace?: () => void
8
+ }
9
+
10
+ export type SpatialPointerId = object | number | string
11
+
12
+ export function getSpatialPointerId(nativeEvent: unknown): SpatialPointerId | null {
13
+ if (!nativeEvent || typeof nativeEvent !== 'object') return null
14
+ const event = nativeEvent as {
15
+ inputSource?: object
16
+ pointerState?: { inputSource?: object }
17
+ }
18
+ return event.inputSource ?? event.pointerState?.inputSource ?? null
19
+ }
20
+
21
+ export class SpatialPointerInput {
22
+ private readonly captures = new Map<SpatialPointerId, SpatialPointerCapture>()
23
+
24
+ capture(pointerId: SpatialPointerId, capture: SpatialPointerCapture): () => void {
25
+ const previous = this.captures.get(pointerId)
26
+ if (previous && previous !== capture) {
27
+ this.captures.delete(pointerId)
28
+ ;(previous.onReplace ?? previous.onCancel)()
29
+ }
30
+ this.captures.set(pointerId, capture)
31
+ return () => {
32
+ if (this.captures.get(pointerId) === capture) {
33
+ this.captures.delete(pointerId)
34
+ }
35
+ }
36
+ }
37
+
38
+ move(pointerId: SpatialPointerId, ray: Ray): boolean {
39
+ const capture = this.captures.get(pointerId)
40
+ if (!capture) return false
41
+ capture.onMove(ray)
42
+ return true
43
+ }
44
+
45
+ release(pointerId: SpatialPointerId): boolean {
46
+ const capture = this.captures.get(pointerId)
47
+ if (!capture) return false
48
+ this.captures.delete(pointerId)
49
+ capture.onRelease()
50
+ return true
51
+ }
52
+
53
+ cancel(pointerId: SpatialPointerId): boolean {
54
+ const capture = this.captures.get(pointerId)
55
+ if (!capture) return false
56
+ this.captures.delete(pointerId)
57
+ capture.onCancel()
58
+ return true
59
+ }
60
+ }
61
+
62
+ export const spatialPointerInput = new SpatialPointerInput()
@@ -0,0 +1,47 @@
1
+ import { test } from 'bun:test'
2
+ import assert from 'node:assert/strict'
3
+ import { snapLocalXZInWorld } from '@pascal-app/core'
4
+ import { Group, Vector3 } from 'three'
5
+
6
+ const close = (a: number, b: number) => assert.ok(Math.abs(a - b) < 1e-8)
7
+
8
+ test('free placement stays under the same hit through God-mode zoom and rotation', () => {
9
+ const pose = { position: [5, 0, -3] as [number, number, number], rotationY: 0.7 }
10
+ const world = new Group()
11
+ const building = new Group()
12
+ building.position.fromArray(pose.position)
13
+ building.rotation.y = pose.rotationY
14
+ world.add(building)
15
+ for (const scale of [0.01, 0.1, 1, 4]) {
16
+ world.scale.setScalar(scale)
17
+ world.position.set(8, 1, -4)
18
+ world.rotation.y = 1.2
19
+ world.updateWorldMatrix(true, true)
20
+ const hit = building.localToWorld(new Vector3(2, 0, -6))
21
+ const local = building.worldToLocal(hit.clone())
22
+ const snapped = snapLocalXZInWorld([local.x, local.z], pose, (value) => value)
23
+ const placed = building.localToWorld(new Vector3(snapped[0], 0, snapped[1]))
24
+ close(placed.distanceTo(hit), 0)
25
+ }
26
+ })
27
+
28
+ test('grid snapping still uses model units and the item footprint', () => {
29
+ const dimensions: number[] = []
30
+ const result = snapLocalXZInWorld([1.23, -2.16], null, (value, axis) => {
31
+ dimensions.push([1.8, 0.6][axis]!)
32
+ return Math.round(value / 0.1) * 0.1
33
+ })
34
+ close(result[0], 1.2)
35
+ close(result[1], -2.2)
36
+ assert.deepEqual(dimensions, [1.8, 0.6])
37
+ })
38
+
39
+ test('rotated buildings snap in model-world axes then return building-local coordinates', () => {
40
+ const result = snapLocalXZInWorld(
41
+ [1.2, 2.3],
42
+ { position: [10, 0, 20], rotationY: Math.PI / 2 },
43
+ Math.round,
44
+ )
45
+ close(result[0], 1)
46
+ close(result[1], 2)
47
+ })
@@ -0,0 +1,145 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
2
+ import {
3
+ type AnyNode,
4
+ BuildingNode,
5
+ clearSceneHistory,
6
+ createZone,
7
+ generateId,
8
+ initSpaceDetectionSync,
9
+ LevelNode,
10
+ useScene,
11
+ type ZoneNode,
12
+ } from '@pascal-app/core'
13
+ import { useViewer } from '@pascal-app/viewer'
14
+ import useEditor from '../store/use-editor'
15
+ import useInteractionScope from '../store/use-interaction-scope'
16
+ import {
17
+ moveFloorRegion,
18
+ pressFloorRegion,
19
+ releaseFloorRegion,
20
+ useFloorRegionDraft,
21
+ } from './floor-region-session'
22
+ import type { FloorRegionPoint, FloorRegionSnapSettings } from './floor-region-snap'
23
+ import { liveGestureKinds } from './gesture-lifecycle'
24
+ import { applyRoomPlan } from './room-structure-commands'
25
+ import {
26
+ cancelTerraceDraft,
27
+ planTerrace,
28
+ startTerraceDraft,
29
+ useTerraceDraft,
30
+ } from './terrace-draft'
31
+
32
+ const LEVEL = 'level_terrace'
33
+ const OFF: FloorRegionSnapSettings = { mode: 'off', step: 0.5 }
34
+ let stop = () => {}
35
+ const nodes = () => useScene.getState().nodes as Record<string, AnyNode>
36
+ const history = () => useScene.temporal.getState().pastStates.length
37
+ const zones = () =>
38
+ Object.values(nodes()).filter((n): n is ZoneNode => n.type === 'zone' && n.spaceRole === 'room')
39
+
40
+ function box(from: FloorRegionPoint, to: FloorRegionPoint) {
41
+ const host = useTerraceDraft.getState().host!
42
+ pressFloorRegion('rectangle', host, from, OFF, 0.2)
43
+ moveFloorRegion(to, OFF, 0.2)
44
+ releaseFloorRegion(0.2)
45
+ }
46
+
47
+ beforeEach(() => {
48
+ globalThis.requestAnimationFrame ??= (callback) => {
49
+ callback(0)
50
+ return 0
51
+ }
52
+ globalThis.cancelAnimationFrame ??= () => {}
53
+ useInteractionScope.getState().end()
54
+ const building = BuildingNode.parse({ id: 'building_terrace', children: [LEVEL] })
55
+ const level = LevelNode.parse({ id: LEVEL, parentId: building.id })
56
+ useScene.setState({
57
+ nodes: { [building.id]: building, [level.id]: level },
58
+ rootNodeIds: [building.id],
59
+ dirtyNodes: new Set(),
60
+ materials: {},
61
+ collections: {},
62
+ readOnly: false,
63
+ })
64
+ useScene.temporal.getState().resume()
65
+ stop = initSpaceDetectionSync(useScene, { getState: () => ({ spaces: {}, setSpaces: () => {} }) })
66
+ applyRoomPlan(
67
+ createZone(nodes(), {
68
+ levelId: LEVEL,
69
+ polygon: [
70
+ [0, 0],
71
+ [6, 0],
72
+ [6, 5],
73
+ [0, 5],
74
+ ],
75
+ enclose: true,
76
+ name: 'Living room',
77
+ mintId: generateId,
78
+ }),
79
+ )
80
+ useViewer.getState().setSelection({ buildingId: building.id, levelId: LEVEL, selectedIds: [] })
81
+ useEditor.setState({ phase: 'building', mode: 'select', room: null })
82
+ useFloorRegionDraft.setState({ draft: null, hover: null })
83
+ clearSceneHistory()
84
+ })
85
+ afterEach(() => {
86
+ cancelTerraceDraft()
87
+ stop()
88
+ stop = () => {}
89
+ })
90
+
91
+ describe('terrace tool', () => {
92
+ test('a terrace is an outdoor room: separators where no wall runs, no ceiling', () => {
93
+ const walls = Object.values(nodes()).filter((n) => n.type === 'wall').length
94
+ const result = planTerrace(nodes(), LEVEL, [
95
+ [6, 0],
96
+ [10, 0],
97
+ [10, 5],
98
+ [6, 5],
99
+ ])
100
+ expect('plan' in result).toBe(true)
101
+ if (!('plan' in result)) return
102
+ applyRoomPlan(result.plan)
103
+ const terrace = nodes()[result.plan.zoneId] as ZoneNode
104
+ expect(terrace).toMatchObject({ name: 'Terrace', spaceRole: 'room', hasCeiling: false })
105
+ expect(Object.values(nodes()).filter((n) => n.type === 'wall')).toHaveLength(walls)
106
+ // Three new sides are separators; the fourth is the house wall it shares.
107
+ expect(Object.values(nodes()).filter((n) => n.type === 'separator')).toHaveLength(3)
108
+ expect(
109
+ Object.values(nodes()).some((n) => n.type === 'ceiling' && n.zoneId === terrace.id),
110
+ ).toBe(false)
111
+ })
112
+
113
+ test('refuses an outline over a room', () => {
114
+ expect(
115
+ planTerrace(nodes(), LEVEL, [
116
+ [4, 1],
117
+ [8, 1],
118
+ [8, 3],
119
+ [4, 3],
120
+ ]),
121
+ ).toEqual({ message: 'The terrace overlaps Living room.' })
122
+ })
123
+
124
+ test('draws like the room tools: each outline one undo step, the tool stays armed', () => {
125
+ expect(startTerraceDraft(LEVEL)).toBe(true)
126
+ expect(liveGestureKinds()).toEqual(['terrace-draft'])
127
+ box([7, 0], [11, 4])
128
+ expect(
129
+ zones()
130
+ .map((zone) => zone.name)
131
+ .sort(),
132
+ ).toEqual(['Living room', 'Terrace'])
133
+ expect(history()).toBe(1)
134
+ expect(useTerraceDraft.getState().host).not.toBeNull()
135
+ // An outline over the house is refused in place; nothing is written.
136
+ box([2, 2], [4, 4])
137
+ expect(useFloorRegionDraft.getState().draft).toMatchObject({
138
+ refusal: 'The terrace overlaps Living room.',
139
+ })
140
+ expect(history()).toBe(1)
141
+ cancelTerraceDraft()
142
+ expect(useTerraceDraft.getState().host).toBeNull()
143
+ expect(useInteractionScope.getState().scope.kind).toBe('idle')
144
+ })
145
+ })
@@ -0,0 +1,213 @@
1
+ import {
2
+ type AnyNode,
3
+ type AnyNodeId,
4
+ area,
5
+ createZone,
6
+ generateId,
7
+ type MultiPolygon,
8
+ type StructurePlan,
9
+ type ToolHint,
10
+ useScene,
11
+ } from '@pascal-app/core'
12
+ import { useViewer } from '@pascal-app/viewer'
13
+ import { create } from 'zustand'
14
+ import useEditor from '../store/use-editor'
15
+ import { CROSSING_MESSAGE, floorRegionSelfIntersects } from './floor-region-geometry'
16
+ import {
17
+ cancelFloorRegion,
18
+ type FloorDraftCommit,
19
+ type FloorDraftPurpose,
20
+ type FloorRegionRoom,
21
+ useFloorRegionDraft,
22
+ } from './floor-region-session'
23
+ import { type FloorRegionPoint, floorRegionSnapTargets } from './floor-region-snap'
24
+ import { beginGesture, type GestureHandle } from './gesture-lifecycle'
25
+ import { applyRoomPlan } from './room-structure-commands'
26
+ import { sfxEmitter } from './sfx-bus'
27
+
28
+ // "Terrace" (Build panel → Outdoor): draws an outdoor room on the level's
29
+ // ground, rectangle or polygon, snapped to the rooms already there and the
30
+ // grid. The terrace is a room like any other — bounded by separators (and by
31
+ // the walls it shares), no walls of its own and no ceiling — so it gets
32
+ // everything rooms have: finish paint, height arrows, Divide, Move, Delete.
33
+ // The tool is one lifecycle gesture from `startTerraceDraft` until cancelled;
34
+ // each outline commits as one undo step and the tool stays armed, like the
35
+ // room tools.
36
+
37
+ export const TERRACE_DRAFT_HANDLE = 'terrace-draft'
38
+ export const TERRACE_NAME = 'Terrace'
39
+ export const TERRACE_TOO_SMALL_MESSAGE = 'Too small for a terrace.'
40
+ export const TERRACE_FAILED_MESSAGE = "Can't place a terrace here."
41
+
42
+ export type TerraceShape = 'rectangle' | 'polygon'
43
+
44
+ export const useTerraceDraft = create<{
45
+ host: FloorRegionRoom | null
46
+ shape: TerraceShape
47
+ }>(() => ({ host: null, shape: 'rectangle' }))
48
+
49
+ /** How far the terrace tool reaches from the origin when the site has no outline. */
50
+ const OPEN_GROUND = 500
51
+
52
+ /**
53
+ * Why a terrace outline cannot be built, or its plan: an outdoor room of the
54
+ * outline, closed with separators where no wall runs, without a ceiling.
55
+ * Core refuses an outline over a room (the same rule MCP's outdoor
56
+ * `create_room` follows) and names the room.
57
+ */
58
+ export function planTerrace(
59
+ nodes: Record<string, AnyNode>,
60
+ levelId: string,
61
+ polygon: readonly FloorRegionPoint[],
62
+ ): { plan: StructurePlan & { zoneId: string } } | { message: string } {
63
+ const outline = polygon.map(([x, z]): [number, number] => [x, z])
64
+ if (floorRegionSelfIntersects(outline)) return { message: CROSSING_MESSAGE }
65
+ if (area([{ outer: outline, holes: [] }]) < 1) return { message: TERRACE_TOO_SMALL_MESSAGE }
66
+ try {
67
+ const plan = createZone(nodes, {
68
+ levelId,
69
+ polygon: outline,
70
+ name: TERRACE_NAME,
71
+ enclose: false,
72
+ intent: { hasCeiling: false },
73
+ mintId: generateId,
74
+ })
75
+ const conflict = plan.conflicts?.[0]
76
+ return conflict ? { message: conflict.message || TERRACE_FAILED_MESSAGE } : { plan }
77
+ } catch {
78
+ return { message: TERRACE_FAILED_MESSAGE }
79
+ }
80
+ }
81
+
82
+ let tool: GestureHandle | null = null
83
+
84
+ function reset() {
85
+ if (useTerraceDraft.getState().host) useTerraceDraft.setState({ host: null })
86
+ const { draft } = useFloorRegionDraft.getState()
87
+ if (draft?.room.purpose === TERRACE) useFloorRegionDraft.setState({ draft: null, hover: null })
88
+ }
89
+
90
+ function commitTerrace(
91
+ host: FloorRegionRoom,
92
+ polygon: readonly FloorRegionPoint[],
93
+ ): FloorDraftCommit {
94
+ const result = planTerrace(useScene.getState().nodes, host.levelId, polygon)
95
+ if ('message' in result) return { ok: false, message: result.message }
96
+ applyRoomPlan(result.plan)
97
+ sfxEmitter.emit('sfx:structure-build')
98
+ // The rooms (and this terrace) now snap the next outline.
99
+ useTerraceDraft.setState({ host: describeTerraceHost(useScene.getState().nodes, host.levelId) })
100
+ return { ok: true }
101
+ }
102
+
103
+ function beginTool(host: FloorRegionRoom) {
104
+ const handle = beginGesture({
105
+ kind: 'terrace-draft',
106
+ scope: { kind: 'handle-drag', nodeId: host.levelId, handle: TERRACE_DRAFT_HANDLE },
107
+ stale: () =>
108
+ useScene.getState().readOnly || !useScene.getState().nodes[host.levelId as AnyNodeId],
109
+ onCancel: () => {
110
+ if (tool === handle) tool = null
111
+ reset()
112
+ },
113
+ })
114
+ tool = handle
115
+ useTerraceDraft.setState({ host })
116
+ return handle
117
+ }
118
+
119
+ const TERRACE: FloorDraftPurpose = {
120
+ persistent: true,
121
+ own: (room) => (tool?.active ? tool : beginTool(room)),
122
+ commit: (room, polygon) => {
123
+ const current = useTerraceDraft.getState().host ?? room
124
+ return commitTerrace(current, polygon)
125
+ },
126
+ fill: '#818cf8',
127
+ }
128
+
129
+ /** The level's ground as a draft room: the site (or open ground), snapping to the rooms on it. */
130
+ export function describeTerraceHost(
131
+ nodes: Record<string, AnyNode>,
132
+ levelId: string,
133
+ ): FloorRegionRoom | null {
134
+ const level = nodes[levelId]
135
+ if (level?.type !== 'level') return null
136
+ const site = Object.values(nodes).find((node) => node.type === 'site')
137
+ const outline =
138
+ site?.type === 'site' && site.polygon?.points?.length && site.polygon.points.length >= 3
139
+ ? site.polygon.points.map(([x, z]): [number, number] => [x, z])
140
+ : ([
141
+ [-OPEN_GROUND, -OPEN_GROUND],
142
+ [OPEN_GROUND, -OPEN_GROUND],
143
+ [OPEN_GROUND, OPEN_GROUND],
144
+ [-OPEN_GROUND, OPEN_GROUND],
145
+ ] as [number, number][])
146
+ const clear: MultiPolygon = [{ outer: outline, holes: [] }]
147
+ const rooms = Object.values(nodes).flatMap((node) =>
148
+ node.type === 'zone' && node.parentId === levelId && node.spaceRole === 'room'
149
+ ? [node.polygon]
150
+ : [],
151
+ )
152
+ return {
153
+ zoneId: '',
154
+ levelId,
155
+ clear,
156
+ elevation: 0,
157
+ angle: 0,
158
+ targets: floorRegionSnapTargets([], rooms),
159
+ purpose: TERRACE,
160
+ }
161
+ }
162
+
163
+ /** Arms the terrace tool on the current level (select mode, so nothing else draws). */
164
+ export function startTerraceDraft(levelId = useViewer.getState().selection.levelId): boolean {
165
+ if (!levelId || useScene.getState().readOnly) return false
166
+ const host = describeTerraceHost(useScene.getState().nodes, levelId)
167
+ if (!host) return false
168
+ cancelTerraceDraft()
169
+ cancelFloorRegion()
170
+ const editor = useEditor.getState()
171
+ if (editor.phase === 'site') editor.setPhase('building')
172
+ if (editor.mode !== 'select') editor.setMode('select')
173
+ if (editor.room) editor.clearRoom()
174
+ useViewer.getState().setSelection({ selectedIds: [] })
175
+ beginTool(host)
176
+ return true
177
+ }
178
+
179
+ export function cancelTerraceDraft() {
180
+ const current = tool
181
+ tool = null
182
+ if (current?.active) current.cancel()
183
+ else reset()
184
+ }
185
+
186
+ export function terraceDraftActive() {
187
+ return !!useTerraceDraft.getState().host
188
+ }
189
+
190
+ export function setTerraceShape(shape: TerraceShape) {
191
+ if (useTerraceDraft.getState().shape === shape) return
192
+ const { draft } = useFloorRegionDraft.getState()
193
+ if (draft?.room.purpose === TERRACE) useFloorRegionDraft.setState({ draft: null })
194
+ useTerraceDraft.setState({ shape })
195
+ }
196
+
197
+ export function toggleTerraceShape() {
198
+ setTerraceShape(useTerraceDraft.getState().shape === 'rectangle' ? 'polygon' : 'rectangle')
199
+ }
200
+
201
+ /** The HUD's shape chip (T). */
202
+ export const TERRACE_SHAPE_HINT: ToolHint = {
203
+ key: 'T',
204
+ label: 'Shape',
205
+ chip: {
206
+ subscribe: (onChange) => useTerraceDraft.subscribe(onChange),
207
+ value: () => useTerraceDraft.getState().shape,
208
+ cycle: toggleTerraceShape,
209
+ labels: { rectangle: 'Shape: Rectangle', polygon: 'Shape: Polygon' },
210
+ icons: { rectangle: 'lucide:square', polygon: 'lucide:pentagon' },
211
+ tooltip: 'Outline shape — click or press T to switch',
212
+ },
213
+ }
@@ -0,0 +1,117 @@
1
+ 'use client'
2
+
3
+ import { type SiteNode, type TerrainVerb, useScene } from '@pascal-app/core'
4
+ import useEditor from '../store/use-editor'
5
+ import type { PanelRow } from './panel-rows'
6
+ import { brushRadiusRange, flattenSite, resetSiteTerrain } from './terrain-sculpt'
7
+
8
+ const TERRAIN_VERBS: TerrainVerb[] = ['raise', 'lower', 'flatten', 'smooth']
9
+
10
+ export function useTerrainPanelRows(): PanelRow[] {
11
+ const verb = useEditor((state) => state.terrainVerb)
12
+ const setVerb = useEditor((state) => state.setTerrainVerb)
13
+ const brush = useEditor((state) => state.terrainBrush)
14
+ const setBrush = useEditor((state) => state.setTerrainBrush)
15
+ const flattenTarget = useEditor((state) => state.terrainFlattenTarget)
16
+ const setFlattenTarget = useEditor((state) => state.setTerrainFlattenTarget)
17
+ const sampling = useEditor((state) => state.terrainSampling)
18
+ const setSampling = useEditor((state) => state.setTerrainSampling)
19
+ const site = useScene((state) => {
20
+ const root = state.rootNodeIds[0]
21
+ const node = root ? state.nodes[root] : undefined
22
+ return node?.type === 'site' ? (node as SiteNode) : null
23
+ })
24
+ const [minRadius, maxRadius] = brushRadiusRange(site)
25
+ const verbIndex = TERRAIN_VERBS.indexOf(verb)
26
+ const cycleVerb = (direction: -1 | 1) => {
27
+ const next =
28
+ TERRAIN_VERBS[(verbIndex + direction + TERRAIN_VERBS.length) % TERRAIN_VERBS.length]
29
+ if (next) setVerb(next)
30
+ }
31
+
32
+ const rows: PanelRow[] = [
33
+ {
34
+ id: 'terrain-verb',
35
+ kind: 'cycle',
36
+ label: 'Brush',
37
+ next: () => cycleVerb(1),
38
+ previous: () => cycleVerb(-1),
39
+ value: verb.charAt(0).toUpperCase() + verb.slice(1),
40
+ },
41
+ {
42
+ id: 'terrain-radius',
43
+ kind: 'stepper',
44
+ label: 'Size',
45
+ max: maxRadius,
46
+ min: minRadius,
47
+ onChange: (radius) => setBrush({ radius }),
48
+ step: 0.5,
49
+ unit: 'm',
50
+ value: Math.max(minRadius, Math.min(maxRadius, brush.radius)),
51
+ },
52
+ {
53
+ id: 'terrain-strength',
54
+ kind: 'stepper',
55
+ label: 'Strength',
56
+ max: 1,
57
+ min: 0.05,
58
+ onChange: (strength) => setBrush({ strength }),
59
+ step: 0.05,
60
+ value: brush.strength,
61
+ },
62
+ {
63
+ id: 'terrain-softness',
64
+ kind: 'stepper',
65
+ label: 'Softness',
66
+ max: 1,
67
+ min: 0,
68
+ onChange: (falloff) => setBrush({ falloff }),
69
+ step: 0.05,
70
+ value: brush.falloff,
71
+ },
72
+ {
73
+ id: 'terrain-shape',
74
+ kind: 'choice',
75
+ label: 'Shape',
76
+ onSelect: () => setBrush({ shape: brush.shape === 'round' ? 'square' : 'round' }),
77
+ value: brush.shape === 'round' ? 'Round' : 'Square',
78
+ },
79
+ ...(verb === 'flatten'
80
+ ? ([
81
+ {
82
+ id: 'terrain-target',
83
+ kind: 'stepper',
84
+ label: 'Target',
85
+ max: 50,
86
+ min: -50,
87
+ onChange: setFlattenTarget,
88
+ step: 0.1,
89
+ unit: 'm',
90
+ value: flattenTarget ?? 0,
91
+ },
92
+ {
93
+ id: 'terrain-sampling',
94
+ kind: 'choice',
95
+ label: 'Pick height',
96
+ onSelect: () => setSampling(!sampling),
97
+ value: sampling ? 'Armed' : 'Off',
98
+ },
99
+ ] satisfies PanelRow[])
100
+ : []),
101
+ {
102
+ id: 'terrain-level-lot',
103
+ kind: 'choice',
104
+ label: 'Whole lot',
105
+ onSelect: site ? () => flattenSite(site, flattenTarget ?? 0) : undefined,
106
+ value: 'Level',
107
+ },
108
+ {
109
+ id: 'terrain-clear',
110
+ kind: 'choice',
111
+ label: 'Terrain',
112
+ onSelect: site?.terrain ? () => resetSiteTerrain(site) : undefined,
113
+ value: site?.terrain ? 'Clear' : 'Empty',
114
+ },
115
+ ]
116
+ return rows
117
+ }
@@ -0,0 +1,16 @@
1
+ import { expect, test } from 'bun:test'
2
+ import { wallCursorIcon } from './tool-cursor-icon'
3
+ import { getWallDrawVariantInfo, WALL_DRAW_VARIANTS } from './wall-draw-variant'
4
+
5
+ test('the wall cursor bubble shows the Rooms tile icon of the variant in hand', () => {
6
+ for (const variant of WALL_DRAW_VARIANTS) {
7
+ expect(wallCursorIcon(variant.mode)).toEqual({
8
+ label: getWallDrawVariantInfo(variant.id).title,
9
+ iconSrc: variant.iconSrc,
10
+ })
11
+ }
12
+ expect(
13
+ new Set(WALL_DRAW_VARIANTS.map((variant) => wallCursorIcon(variant.mode).iconSrc)).size,
14
+ ).toBe(3)
15
+ expect(getWallDrawVariantInfo('walls').iconSrc).toBe('/icons/wall.webp')
16
+ })
@@ -0,0 +1,11 @@
1
+ import type { ContinuationMode } from './continuation'
2
+ import { getWallDrawVariantInfo, wallDrawVariantOf } from './wall-draw-variant'
3
+
4
+ /**
5
+ * The cursor bubble's icon for the wall tool: the Rooms variant in hand
6
+ * (Rectangle / Polygon / Walls), the same art as the Build panel tile.
7
+ */
8
+ export function wallCursorIcon(mode: ContinuationMode): { label: string; iconSrc: string } {
9
+ const variant = getWallDrawVariantInfo(wallDrawVariantOf(mode))
10
+ return { label: variant.title, iconSrc: variant.iconSrc }
11
+ }