@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,179 @@
1
+ import { afterAll, afterEach, beforeEach, expect, test } from 'bun:test'
2
+ import {
3
+ type AnyNode,
4
+ type AnyNodeId,
5
+ area,
6
+ CeilingNode,
7
+ clearSceneHistory,
8
+ LevelNode,
9
+ useScene,
10
+ ZoneNode,
11
+ } from '@pascal-app/core'
12
+ import useEditor from '../store/use-editor'
13
+ import {
14
+ describeCeilingRegionRoom,
15
+ floorRegionRoomGone,
16
+ moveFloorRegion,
17
+ PICK_MATERIAL_MESSAGE,
18
+ pressFloorRegion,
19
+ releaseFloorRegion,
20
+ useFloorRegionDraft,
21
+ } from './floor-region-session'
22
+ import type { FloorRegionPoint } from './floor-region-snap'
23
+ import { paintRegionHoverHint, usePaintRegionMode } from './paint-region-mode'
24
+
25
+ // Rectangle and Polygon on a ceiling: the floor's outline gesture, drawn on the
26
+ // ceiling's underside, committing one region to the room (an automatic
27
+ // ceiling) or to the ceiling (a manual one) in one undo step.
28
+
29
+ const square = (x: number, z: number, size: number): [number, number][] => [
30
+ [x, z],
31
+ [x + size, z],
32
+ [x + size, z + size],
33
+ [x, z + size],
34
+ ]
35
+ const OFF = { mode: 'off', step: 0.5 } as const
36
+ const CLOSE = 0.2
37
+
38
+ const level = LevelNode.parse({ id: 'level_ceiling_region' })
39
+ const zone = ZoneNode.parse({
40
+ id: 'zone_ceiling_region',
41
+ name: 'Room',
42
+ parentId: level.id,
43
+ polygon: square(0, 0, 4),
44
+ floor: { regions: [{ id: 'rug', polygon: square(1, 1, 1), finish: 'library:tile' }] },
45
+ })
46
+ // A floor opening cut the room's ceiling in two; both parts are the room's.
47
+ const west = CeilingNode.parse({
48
+ id: 'ceiling_west',
49
+ parentId: level.id,
50
+ polygon: [
51
+ [0, 0],
52
+ [2, 0],
53
+ [2, 4],
54
+ [0, 4],
55
+ ],
56
+ boundary: 'auto',
57
+ zoneId: zone.id,
58
+ height: 2.5,
59
+ })
60
+ const east = CeilingNode.parse({
61
+ ...west,
62
+ id: 'ceiling_east',
63
+ polygon: [
64
+ [2.5, 0],
65
+ [4, 0],
66
+ [4, 4],
67
+ [2.5, 4],
68
+ ],
69
+ })
70
+ const manual = CeilingNode.parse({
71
+ id: 'ceiling_manual',
72
+ parentId: level.id,
73
+ polygon: square(6, 0, 2),
74
+ height: 2.2,
75
+ })
76
+ const nodes = Object.fromEntries(
77
+ [level, zone, west, east, manual].map((node) => [node.id, node]),
78
+ ) as Record<string, AnyNode>
79
+
80
+ const stubbed: string[] = []
81
+ const before = useScene.getState()
82
+ const node = <T extends AnyNode>(id: string) => useScene.getState().nodes[id as AnyNodeId] as T
83
+ const history = () => useScene.temporal.getState().pastStates.length
84
+
85
+ function drag(ceiling: CeilingNode, from: FloorRegionPoint, to: FloorRegionPoint) {
86
+ const room = describeCeilingRegionRoom(useScene.getState().nodes, ceiling)
87
+ pressFloorRegion('rectangle', room, from, OFF, CLOSE)
88
+ moveFloorRegion(to, OFF, CLOSE)
89
+ releaseFloorRegion(CLOSE)
90
+ }
91
+
92
+ beforeEach(() => {
93
+ globalThis.requestAnimationFrame ??= (callback) => {
94
+ callback(0)
95
+ return 0
96
+ }
97
+ globalThis.cancelAnimationFrame ??= () => {}
98
+ if (!globalThis.window) {
99
+ globalThis.window = new EventTarget() as Window & typeof globalThis
100
+ stubbed.push('window')
101
+ }
102
+ useScene.setState({ nodes, materials: {}, readOnly: false })
103
+ useScene.temporal.getState().resume()
104
+ clearSceneHistory()
105
+ useEditor.setState({
106
+ mode: 'material-paint',
107
+ activePaintMaterial: { materialPreset: 'library:blue', sourceTarget: 'ceiling' } as never,
108
+ })
109
+ usePaintRegionMode.getState().setMode('rectangle')
110
+ useFloorRegionDraft.setState({ draft: null, hover: null })
111
+ })
112
+ afterEach(() => {
113
+ useScene.setState(before)
114
+ usePaintRegionMode.getState().setMode('surface')
115
+ useFloorRegionDraft.setState({ draft: null, hover: null })
116
+ useEditor.setState({ mode: 'select', activePaintMaterial: null })
117
+ })
118
+ afterAll(async () => {
119
+ await new Promise((resolve) => setTimeout(resolve, 50))
120
+ for (const key of stubbed.splice(0)) delete (globalThis as Record<string, unknown>)[key]
121
+ })
122
+
123
+ test('an automatic ceiling draws on every part of its room, at the plane it draws at', () => {
124
+ const room = describeCeilingRegionRoom(nodes, west)
125
+ expect(room).toMatchObject({ zoneId: zone.id, ceilingId: west.id, levelId: level.id })
126
+ expect(room.elevation).toBeCloseTo(2.49)
127
+ expect(area(room.clear)).toBeCloseTo(8 + 6)
128
+ expect(describeCeilingRegionRoom(nodes, manual)).toMatchObject({ zoneId: manual.id })
129
+ })
130
+
131
+ test('a rectangle on a room ceiling paints one room region, clipped to the ceiling', () => {
132
+ drag(west, [1, 1], [3, 3])
133
+ const regions = node<ZoneNode>(zone.id).ceiling?.regions ?? []
134
+ expect(regions).toHaveLength(1)
135
+ expect(regions[0]!.finish).toBe('library:blue')
136
+ // Across the opening gap: 1 × 2 on the west part, 0.5 × 2 on the east part.
137
+ expect(area([{ outer: regions[0]!.polygon, holes: [] }])).toBeGreaterThan(2.9)
138
+ expect(history()).toBe(1)
139
+ // The floor is untouched.
140
+ expect(node<ZoneNode>(zone.id).floor).toEqual(zone.floor)
141
+ expect(useFloorRegionDraft.getState().draft).toBeNull()
142
+ })
143
+
144
+ test('a polygon on a manual ceiling paints the ceiling itself', () => {
145
+ usePaintRegionMode.getState().setMode('polygon')
146
+ const room = describeCeilingRegionRoom(useScene.getState().nodes, manual)
147
+ for (const point of [
148
+ [6.2, 0.2],
149
+ [7.5, 0.2],
150
+ [7.5, 1.5],
151
+ [6.2, 0.2],
152
+ ] as FloorRegionPoint[]) {
153
+ pressFloorRegion('polygon', room, point, OFF, CLOSE)
154
+ releaseFloorRegion(CLOSE)
155
+ }
156
+ expect(node<CeilingNode>(manual.id).regions).toHaveLength(1)
157
+ expect(node<ZoneNode>(zone.id).ceiling).toBeUndefined()
158
+ expect(history()).toBe(1)
159
+ })
160
+
161
+ test('outside the ceiling, or with nothing to paint with, nothing is written', () => {
162
+ drag(manual, [10, 10], [12, 12])
163
+ expect(node<CeilingNode>(manual.id).regions).toBeUndefined()
164
+ useEditor.setState({ activePaintMaterial: null })
165
+ drag(manual, [6.5, 0.5], [7.5, 1.5])
166
+ expect(node<CeilingNode>(manual.id).regions).toBeUndefined()
167
+ expect(usePaintRegionMode.getState().notice).toBe(PICK_MATERIAL_MESSAGE)
168
+ expect(history()).toBe(0)
169
+ })
170
+
171
+ test('a ceiling draft ends when its ceiling goes; the hints name ceilings', () => {
172
+ const room = describeCeilingRegionRoom(nodes, manual)
173
+ pressFloorRegion('rectangle', room, [6.5, 0.5], OFF, CLOSE)
174
+ const { [manual.id]: _, ...rest } = nodes
175
+ expect(floorRegionRoomGone(rest)).toBe(true)
176
+ expect(floorRegionRoomGone(nodes)).toBe(false)
177
+ expect(paintRegionHoverHint('polygon')).toBe('Hover a floor or ceiling')
178
+ expect(paintRegionHoverHint('rectangle')).toBe('Hover a wall, floor or ceiling')
179
+ })
@@ -1,5 +1,14 @@
1
1
  import { describe, expect, test } from 'bun:test'
2
- import { resolveSelectModeHelpHints } from './contextual-help'
2
+ import { resolveRotateHandleHelpHints, resolveSelectModeHelpHints } from './contextual-help'
3
+
4
+ test('rotation advertises Alt as the temporary free-rotation override', () => {
5
+ expect(resolveRotateHandleHelpHints(false)).toEqual([
6
+ { keys: ['Alt'], label: 'Hold to rotate freely', active: false },
7
+ ])
8
+ expect(resolveRotateHandleHelpHints(true)).toEqual([
9
+ { keys: ['Alt'], label: 'Rotating freely (no angle step)', active: true },
10
+ ])
11
+ })
3
12
 
4
13
  describe('resolveSelectModeHelpHints', () => {
5
14
  test('stays hidden in idle select mode with no selection', () => {
@@ -117,4 +126,21 @@ describe('resolveSelectModeHelpHints', () => {
117
126
  },
118
127
  ])
119
128
  })
129
+
130
+ test('shows the single-side radius hint for rounded openings', () => {
131
+ expect(
132
+ resolveSelectModeHelpHints({
133
+ selectedCount: 1,
134
+ hasMovableSelection: true,
135
+ hasRotatableSelection: false,
136
+ hasOpeningRadiusSelection: true,
137
+ commandPressed: false,
138
+ shiftPressed: false,
139
+ }),
140
+ ).toContainEqual({
141
+ keys: ['Shift'],
142
+ label: 'Adjust one side’s corner radius',
143
+ active: false,
144
+ })
145
+ })
120
146
  })
@@ -11,8 +11,8 @@ export type ContextualShortcutHint = {
11
11
  }
12
12
 
13
13
  // `activeHandleDrag.label` value a rotate gizmo sets while dragging, so the
14
- // contextual HUD can surface the Shift = free-rotation toggle for the duration
15
- // (mirrors how wall drafting advertises Shift). Distinct from resize handles,
14
+ // contextual HUD can surface the rotation snapping mode for the duration.
15
+ // Distinct from resize handles,
16
16
  // which route their own measurement label here.
17
17
  export const ROTATE_HANDLE_DRAG_LABEL = 'rotate-handle'
18
18
 
@@ -29,15 +29,12 @@ export const RESIZE_HANDLE_DRAG_LABEL = 'resize-handle'
29
29
  export const GROUP_MOVE_DRAG_LABEL = 'group-move-handle'
30
30
  export const GROUP_ROTATE_DRAG_LABEL = 'group-rotate-handle'
31
31
 
32
- // Hints shown while a rotate gizmo is mid-drag: Shift bypasses the angle step
33
- // (free rotation), the same toggle wall drafting exposes. `active` lights the
34
- // pill while Shift is held.
35
- export function resolveRotateHandleHelpHints(shiftPressed: boolean): ContextualShortcutHint[] {
32
+ export function resolveRotateHandleHelpHints(altPressed: boolean): ContextualShortcutHint[] {
36
33
  return [
37
34
  {
38
- keys: [SHIFT_KEY],
39
- label: shiftPressed ? 'Rotating freely (no angle step)' : 'Hold to rotate freely',
40
- active: shiftPressed,
35
+ keys: [ALT_KEY],
36
+ label: altPressed ? 'Rotating freely (no angle step)' : 'Hold to rotate freely',
37
+ active: altPressed,
41
38
  },
42
39
  ]
43
40
  }
@@ -46,6 +43,7 @@ export type SelectModeHelpContext = {
46
43
  selectedCount: number
47
44
  hasMovableSelection: boolean
48
45
  hasRotatableSelection: boolean
46
+ hasOpeningRadiusSelection?: boolean
49
47
  commandPressed: boolean
50
48
  shiftPressed: boolean
51
49
  // When a single MEP node is selected its in-world handle rig (click a dot to
@@ -67,6 +65,7 @@ export function resolveSelectModeHelpHints({
67
65
  selectedCount,
68
66
  hasMovableSelection,
69
67
  hasRotatableSelection,
68
+ hasOpeningRadiusSelection = false,
70
69
  commandPressed,
71
70
  shiftPressed,
72
71
  mepSelection = null,
@@ -142,6 +141,14 @@ export function resolveSelectModeHelpHints({
142
141
  })
143
142
  }
144
143
 
144
+ if (hasOpeningRadiusSelection) {
145
+ hints.push({
146
+ keys: [SHIFT_KEY],
147
+ label: 'Adjust one side’s corner radius',
148
+ active: shiftPressed,
149
+ })
150
+ }
151
+
145
152
  hints.push({
146
153
  keys: [[COMMAND_KEY, SHIFT_KEY], LEFT_CLICK],
147
154
  label: 'Add or remove objects from the selection',
@@ -1,5 +1,10 @@
1
1
  import { describe, expect, test } from 'bun:test'
2
- import { CONTINUATION_PROFILES, continuationContextOf, nextContinuation } from './continuation'
2
+ import {
3
+ CONTINUATION_PROFILES,
4
+ continuationContextOf,
5
+ keyCyclableContinuationContext,
6
+ nextContinuation,
7
+ } from './continuation'
3
8
 
4
9
  describe('canopy continuation', () => {
5
10
  test('maps the canopy tool to its own single and continuous profile', () => {
@@ -9,3 +14,16 @@ describe('canopy continuation', () => {
9
14
  expect(nextContinuation('canopy', 'continuous')).toBe('single')
10
15
  })
11
16
  })
17
+
18
+ describe('wall drawing mode', () => {
19
+ test('is picked in the Build panel, never cycled by C or a HUD chip', () => {
20
+ expect(CONTINUATION_PROFILES.wall.options).toEqual(['room', 'single', 'rectangle'])
21
+ expect(keyCyclableContinuationContext('wall')).toBeNull()
22
+ })
23
+
24
+ test('other contexts still cycle from the keyboard', () => {
25
+ expect(keyCyclableContinuationContext('fence')).toBe('fence')
26
+ expect(keyCyclableContinuationContext('point')).toBe('point')
27
+ expect(keyCyclableContinuationContext(null)).toBeNull()
28
+ })
29
+ })
@@ -8,26 +8,37 @@ export const CONTINUATION_PROFILES: Record<
8
8
  default: ContinuationMode
9
9
  labels: Record<string, string>
10
10
  icons: Record<string, string>
11
+ /**
12
+ * Picked once from the Build panel before drawing (`wall-draw-variant.ts`),
13
+ * never cycled by C or a HUD chip mid-draw.
14
+ */
15
+ chosenInPanel?: boolean
11
16
  }
12
17
  > = {
18
+ // The wall tool's drawing mode: a chain that closes into a room ('room', the
19
+ // Build panel's Polygon), one wall per two clicks ('single', Walls), or a
20
+ // two-corner box of four walls ('rectangle', Rectangle).
13
21
  wall: {
14
- options: ['room', 'single'],
22
+ options: ['room', 'single', 'rectangle'],
15
23
  default: 'room',
16
- labels: { room: 'Room (auto-close)', single: 'Single wall' },
17
- icons: { room: 'lucide:square', single: 'lucide:minus' },
24
+ labels: { room: 'Polygon room', single: 'Walls', rectangle: 'Rectangle room' },
25
+ icons: { room: 'lucide:pentagon', single: 'lucide:minus', rectangle: 'lucide:square' },
26
+ chosenInPanel: true,
18
27
  },
19
28
  fence: {
20
- options: ['single', 'continuous', 'curved'],
29
+ options: ['single', 'continuous', 'curved', 'freehand'],
21
30
  default: 'continuous',
22
31
  labels: {
23
32
  continuous: 'Continuous',
24
33
  single: 'Single fence',
25
34
  curved: 'Curved fence',
35
+ freehand: 'Freehand fence',
26
36
  },
27
37
  icons: {
28
38
  continuous: 'lucide:waypoints',
29
39
  single: 'lucide:minus',
30
40
  curved: 'lucide:spline',
41
+ freehand: 'lucide:scribble',
31
42
  },
32
43
  },
33
44
  point: {
@@ -62,6 +73,16 @@ export function nextContinuation(
62
73
  return profile.options[(index + 1) % profile.options.length] ?? profile.default
63
74
  }
64
75
 
76
+ /**
77
+ * The context C and the HUD chip may cycle — null when there is none, or when
78
+ * its mode is chosen in the Build panel instead.
79
+ */
80
+ export function keyCyclableContinuationContext(
81
+ context: ContinuationContext | null,
82
+ ): ContinuationContext | null {
83
+ return context && !CONTINUATION_PROFILES[context].chosenInPanel ? context : null
84
+ }
85
+
65
86
  export function continuationContextOf(kind: string): ContinuationContext | null {
66
87
  if (kind === 'wall') return 'wall'
67
88
  if (kind === 'fence') return 'fence'
@@ -86,3 +86,17 @@ export function closeDoorOpenState(doorId: AnyNodeId, options?: DoorOpenAnimatio
86
86
  const currentSwingAngle = getDisplayedDoorValue(doorId, 'swingAngle', node.swingAngle)
87
87
  startDoorOpenAnimation(doorId, 'swingAngle', currentSwingAngle, 0, options)
88
88
  }
89
+
90
+ export function openDoorOpenState(doorId: AnyNodeId, options?: DoorOpenAnimationOptions) {
91
+ const node = useScene.getState().nodes[doorId]
92
+ if (node?.type !== 'door' || node.openingKind === 'opening') return
93
+
94
+ if (isOperationDoorType(node.doorType)) {
95
+ const currentOpenAmount = getDisplayedDoorValue(doorId, 'operationState', node.operationState)
96
+ startDoorOpenAnimation(doorId, 'operationState', currentOpenAmount, 1, options)
97
+ return
98
+ }
99
+
100
+ const currentSwingAngle = getDisplayedDoorValue(doorId, 'swingAngle', node.swingAngle)
101
+ startDoorOpenAnimation(doorId, 'swingAngle', currentSwingAngle, DOOR_SWING_OPEN_ANGLE, options)
102
+ }
@@ -0,0 +1,96 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
2
+ import { BuildingNode, LevelNode, useScene } from '@pascal-app/core'
3
+ import { useViewer } from '@pascal-app/viewer'
4
+ import useEditor, { type CaptureMode } from '../store/use-editor'
5
+ import { showsWholeBuilding } from './editor-level-display'
6
+
7
+ const building = BuildingNode.parse({ id: 'building_capture', children: [] })
8
+ const levels = [0, 1, 2].map((level) =>
9
+ LevelNode.parse({ id: `level_capture_${level}`, level, parentId: building.id }),
10
+ )
11
+ const [ground, first, second] = levels.map((level) => level.id) as [
12
+ LevelNode['id'],
13
+ LevelNode['id'],
14
+ LevelNode['id'],
15
+ ]
16
+
17
+ const wholeBuilding = () => {
18
+ const state = useEditor.getState()
19
+ return showsWholeBuilding({
20
+ isPreviewMode: state.isPreviewMode,
21
+ isFirstPersonMode: state.isFirstPersonMode,
22
+ captureMode: state.captureMode,
23
+ captureLevelId: state.captureLevelId,
24
+ })
25
+ }
26
+ const activeLevel = () => useViewer.getState().selection.levelId
27
+
28
+ beforeEach(() => {
29
+ useScene.setState({
30
+ nodes: Object.fromEntries(
31
+ [{ ...building, children: levels.map((level) => level.id) }, ...levels].map((node) => [
32
+ node.id,
33
+ node,
34
+ ]),
35
+ ),
36
+ rootNodeIds: [building.id],
37
+ } as never)
38
+ useViewer.getState().setSelection({ buildingId: building.id, levelId: second })
39
+ })
40
+ afterEach(() => useEditor.getState().setCaptureMode(false))
41
+
42
+ describe('capture level picker', () => {
43
+ test('editing hides the levels above; capture opens on the whole building', () => {
44
+ expect(wholeBuilding()).toBe(false)
45
+ useEditor.getState().setCaptureMode(true)
46
+ expect(useEditor.getState().captureLevelId).toBeNull()
47
+ expect(wholeBuilding()).toBe(true)
48
+ expect(activeLevel()).toBe(second)
49
+ })
50
+
51
+ test('picking a level shows it and the levels below, as while editing', () => {
52
+ useEditor.getState().setCaptureMode(true)
53
+ useEditor.getState().setCaptureLevel(first)
54
+ expect(wholeBuilding()).toBe(false)
55
+ expect(activeLevel()).toBe(first)
56
+ useEditor.getState().setCaptureLevel(null)
57
+ expect(wholeBuilding()).toBe(true)
58
+ })
59
+
60
+ test("leaving capture restores the editor's active level and the editing rule", () => {
61
+ useEditor.getState().setCaptureMode(true)
62
+ useEditor.getState().setCaptureLevel(ground)
63
+ useEditor.getState().setCaptureMode(false)
64
+ expect(activeLevel()).toBe(second)
65
+ expect(useEditor.getState().captureLevelId).toBeNull()
66
+ expect(wholeBuilding()).toBe(false)
67
+ // The next capture opens on the whole building again.
68
+ useEditor.getState().setCaptureMode({ mode: 'standard', crop: 'area' })
69
+ expect(wholeBuilding()).toBe(true)
70
+ })
71
+
72
+ test('a level picked outside capture is ignored', () => {
73
+ useEditor.getState().setCaptureLevel(ground)
74
+ expect(activeLevel()).toBe(second)
75
+ expect(useEditor.getState().captureLevelId).toBeNull()
76
+ })
77
+
78
+ test('preset captures and the walkthrough keep their own rules', () => {
79
+ const preset: CaptureMode = { mode: 'preset', isolated: [] }
80
+ const base = { isPreviewMode: false, isFirstPersonMode: false, captureLevelId: null }
81
+ expect(showsWholeBuilding({ ...base, captureMode: preset })).toBe(false)
82
+ expect(showsWholeBuilding({ ...base, captureMode: { mode: 'idle' } })).toBe(false)
83
+ expect(
84
+ showsWholeBuilding({ ...base, isFirstPersonMode: true, captureMode: { mode: 'idle' } }),
85
+ ).toBe(true)
86
+ // A walk capture on a picked level still shows only that level and below.
87
+ expect(
88
+ showsWholeBuilding({
89
+ ...base,
90
+ isFirstPersonMode: true,
91
+ captureMode: { mode: 'standard' },
92
+ captureLevelId: first,
93
+ }),
94
+ ).toBe(false)
95
+ })
96
+ })
@@ -0,0 +1,48 @@
1
+ import { useViewer } from '@pascal-app/viewer'
2
+ import { useEffect } from 'react'
3
+ import type { CaptureMode } from '../store/use-editor'
4
+
5
+ /**
6
+ * Whether the editor shows every level. Preview and the walkthrough show the
7
+ * whole building; a scene capture shows what its level picker says (the
8
+ * whole building, or a level and the ones below it). A preset capture frames
9
+ * its isolated item and keeps the editing rule.
10
+ */
11
+ export function showsWholeBuilding(state: {
12
+ isPreviewMode: boolean
13
+ isFirstPersonMode: boolean
14
+ captureMode: CaptureMode
15
+ captureLevelId: string | null
16
+ }): boolean {
17
+ if (state.captureMode.mode === 'standard') return state.captureLevelId === null
18
+ return state.isPreviewMode || state.isFirstPersonMode
19
+ }
20
+
21
+ /**
22
+ * The editor's one level display: stacked, with every level above the
23
+ * selected one hidden — the selected level and the ones below stay, so the
24
+ * storey being worked on is never covered. There is no Stack / Exploded /
25
+ * Solo choice while editing; a mode left over from the viewer (or persisted
26
+ * by an older build) is set back to stacked, and kept there.
27
+ *
28
+ * The walkthrough shows the whole building, so the upper levels come back
29
+ * while it is on; Preview hands the display to the viewer's own floor rule
30
+ * (`useViewerFloorDisplay`).
31
+ */
32
+ export function useEditorLevelDisplay(showWholeBuilding: boolean) {
33
+ useEffect(() => {
34
+ if (showWholeBuilding) return
35
+ const keepStacked = (mode: string) => {
36
+ if (mode !== 'stacked') useViewer.getState().setLevelMode('stacked')
37
+ }
38
+ useViewer.setState({ hideLevelsAboveSelection: true })
39
+ keepStacked(useViewer.getState().levelMode)
40
+ const unsubscribe = useViewer.subscribe((state, previous) => {
41
+ if (state.levelMode !== previous.levelMode) keepStacked(state.levelMode)
42
+ })
43
+ return () => {
44
+ unsubscribe()
45
+ useViewer.setState({ hideLevelsAboveSelection: false })
46
+ }
47
+ }, [showWholeBuilding])
48
+ }
@@ -0,0 +1,144 @@
1
+ import type { IconRef, LazyComponent } from '@pascal-app/core'
2
+ import useEditor from '../store/use-editor'
3
+ import { VIEW_2D, VIEW_3D } from '../store/view-layout'
4
+
5
+ /**
6
+ * A view fills a pane of the stage, beside or instead of the 3D scene and the
7
+ * 2D plan. Hosts and plugins register views the way they register rail panels;
8
+ * a plugin registers as many as it needs, each tagged with its `pluginId`.
9
+ */
10
+ export type EditorHostView = {
11
+ id: string
12
+ label: string
13
+ icon: IconRef
14
+ component: LazyComponent
15
+ /** The view exists in a project only while this plugin is installed there. */
16
+ pluginId?: string
17
+ /** Workspaces the view is offered in. Default `['edit']`, like panels. */
18
+ workspaces?: readonly string[]
19
+ description?: string
20
+ /** Shown in the view bar until the user unpins it. Default false: the "More views" menu only. */
21
+ defaultPinned?: boolean
22
+ }
23
+
24
+ /** A view as the stage needs it. The scene views have no component: the stage owns their canvases. */
25
+ export type EditorViewDescriptor = Omit<EditorHostView, 'component'> & {
26
+ component?: LazyComponent
27
+ /** 3D and 2D: the views the level selector, tool dock and scene overlays belong to. */
28
+ scene?: boolean
29
+ }
30
+
31
+ export const SCENE_VIEWS: readonly EditorViewDescriptor[] = [
32
+ {
33
+ id: VIEW_3D,
34
+ label: '3D',
35
+ icon: { kind: 'url', src: '/icons/building.webp' },
36
+ scene: true,
37
+ defaultPinned: true,
38
+ },
39
+ {
40
+ id: VIEW_2D,
41
+ label: '2D',
42
+ icon: { kind: 'url', src: '/icons/blueprint.webp' },
43
+ scene: true,
44
+ defaultPinned: true,
45
+ },
46
+ ]
47
+
48
+ export function isSceneView(viewId: string): boolean {
49
+ return viewId === VIEW_3D || viewId === VIEW_2D
50
+ }
51
+
52
+ function isDevMode(): boolean {
53
+ if (typeof process !== 'undefined' && process.env?.NODE_ENV) {
54
+ return process.env.NODE_ENV !== 'production'
55
+ }
56
+ return false
57
+ }
58
+
59
+ class EditorHostViewRegistryImpl {
60
+ private readonly views = new Map<string, EditorHostView>()
61
+ private readonly listeners = new Set<() => void>()
62
+ private cached: EditorHostView[] = []
63
+
64
+ subscribe = (onChange: () => void): (() => void) => {
65
+ this.listeners.add(onChange)
66
+ return () => {
67
+ this.listeners.delete(onChange)
68
+ }
69
+ }
70
+
71
+ getSnapshot = (): EditorHostView[] => this.cached
72
+
73
+ reset(): void {
74
+ this.views.clear()
75
+ this.emit()
76
+ }
77
+
78
+ registerView(view: EditorHostView): void {
79
+ if (typeof view.id !== 'string' || view.id.length === 0) {
80
+ throw new Error('[editor:host-views] view id must be a non-empty string')
81
+ }
82
+ if (isSceneView(view.id)) {
83
+ throw new Error(`[editor:host-views] "${view.id}" is a built-in view`)
84
+ }
85
+ if (this.views.has(view.id)) {
86
+ if (isDevMode()) {
87
+ console.warn(`[editor:host-views] re-registering view "${view.id}" (HMR)`)
88
+ } else {
89
+ throw new Error(`[editor:host-views] duplicate view id: "${view.id}" already registered`)
90
+ }
91
+ }
92
+ this.views.set(view.id, view)
93
+ this.emit()
94
+ }
95
+
96
+ private emit(): void {
97
+ this.cached = Array.from(this.views.values())
98
+ for (const listener of this.listeners) listener()
99
+ }
100
+ }
101
+
102
+ export const editorHostViewRegistry = new EditorHostViewRegistryImpl()
103
+
104
+ export function registerEditorHostView(view: EditorHostView): void {
105
+ editorHostViewRegistry.registerView(view)
106
+ }
107
+
108
+ /**
109
+ * Show a view: focus it if it is on screen, else put it in the focused pane —
110
+ * or, with `beside`, split and open it next to what is there.
111
+ */
112
+ export function openEditorView(viewId: string, options?: { beside?: boolean }): void {
113
+ useEditor.getState().showView(viewId, options)
114
+ }
115
+
116
+ /**
117
+ * The views a project can show in a workspace: the scene views, then every
118
+ * registered view offered there whose plugin (if any) is installed.
119
+ */
120
+ export function availableViews({
121
+ registered,
122
+ workspaceMode,
123
+ installedPlugins,
124
+ }: {
125
+ registered: readonly EditorViewDescriptor[]
126
+ workspaceMode: string
127
+ installedPlugins: readonly string[]
128
+ }): EditorViewDescriptor[] {
129
+ return [
130
+ ...SCENE_VIEWS,
131
+ ...registered.filter(
132
+ (view) =>
133
+ (view.workspaces ?? ['edit']).includes(workspaceMode) &&
134
+ (!view.pluginId || installedPlugins.includes(view.pluginId)),
135
+ ),
136
+ ]
137
+ }
138
+
139
+ export function isViewPinned(
140
+ view: EditorViewDescriptor,
141
+ pinnedViews: Readonly<Record<string, boolean>>,
142
+ ): boolean {
143
+ return pinnedViews[view.id] ?? view.defaultPinned ?? false
144
+ }