@pascal-app/editor 1.0.3 → 1.0.5

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 (624) hide show
  1. package/README.md +106 -0
  2. package/package.json +42 -38
  3. package/src/components/editor/__bench__/room-label-placement.bench.ts +108 -0
  4. package/src/components/editor/action-menu-button.tsx +83 -0
  5. package/src/components/editor/bake-thumbnail.tsx +307 -5
  6. package/src/components/editor/capture-level-picker.tsx +72 -0
  7. package/src/components/editor/construction-reveal-audio.test.tsx +150 -0
  8. package/src/components/editor/construction-reveal-audio.tsx +40 -0
  9. package/src/components/editor/custom-camera-controls.tsx +275 -57
  10. package/src/components/editor/delete-confirmation-dialog.tsx +297 -7
  11. package/src/components/editor/duplicate-node.ts +244 -0
  12. package/src/components/editor/editor-layout-mobile.tsx +37 -26
  13. package/src/components/editor/editor-layout-v2.tsx +132 -59
  14. package/src/components/editor/export-manager.tsx +47 -26
  15. package/src/components/editor/first-person/build-collider-world.test.ts +320 -0
  16. package/src/components/editor/first-person/build-collider-world.ts +28 -7
  17. package/src/components/editor/first-person/ground-holes.test.ts +128 -0
  18. package/src/components/editor/first-person/ground-holes.ts +130 -0
  19. package/src/components/editor/first-person/mechanism-targets.test.ts +162 -0
  20. package/src/components/editor/first-person/mechanism-targets.ts +88 -0
  21. package/src/components/editor/first-person-controls.tsx +220 -9
  22. package/src/components/editor/floating-action-menu.tsx +63 -207
  23. package/src/components/editor/floor-aware-camera-controls.test.ts +466 -0
  24. package/src/components/editor/floor-aware-camera-controls.ts +324 -0
  25. package/src/components/editor/floor-openings-overlay.tsx +243 -0
  26. package/src/components/editor/floor-region-controls.tsx +846 -0
  27. package/src/components/editor/floor-region-webgpu-materials.test.ts +12 -0
  28. package/src/components/editor/floor-surface.test.ts +165 -0
  29. package/src/components/editor/floor-surface.ts +182 -0
  30. package/src/components/editor/floorplan-background-selection.test.ts +11 -2
  31. package/src/components/editor/floorplan-background-selection.ts +1 -3
  32. package/src/components/editor/floorplan-mode-coordinator.tsx +2 -2
  33. package/src/components/editor/floorplan-panel.tsx +414 -363
  34. package/src/components/editor/follow-pascal.test.tsx +912 -0
  35. package/src/components/editor/follow-pascal.tsx +354 -0
  36. package/src/components/editor/footprint-height-handle.tsx +204 -0
  37. package/src/components/editor/group-actions.ts +56 -2
  38. package/src/components/editor/group-move-3d.ts +2 -0
  39. package/src/components/editor/group-rotate-handle.tsx +2 -0
  40. package/src/components/editor/group-transform-shared.test.ts +460 -0
  41. package/src/components/editor/handles/handle-hit-priority.test.tsx +63 -0
  42. package/src/components/editor/handles/handle-hit-priority.tsx +37 -0
  43. package/src/components/editor/handles/swallow-next-click.test.ts +28 -0
  44. package/src/components/editor/handles/use-handle-drag.ts +269 -197
  45. package/src/components/editor/index.tsx +409 -197
  46. package/src/components/editor/join-walls-pill.tsx +44 -0
  47. package/src/components/editor/node-action-menu.tsx +68 -70
  48. package/src/components/editor/node-arrow-handles.tsx +25 -6
  49. package/src/components/editor/open-wall-ends-3d-layer.tsx +273 -0
  50. package/src/components/editor/open-wall-ends-hint.tsx +89 -0
  51. package/src/components/editor/quick-measurement-hud.tsx +3 -2
  52. package/src/components/editor/registry-action-contributions.test.tsx +63 -0
  53. package/src/components/editor/registry-action-contributions.tsx +80 -1
  54. package/src/components/editor/riser-diagram-panel.tsx +14 -7
  55. package/src/components/editor/room-controls.tsx +847 -0
  56. package/src/components/editor/room-floor-highlight.tsx +128 -0
  57. package/src/components/editor/room-handles-2d.tsx +354 -0
  58. package/src/components/editor/room-handles.tsx +675 -0
  59. package/src/components/editor/room-highlight.tsx +233 -0
  60. package/src/components/editor/room-labels.test.ts +850 -0
  61. package/src/components/editor/room-labels.tsx +1890 -0
  62. package/src/components/editor/room-transform-ghost.tsx +256 -0
  63. package/src/components/editor/room-webgpu-materials.test.ts +27 -0
  64. package/src/components/editor/selection-manager.tsx +474 -419
  65. package/src/components/editor/slab-hole-highlights.tsx +5 -2
  66. package/src/components/editor/snapshot-capture-overlay.tsx +7 -2
  67. package/src/components/editor/snapshot-capture.test.ts +119 -60
  68. package/src/components/editor/snapshot-capture.ts +36 -5
  69. package/src/components/editor/thumbnail-generator.test.ts +654 -0
  70. package/src/components/editor/thumbnail-generator.tsx +1103 -223
  71. package/src/components/editor/use-floorplan-background-placement.ts +34 -12
  72. package/src/components/editor/vector-edge-extractor.tsx +121 -0
  73. package/src/components/editor/views/host-view.tsx +61 -0
  74. package/src/components/editor/views/plugins-view.tsx +663 -0
  75. package/src/components/editor/views/scene-region.tsx +59 -0
  76. package/src/components/editor/views/use-editor-views.ts +57 -0
  77. package/src/components/editor/views/view-bar.test.tsx +54 -0
  78. package/src/components/editor/views/view-bar.tsx +343 -0
  79. package/src/components/editor/wall-justification.test.ts +140 -0
  80. package/src/components/editor/wall-move-side-handles.tsx +43 -142
  81. package/src/components/editor/wall-push-arrow.tsx +92 -0
  82. package/src/components/editor/wall-region-handles.tsx +509 -0
  83. package/src/components/editor/wall-snap-beacon-layer.tsx +8 -4
  84. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +8 -4
  85. package/src/components/editor-2d/floorplan-group-move.tsx +3 -0
  86. package/src/components/editor-2d/floorplan-open-wall-ends-layer.test.tsx +86 -0
  87. package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +290 -0
  88. package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +2 -2
  89. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +11 -1
  90. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +116 -106
  91. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.test.ts +156 -0
  92. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +278 -0
  93. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +77 -0
  94. package/src/components/editor-2d/renderers/{floorplan-draft-wall-measurement.tsx → floorplan-draft-measurement.tsx} +8 -8
  95. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +4 -1
  96. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +38 -8
  97. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +319 -233
  98. package/src/components/editor-2d/renderers/floorplan-reveal.test.tsx +254 -0
  99. package/src/components/editor-2d/renderers/floorplan-reveal.ts +141 -0
  100. package/src/components/editor-2d/svg-paths.ts +12 -15
  101. package/src/components/point-ask/bubble.tsx +538 -0
  102. package/src/components/point-ask/hover-chip.tsx +91 -0
  103. package/src/components/point-ask/mode-pill.tsx +92 -0
  104. package/src/components/point-ask/optional-motion.ts +22 -0
  105. package/src/components/point-ask/outline-corners.tsx +93 -0
  106. package/src/components/point-ask/pins.tsx +711 -0
  107. package/src/components/point-ask/point-ask-layer.tsx +83 -0
  108. package/src/components/point-ask/point-ask-styles.ts +105 -0
  109. package/src/components/point-ask/region-marquee-styles.ts +15 -0
  110. package/src/components/point-ask/region-marquee.tsx +64 -0
  111. package/src/components/procedural-items/inspector.tsx +40 -15
  112. package/src/components/systems/ceiling/ceiling-bracket-batch.test.ts +12 -2
  113. package/src/components/systems/ceiling/ceiling-selection-affordance-system.test.ts +97 -14
  114. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +18 -18
  115. package/src/components/systems/roof/roof-edit-system.tsx +1 -0
  116. package/src/components/systems/zone/zone-system.test.ts +47 -0
  117. package/src/components/systems/zone/zone-system.tsx +29 -68
  118. package/src/components/tools/distribution/connection-compatibility.test.ts +34 -0
  119. package/src/components/tools/distribution/connection-compatibility.ts +43 -0
  120. package/src/components/tools/distribution/distribution-public.ts +9 -0
  121. package/src/components/tools/distribution/distribution-run-contract.test.ts +26 -0
  122. package/src/components/tools/distribution/distribution-run-contract.ts +94 -0
  123. package/src/components/tools/distribution/distribution-run-tool.test.ts +241 -0
  124. package/src/components/tools/distribution/distribution-run-tool.test.tsx +125 -0
  125. package/src/components/tools/distribution/distribution-run-tool.tsx +1323 -0
  126. package/src/components/tools/distribution/draw-alignment.ts +35 -0
  127. package/src/components/tools/distribution/ports.test.ts +56 -0
  128. package/src/components/tools/distribution/ports.ts +378 -0
  129. package/src/components/tools/distribution/run-cursor.test.ts +135 -0
  130. package/src/components/tools/distribution/run-cursor.ts +36 -0
  131. package/src/components/tools/distribution/run-direction-feedback.test.ts +60 -0
  132. package/src/components/tools/distribution/run-direction-feedback.tsx +251 -0
  133. package/src/components/tools/distribution/run-port-snap.test.ts +79 -0
  134. package/src/components/tools/distribution/run-port-snap.ts +25 -0
  135. package/src/components/tools/distribution/run-screen-direction.test.ts +23 -0
  136. package/src/components/tools/distribution/run-screen-direction.ts +30 -0
  137. package/src/components/tools/elevator/elevator-tool.tsx +3 -6
  138. package/src/components/tools/elevator/move-elevator-tool.tsx +2 -3
  139. package/src/components/tools/item/item-surface-strategy-resolver.test.ts +67 -68
  140. package/src/components/tools/item/placement-history.test.tsx +512 -0
  141. package/src/components/tools/item/placement-strategies.test.ts +46 -1
  142. package/src/components/tools/item/placement-strategies.ts +11 -10
  143. package/src/components/tools/item/shelf-surface-strategy-resolver.test.ts +56 -56
  144. package/src/components/tools/item/use-draft-node.test.tsx +432 -1
  145. package/src/components/tools/item/use-draft-node.ts +166 -95
  146. package/src/components/tools/item/use-placement-coordinator.tsx +148 -95
  147. package/src/components/tools/paint-region/use-paint-tint.ts +37 -0
  148. package/src/components/tools/paint-region/wall-paint-region-tool.tsx +466 -0
  149. package/src/components/tools/registry/item-surface-move.test.ts +62 -63
  150. package/src/components/tools/registry/item-surface-move.ts +2 -0
  151. package/src/components/tools/registry/item-surface-resolver.test.ts +26 -26
  152. package/src/components/tools/registry/move-registry-node-tool.test.ts +268 -1
  153. package/src/components/tools/registry/move-registry-node-tool.tsx +66 -30
  154. package/src/components/tools/select/box-select-tool.tsx +2 -12
  155. package/src/components/tools/select/plane-box-select-tool.test.tsx +79 -0
  156. package/src/components/tools/select/plane-box-select-tool.tsx +35 -22
  157. package/src/components/tools/select/select-candidates.test.ts +37 -4
  158. package/src/components/tools/select/select-candidates.ts +15 -11
  159. package/src/components/tools/shared/cursor-sphere.tsx +36 -12
  160. package/src/components/tools/site/site-boundary-editor.tsx +1 -2
  161. package/src/components/tools/tool-manager.tsx +59 -24
  162. package/src/components/tools/wall/wall-drafting.test.ts +175 -47
  163. package/src/components/tools/zone/zone-tool.tsx +4 -12
  164. package/src/components/ui/action-menu/action-button.tsx +9 -1
  165. package/src/components/ui/action-menu/camera-actions.tsx +9 -4
  166. package/src/components/ui/action-menu/control-modes.tsx +15 -42
  167. package/src/components/ui/action-menu/follow-pascal-pill.tsx +32 -0
  168. package/src/components/ui/action-menu/follow-pascal-toggle.tsx +36 -0
  169. package/src/components/ui/action-menu/index.tsx +56 -33
  170. package/src/components/ui/action-menu/measurement-control.tsx +14 -19
  171. package/src/components/ui/action-menu/placement.ts +20 -0
  172. package/src/components/ui/action-menu/point-ask-button.tsx +52 -0
  173. package/src/components/ui/action-menu/view-toggles.tsx +47 -42
  174. package/src/components/ui/build-panel/build-panel.tsx +242 -0
  175. package/src/components/ui/command-palette/editor-commands.tsx +7 -42
  176. package/src/components/ui/command-palette/index.tsx +50 -39
  177. package/src/components/ui/command-palette/ranked-groups.test.ts +31 -0
  178. package/src/components/ui/controls/action-button.tsx +5 -2
  179. package/src/components/ui/controls/finish-swatch.tsx +110 -0
  180. package/src/components/ui/controls/material-paint-panel.tsx +97 -26
  181. package/src/components/ui/controls/material-picker.tsx +4 -1
  182. package/src/components/ui/controls/metric-control.tsx +1 -1
  183. package/src/components/ui/controls/scene-material-list.tsx +4 -32
  184. package/src/components/ui/controls/segmented-control.tsx +21 -8
  185. package/src/components/ui/controls/select-control.tsx +65 -0
  186. package/src/components/ui/controls/slider-control.tsx +1 -1
  187. package/src/components/ui/controls/toggle-control.tsx +3 -3
  188. package/src/components/ui/controls/tool-options-panel.tsx +1 -1
  189. package/src/components/ui/floating-level-selector.tsx +159 -59
  190. package/src/components/ui/helpers/building-helper.tsx +4 -1
  191. package/src/components/ui/helpers/contextual-helper-panel.tsx +181 -72
  192. package/src/components/ui/helpers/helper-manager.test.tsx +121 -0
  193. package/src/components/ui/helpers/helper-manager.tsx +331 -14
  194. package/src/components/ui/helpers/item-helper.tsx +17 -1
  195. package/src/components/ui/helpers/registered-tool-helper.tsx +8 -1
  196. package/src/components/ui/icon-ref.tsx +45 -1
  197. package/src/components/ui/level-duplicate-dialog.tsx +1 -1
  198. package/src/components/ui/node-icon-image.tsx +16 -0
  199. package/src/components/ui/panels/collections/collections-popover.tsx +28 -22
  200. package/src/components/ui/panels/floor-foundation-panel.tsx +551 -0
  201. package/src/components/ui/panels/floor-opening-panel.tsx +143 -0
  202. package/src/components/ui/panels/mobile-panel-sheet.tsx +4 -4
  203. package/src/components/ui/panels/mobile-selection-bar.tsx +8 -5
  204. package/src/components/ui/panels/multi-field-value.test.ts +26 -3
  205. package/src/components/ui/panels/multi-field-value.ts +6 -3
  206. package/src/components/ui/panels/multi-parametric-inspector.tsx +52 -2
  207. package/src/components/ui/panels/multi-selection-panel.tsx +161 -5
  208. package/src/components/ui/panels/node-display.ts +8 -3
  209. package/src/components/ui/panels/panel-manager.tsx +103 -68
  210. package/src/components/ui/panels/panel-wrapper.tsx +130 -62
  211. package/src/components/ui/panels/parametric-field-control.tsx +3 -3
  212. package/src/components/ui/panels/parametric-inspector.tsx +7 -7
  213. package/src/components/ui/panels/reference-panel.tsx +2 -3
  214. package/src/components/ui/panels/room-construction-rows.tsx +774 -0
  215. package/src/components/ui/panels/room-name-options.test.ts +55 -0
  216. package/src/components/ui/panels/room-name-options.ts +47 -0
  217. package/src/components/ui/panels/room-openings-field.tsx +110 -0
  218. package/src/components/ui/panels/room-panel.tsx +790 -0
  219. package/src/components/ui/panels/shape-choice.tsx +58 -0
  220. package/src/components/ui/panels/wall-region-list.tsx +109 -0
  221. package/src/components/ui/primitives/badge.tsx +8 -0
  222. package/src/components/ui/primitives/color-dot.tsx +36 -24
  223. package/src/components/ui/primitives/sidebar.tsx +3 -1
  224. package/src/components/ui/right-stack.test.ts +29 -0
  225. package/src/components/ui/right-stack.tsx +164 -0
  226. package/src/components/ui/sidebar/app-sidebar.tsx +1 -1
  227. package/src/components/ui/sidebar/icon-rail.tsx +8 -3
  228. package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +1 -1
  229. package/src/components/ui/sidebar/panels/items-panel/index.tsx +6 -6
  230. package/src/components/ui/sidebar/panels/settings-panel/animation-speed-setting.tsx +40 -0
  231. package/src/components/ui/sidebar/panels/settings-panel/audio-settings-dialog.tsx +7 -8
  232. package/src/components/ui/sidebar/panels/settings-panel/construction-animation-setting.test.tsx +170 -0
  233. package/src/components/ui/sidebar/panels/settings-panel/construction-animation-setting.tsx +92 -0
  234. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +138 -27
  235. package/src/components/ui/sidebar/panels/settings-panel/interface-theme.test.tsx +20 -0
  236. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +5 -5
  237. package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +4 -10
  238. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +3 -11
  239. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -4
  240. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +1 -5
  241. package/src/components/ui/sidebar/panels/site-panel/collection-tree-node.tsx +197 -0
  242. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -5
  243. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +1 -5
  244. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +1 -5
  245. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -5
  246. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +24 -11
  247. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +1 -5
  248. package/src/components/ui/sidebar/panels/site-panel/index.tsx +181 -223
  249. package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +30 -8
  250. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +1 -3
  251. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +4 -1
  252. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +1 -3
  253. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +1 -4
  254. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +1 -3
  255. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +21 -13
  256. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +1 -5
  257. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -5
  258. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +1 -5
  259. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +13 -37
  260. package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +2 -3
  261. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -5
  262. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +1 -5
  263. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +9 -4
  264. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +2 -4
  265. package/src/components/ui/sidebar/tab-bar.tsx +14 -4
  266. package/src/components/ui/sidebar/use-plugin-panels.tsx +15 -5
  267. package/src/components/viewer/floorplan-compass-button.tsx +9 -1
  268. package/src/components/viewer/floorplan-preview.tsx +5 -6
  269. package/src/components/viewer/viewer-controls-bar.tsx +18 -6
  270. package/src/components/viewer/viewer-hover-label.tsx +34 -0
  271. package/src/components/viewer/viewer-rooms.tsx +191 -0
  272. package/src/components/viewer/viewer-scene-header.tsx +181 -145
  273. package/src/components/viewer/viewer-selection-manager.tsx +124 -0
  274. package/src/components/viewer/viewer-stage.tsx +2 -2
  275. package/src/components/viewer-zone-system.tsx +3 -8
  276. package/src/hooks/use-auto-save.integration.test.tsx +215 -43
  277. package/src/hooks/use-auto-save.ts +60 -9
  278. package/src/hooks/use-ceiling-events.test.ts +123 -118
  279. package/src/hooks/use-grid-events.test.ts +105 -105
  280. package/src/hooks/use-keyboard.test.ts +243 -19
  281. package/src/hooks/use-keyboard.ts +118 -83
  282. package/src/hooks/use-open-wall-ends.test.ts +99 -0
  283. package/src/hooks/use-open-wall-ends.ts +238 -0
  284. package/src/hooks/use-placement-notice.test.ts +71 -0
  285. package/src/hooks/use-placement-notice.ts +73 -0
  286. package/src/hooks/use-selected-room.ts +143 -0
  287. package/src/index.tsx +312 -58
  288. package/src/lib/__fixtures__/owner-victor-seattle-parking.json +1 -0
  289. package/src/lib/__fixtures__/owner-villa-aurelia-basement.json +1 -0
  290. package/src/lib/add-storey.test.ts +177 -0
  291. package/src/lib/add-storey.ts +59 -0
  292. package/src/lib/below-grade.test.tsx +171 -0
  293. package/src/lib/below-grade.ts +133 -0
  294. package/src/lib/camera-facing.test.ts +24 -0
  295. package/src/lib/camera-facing.ts +24 -0
  296. package/src/lib/ceiling-edit-session.test.ts +321 -0
  297. package/src/lib/ceiling-edit-session.ts +206 -0
  298. package/src/lib/ceiling-region-session.test.ts +179 -0
  299. package/src/lib/continuation.test.ts +19 -1
  300. package/src/lib/continuation.ts +25 -4
  301. package/src/lib/direct-manipulation.ts +2 -0
  302. package/src/lib/door-interaction.ts +14 -0
  303. package/src/lib/download-project-file.ts +17 -0
  304. package/src/lib/editor-level-display.test.ts +96 -0
  305. package/src/lib/editor-level-display.ts +55 -0
  306. package/src/lib/editor-views.ts +178 -0
  307. package/src/lib/export-geometry-wait.test.ts +90 -0
  308. package/src/lib/export-texture-utils.ts +2 -4
  309. package/src/lib/floor-edit-session.ts +153 -0
  310. package/src/lib/floor-footprints.test.ts +563 -0
  311. package/src/lib/floor-footprints.ts +395 -0
  312. package/src/lib/floor-foundation-support.test.ts +284 -0
  313. package/src/lib/floor-opening-draft.ts +218 -0
  314. package/src/lib/floor-openings.test.ts +216 -0
  315. package/src/lib/floor-region-geometry.ts +191 -0
  316. package/src/lib/floor-region-session.test.ts +485 -0
  317. package/src/lib/floor-region-session.ts +589 -0
  318. package/src/lib/floor-region-snap.ts +101 -0
  319. package/src/lib/floor-step-hover.test.ts +32 -0
  320. package/src/lib/floorplan/annotation-visibility.ts +17 -0
  321. package/src/lib/floorplan/dimension-drive.test.ts +360 -0
  322. package/src/lib/floorplan/dimension-drive.ts +479 -0
  323. package/src/lib/floorplan/floorplan-export.test.ts +12 -8
  324. package/src/lib/floorplan/floorplan-export.tsx +32 -8
  325. package/src/lib/floorplan/floorplan-extension.ts +32 -0
  326. package/src/lib/floorplan/floorplan-mode.test.ts +4 -0
  327. package/src/lib/floorplan/floorplan-mode.ts +2 -0
  328. package/src/lib/floorplan/floorplan-pdfkit-document.ts +363 -27
  329. package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +177 -8
  330. package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +60 -25
  331. package/src/lib/floorplan/floorplan-plugin-install.test.tsx +138 -0
  332. package/src/lib/floorplan/floorplan-readonly.ts +6 -0
  333. package/src/lib/floorplan/geometry.ts +13 -6
  334. package/src/lib/floorplan/index.ts +0 -9
  335. package/src/lib/floorplan/items.ts +5 -2
  336. package/src/lib/floorplan/marks.test.ts +213 -0
  337. package/src/lib/floorplan/marks.ts +238 -0
  338. package/src/lib/floorplan/open-wall-ends.test.ts +268 -0
  339. package/src/lib/floorplan/open-wall-ends.ts +162 -0
  340. package/src/lib/floorplan/pdf-fonts/GeistMono-Bold.ttf +0 -0
  341. package/src/lib/floorplan/pdf-fonts/GeistMono-Regular.ttf +0 -0
  342. package/src/lib/floorplan/pdf-fonts/LICENSE-GeistMono.txt +92 -0
  343. package/src/lib/floorplan/pdf-fonts/LICENSE-LiberationSans.txt +102 -0
  344. package/src/lib/floorplan/pdf-fonts/LiberationSans-Bold.ttf +0 -0
  345. package/src/lib/floorplan/pdf-fonts/LiberationSans-Regular.ttf +0 -0
  346. package/src/lib/floorplan/pdfkit-standard-fonts.d.ts +5 -0
  347. package/src/lib/floorplan/schedules.test.ts +189 -0
  348. package/src/lib/floorplan/schedules.ts +339 -0
  349. package/src/lib/floorplan/sheet-export.test.ts +124 -0
  350. package/src/lib/floorplan/sheet-export.ts +318 -0
  351. package/src/lib/floorplan/site-plan/build-site-plan-drawing.ts +768 -0
  352. package/src/lib/floorplan/site-plan/contours.test.ts +43 -0
  353. package/src/lib/floorplan/site-plan/contributors.ts +97 -0
  354. package/src/lib/floorplan/site-plan/coverage.ts +230 -0
  355. package/src/lib/floorplan/site-plan/drawing-type-switch.test.ts +15 -0
  356. package/src/lib/floorplan/site-plan/drawing-type-switch.tsx +86 -0
  357. package/src/lib/floorplan/site-plan/front-edge.test.ts +103 -0
  358. package/src/lib/floorplan/site-plan/front-edge.ts +157 -0
  359. package/src/lib/floorplan/site-plan/geometry.test.ts +269 -0
  360. package/src/lib/floorplan/site-plan/geometry.ts +142 -0
  361. package/src/lib/floorplan/site-plan/index.ts +55 -0
  362. package/src/lib/floorplan/site-plan/site-annotations.test.ts +483 -0
  363. package/src/lib/floorplan/site-plan/site-annotations.ts +1084 -0
  364. package/src/lib/floorplan/site-plan/site-parts.ts +287 -0
  365. package/src/lib/floorplan/site-plan/site-plan-layer.tsx +138 -0
  366. package/src/lib/floorplan/site-plan/street-name.test.ts +69 -0
  367. package/src/lib/floorplan/site-plan/street-name.ts +302 -0
  368. package/src/lib/floorplan/site-plan/yards.test.ts +70 -0
  369. package/src/lib/floorplan/types.ts +1 -27
  370. package/src/lib/follow-pascal-exterior.test-fixture.ts +17 -0
  371. package/src/lib/follow-pascal-exterior.test.ts +171 -0
  372. package/src/lib/follow-pascal-exterior.ts +39 -0
  373. package/src/lib/follow-pascal-presentation.test.ts +203 -0
  374. package/src/lib/follow-pascal-presentation.ts +129 -0
  375. package/src/lib/follow-pascal-sight.ts +84 -0
  376. package/src/lib/follow-pascal-targets.test.ts +406 -0
  377. package/src/lib/follow-pascal-targets.ts +198 -0
  378. package/src/lib/follow-pascal.test.ts +350 -0
  379. package/src/lib/follow-pascal.ts +284 -0
  380. package/src/lib/fresh-planar-placement.test.ts +148 -0
  381. package/src/lib/fresh-planar-placement.ts +114 -37
  382. package/src/lib/geometry-script/author.ts +157 -0
  383. package/src/lib/geometry-script/client.ts +159 -0
  384. package/src/lib/geometry-script/finish-scripts.ts +127 -0
  385. package/src/lib/geometry-script/protocol.ts +13 -0
  386. package/src/lib/gesture-lifecycle.test.ts +595 -0
  387. package/src/lib/gesture-lifecycle.ts +290 -0
  388. package/src/lib/glb-export-zone-holes.test.ts +41 -0
  389. package/src/lib/glb-export.test.ts +636 -126
  390. package/src/lib/glb-export.ts +241 -120
  391. package/src/lib/history.test.ts +18 -7
  392. package/src/lib/history.ts +7 -0
  393. package/src/lib/hud-title.ts +109 -0
  394. package/src/lib/hydration-gated-storage.ts +49 -0
  395. package/src/lib/ifc-export.test.ts +174 -0
  396. package/src/lib/ifc-export.ts +201 -0
  397. package/src/lib/ifc-parts.ts +15 -0
  398. package/src/lib/inspector-expanded.test.ts +44 -0
  399. package/src/lib/inspector-expanded.ts +36 -0
  400. package/src/lib/interaction/scope.ts +22 -0
  401. package/src/lib/keyboard-pan.test.ts +129 -7
  402. package/src/lib/keyboard-pan.ts +161 -12
  403. package/src/lib/level-selection.test.ts +57 -0
  404. package/src/lib/level-selection.ts +16 -1
  405. package/src/lib/live-gesture-round-trip.test.ts +360 -0
  406. package/src/lib/lot/__fixtures__/map-dossier-miami-shores.json +1 -0
  407. package/src/lib/lot/__fixtures__/map-dossier-stpete.json +1 -0
  408. package/src/lib/lot/clean-ring.test.ts +187 -0
  409. package/src/lib/lot/clean-ring.ts +217 -0
  410. package/src/lib/lot/dossier.test.ts +202 -0
  411. package/src/lib/lot/dossier.ts +498 -0
  412. package/src/lib/lot/drop-in.test.ts +326 -0
  413. package/src/lib/lot/drop-in.ts +392 -0
  414. package/src/lib/lot/index.ts +43 -0
  415. package/src/lib/lot/lot-patch.test.ts +340 -0
  416. package/src/lib/lot/lot-patch.ts +394 -0
  417. package/src/lib/lot/parcel-provider.ts +43 -0
  418. package/src/lib/lot/terrain.test.ts +161 -0
  419. package/src/lib/lot/terrain.ts +266 -0
  420. package/src/lib/material-catalog-model.ts +3 -1
  421. package/src/lib/material-paint-panel-model.ts +9 -5
  422. package/src/lib/material-paint.ts +4 -0
  423. package/src/lib/measurement-kind.test.ts +6 -8
  424. package/src/lib/measurements.test.ts +23 -0
  425. package/src/lib/mezzanine-actions.test.ts +342 -0
  426. package/src/lib/mezzanine-draft.test.ts +501 -0
  427. package/src/lib/mezzanine-draft.ts +278 -0
  428. package/src/lib/mezzanine-messages.ts +26 -0
  429. package/src/lib/mezzanine-selection.test.ts +207 -0
  430. package/src/lib/mezzanine-stairs.ts +46 -0
  431. package/src/lib/model-export.ts +3 -1
  432. package/src/lib/node-action-movement.ts +17 -6
  433. package/src/lib/node-display-label.ts +37 -0
  434. package/src/lib/orbit-floor-clearance.test.ts +201 -0
  435. package/src/lib/orbit-floor-clearance.ts +154 -0
  436. package/src/lib/paint-commit-route.test.ts +37 -0
  437. package/src/lib/paint-commit-route.ts +17 -0
  438. package/src/lib/paint-eyedropper.test.tsx +165 -0
  439. package/src/lib/paint-eyedropper.ts +99 -0
  440. package/src/lib/paint-pass-through.test.ts +122 -0
  441. package/src/lib/paint-pass-through.ts +78 -0
  442. package/src/lib/paint-region-hover.ts +25 -0
  443. package/src/lib/paint-region-mode.ts +154 -0
  444. package/src/lib/paint-regions.test.ts +166 -0
  445. package/src/lib/paint-regions.ts +192 -0
  446. package/src/lib/paint-scope.test.ts +317 -35
  447. package/src/lib/paint-scope.ts +267 -25
  448. package/src/lib/panel-rows.ts +21 -0
  449. package/src/lib/parametric-node-update.ts +16 -7
  450. package/src/lib/plate-paint-affected.test.ts +300 -0
  451. package/src/lib/plate-paint-affected.ts +177 -0
  452. package/src/lib/plugin-chat.ts +65 -0
  453. package/src/lib/plugin-panels.test.ts +25 -0
  454. package/src/lib/plugin-panels.ts +94 -0
  455. package/src/lib/point-ask/__fixtures__/consumer-browser.tsx +76 -0
  456. package/src/lib/point-ask/__fixtures__/house.ts +81 -0
  457. package/src/lib/point-ask/ask-created.test.ts +195 -0
  458. package/src/lib/point-ask/ask-flow.test.ts +380 -0
  459. package/src/lib/point-ask/build-context.test.ts +173 -0
  460. package/src/lib/point-ask/build-context.ts +94 -0
  461. package/src/lib/point-ask/capture-crop.ts +129 -0
  462. package/src/lib/point-ask/choreography.ts +69 -0
  463. package/src/lib/point-ask/compare.test.ts +145 -0
  464. package/src/lib/point-ask/compare.ts +73 -0
  465. package/src/lib/point-ask/describe-target.test.ts +165 -0
  466. package/src/lib/point-ask/describe-target.ts +234 -0
  467. package/src/lib/point-ask/dictation.ts +79 -0
  468. package/src/lib/point-ask/edge-marker.test.ts +61 -0
  469. package/src/lib/point-ask/edge-marker.ts +27 -0
  470. package/src/lib/point-ask/frame-loop.ts +25 -0
  471. package/src/lib/point-ask/history-marks.test.ts +94 -0
  472. package/src/lib/point-ask/history-marks.ts +39 -0
  473. package/src/lib/point-ask/host-api.ts +32 -0
  474. package/src/lib/point-ask/outline-box.test.ts +30 -0
  475. package/src/lib/point-ask/outline-box.ts +11 -0
  476. package/src/lib/point-ask/overlay-metrics.ts +6 -0
  477. package/src/lib/point-ask/pick.test.ts +220 -0
  478. package/src/lib/point-ask/pick.ts +121 -0
  479. package/src/lib/point-ask/pin-machine.test.ts +494 -0
  480. package/src/lib/point-ask/pin-machine.ts +217 -0
  481. package/src/lib/point-ask/placement.test.ts +203 -0
  482. package/src/lib/point-ask/placement.ts +141 -0
  483. package/src/lib/point-ask/pointer-tracker.ts +34 -0
  484. package/src/lib/point-ask/projector.ts +83 -0
  485. package/src/lib/point-ask/region.test.ts +157 -0
  486. package/src/lib/point-ask/region.ts +136 -0
  487. package/src/lib/point-ask/retry.test.ts +217 -0
  488. package/src/lib/point-ask/retry.ts +37 -0
  489. package/src/lib/point-ask/scene-commit.test.ts +312 -0
  490. package/src/lib/point-ask/scene-commit.ts +32 -0
  491. package/src/lib/point-ask/session.ts +1391 -0
  492. package/src/lib/point-ask/spring-keyframes.test.ts +45 -0
  493. package/src/lib/point-ask/spring-keyframes.ts +38 -0
  494. package/src/lib/point-ask/spring.test.ts +63 -0
  495. package/src/lib/point-ask/spring.ts +107 -0
  496. package/src/lib/point-ask/suggestions.test.ts +80 -0
  497. package/src/lib/point-ask/suggestions.ts +125 -0
  498. package/src/lib/point-ask/target-scope.test.ts +622 -0
  499. package/src/lib/point-ask/target-scope.ts +338 -0
  500. package/src/lib/point-ask/types.ts +48 -0
  501. package/src/lib/portable-export.test.ts +34 -1
  502. package/src/lib/portable-export.ts +119 -53
  503. package/src/lib/print-feature-thickness.ts +10 -1
  504. package/src/lib/print-roof-solids.test.ts +39 -1
  505. package/src/lib/print-roof-solids.ts +43 -10
  506. package/src/lib/print-shell-compiler-manifold-core.ts +1 -1
  507. package/src/lib/print-shell-compiler.ts +1 -0
  508. package/src/lib/registered-draft-snap.ts +12 -0
  509. package/src/lib/roof-duplication.ts +10 -2
  510. package/src/lib/room-assembly-overlay.test.ts +275 -0
  511. package/src/lib/room-assembly-overlay.ts +265 -0
  512. package/src/lib/room-built-on.test.ts +379 -0
  513. package/src/lib/room-built-on.ts +138 -0
  514. package/src/lib/room-construction-commands.ts +712 -0
  515. package/src/lib/room-construction.test.ts +746 -0
  516. package/src/lib/room-construction.ts +385 -0
  517. package/src/lib/room-controls-phase6.test.ts +630 -0
  518. package/src/lib/room-divide-session.ts +523 -0
  519. package/src/lib/room-handle-drag.ts +1068 -0
  520. package/src/lib/room-name-catalog.test.ts +90 -0
  521. package/src/lib/room-name-catalog.ts +168 -0
  522. package/src/lib/room-push-dimensions.test.ts +311 -0
  523. package/src/lib/room-push-dimensions.ts +251 -0
  524. package/src/lib/room-selection-commands.ts +62 -0
  525. package/src/lib/room-selection.test.ts +1017 -0
  526. package/src/lib/room-selection.ts +417 -0
  527. package/src/lib/room-structure-commands.ts +94 -0
  528. package/src/lib/room-structure.test.ts +921 -0
  529. package/src/lib/room-transform-session.ts +570 -0
  530. package/src/lib/room-zone-routing.test.ts +294 -0
  531. package/src/lib/room-zone-routing.ts +197 -0
  532. package/src/lib/scene-clipboard.test.ts +610 -1
  533. package/src/lib/scene-clipboard.ts +269 -58
  534. package/src/lib/scene-material-refs.test.ts +79 -0
  535. package/src/lib/scene-material-refs.ts +79 -0
  536. package/src/lib/scene.test.ts +24 -36
  537. package/src/lib/scene.ts +33 -57
  538. package/src/lib/selection-routing.test.ts +129 -45
  539. package/src/lib/selection-routing.ts +54 -75
  540. package/src/lib/session-writes.test.ts +120 -0
  541. package/src/lib/session-writes.ts +205 -0
  542. package/src/lib/sfx-bus.ts +13 -0
  543. package/src/lib/sfx-player.test.ts +70 -9
  544. package/src/lib/sfx-player.ts +6 -0
  545. package/src/lib/sidebar-panel.ts +62 -0
  546. package/src/lib/site-boundary.test.ts +4 -6
  547. package/src/lib/site-boundary.ts +1 -1
  548. package/src/lib/snapping-mode.test.ts +4 -0
  549. package/src/lib/snapping-mode.ts +8 -1
  550. package/src/lib/stair-levels.test.ts +25 -0
  551. package/src/lib/stair-levels.ts +22 -3
  552. package/src/lib/terrace-draft.test.ts +145 -0
  553. package/src/lib/terrace-draft.ts +214 -0
  554. package/src/lib/tool-cursor-icon.test.ts +16 -0
  555. package/src/lib/tool-cursor-icon.ts +11 -0
  556. package/src/lib/typing-target.ts +9 -0
  557. package/src/lib/units.ts +77 -39
  558. package/src/lib/vector-edges.test.ts +58 -0
  559. package/src/lib/vector-edges.ts +400 -0
  560. package/src/lib/viewer-selection.test.ts +577 -0
  561. package/src/lib/viewer-selection.ts +580 -0
  562. package/src/lib/wall-draw-variant.test.ts +71 -0
  563. package/src/lib/wall-draw-variant.ts +91 -0
  564. package/src/lib/wall-paint-region-session.ts +246 -0
  565. package/src/lib/wall-paint-region.test.ts +373 -0
  566. package/src/lib/wall-placement-pointer.test.ts +42 -0
  567. package/src/lib/wall-placement-pointer.ts +57 -0
  568. package/src/lib/wall-polygon-draft.test.ts +262 -0
  569. package/src/lib/wall-polygon-draft.ts +286 -0
  570. package/src/lib/wall-push.test.ts +453 -0
  571. package/src/lib/wall-region-face.ts +352 -0
  572. package/src/lib/wall-region-handles.test.ts +304 -0
  573. package/src/lib/wall-region-handles.ts +368 -0
  574. package/src/lib/wall-region-list.test.ts +119 -0
  575. package/src/lib/wall-region-list.ts +136 -0
  576. package/src/lib/wall-region-snap.ts +110 -0
  577. package/src/lib/window-changes.ts +35 -0
  578. package/src/lib/window-interaction.ts +14 -0
  579. package/src/lib/zone-content.ts +4 -1
  580. package/src/lib/zone-name.test.ts +17 -0
  581. package/src/lib/zone-name.ts +17 -0
  582. package/src/store/opens-in-select-mode.test.ts +223 -0
  583. package/src/store/paint-mode.test.ts +188 -0
  584. package/src/store/terrain-sculpt-mode.test.ts +19 -21
  585. package/src/store/tool-mode.test.ts +137 -6
  586. package/src/store/use-audio.test.ts +146 -0
  587. package/src/store/use-audio.tsx +127 -3
  588. package/src/store/use-construction-reveal.test.ts +173 -0
  589. package/src/store/use-construction-reveal.ts +145 -0
  590. package/src/store/use-corner-snap-hint.test.ts +24 -0
  591. package/src/store/use-corner-snap-hint.ts +47 -0
  592. package/src/store/use-delete-confirmation.ts +28 -1
  593. package/src/store/use-drawing-view.test.ts +24 -3
  594. package/src/store/use-drawing-view.ts +29 -1
  595. package/src/store/use-editor.tsx +432 -321
  596. package/src/store/use-floorplan-draft-preview.ts +13 -0
  597. package/src/store/use-floorplan-marquee.ts +12 -2
  598. package/src/store/use-follow-pascal.test.ts +62 -0
  599. package/src/store/use-follow-pascal.ts +53 -0
  600. package/src/store/use-point-ask.ts +247 -0
  601. package/src/store/use-quick-measurement-hud.ts +3 -3
  602. package/src/store/use-stair-build-preview.ts +33 -5
  603. package/src/store/use-wall-move-ghosts.ts +4 -2
  604. package/src/store/use-wall-region-handles-selection.test.ts +222 -0
  605. package/src/store/use-wall-region-handles-selection.ts +139 -0
  606. package/src/store/view-layout.test.ts +201 -0
  607. package/src/store/view-layout.ts +199 -0
  608. package/src/test-utils/immediate-animation-frames.ts +16 -0
  609. package/src/test-utils/live-gesture-round-trip.ts +224 -0
  610. package/src/test-utils/render-current-markup.tsx +16 -0
  611. package/src/test-utils/selection-harness.ts +82 -0
  612. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +0 -499
  613. package/src/components/systems/zone/zone-label-editor-system.tsx +0 -333
  614. package/src/components/tools/fence/fence-drafting.test.ts +0 -100
  615. package/src/components/tools/fence/fence-drafting.ts +0 -266
  616. package/src/components/tools/stair/stair-click-guard.test.ts +0 -58
  617. package/src/components/tools/stair/stair-click-guard.ts +0 -72
  618. package/src/components/tools/stair/stair-defaults.ts +0 -18
  619. package/src/components/tools/stair/stair-tool.tsx +0 -634
  620. package/src/components/ui/sidebar/panels/plugins-panel.tsx +0 -218
  621. package/src/components/viewer/viewer-units-panel.tsx +0 -116
  622. package/src/lib/floorplan/stairs.ts +0 -489
  623. package/src/lib/level-duplication.test.ts +0 -144
  624. package/src/lib/level-duplication.ts +0 -195
@@ -0,0 +1,242 @@
1
+ 'use client'
2
+
3
+ import { ChevronRight } from 'lucide-react'
4
+ import { type ComponentPropsWithoutRef, type ReactNode, useEffect, useState } from 'react'
5
+ import { cn } from '../../../lib/utils'
6
+ import {
7
+ WALL_DRAW_IDLE_CAPTION,
8
+ WALL_DRAW_VARIANTS,
9
+ type WallDrawVariant,
10
+ } from '../../../lib/wall-draw-variant'
11
+
12
+ /**
13
+ * The Build panel's grouped layout (Rooms / Add to rooms / Outdoor / Advanced),
14
+ * shared by the community and standalone editor Build tabs so both read the
15
+ * same. Hosts own what each tile arms; these only lay the groups out.
16
+ */
17
+ export function BuildPanelSection({
18
+ children,
19
+ hint,
20
+ id,
21
+ title,
22
+ }: {
23
+ children: ReactNode
24
+ hint?: string
25
+ id: string
26
+ title: string
27
+ }) {
28
+ return (
29
+ <section className="flex flex-col" data-build-group={id}>
30
+ <div className="mb-2 flex items-baseline justify-between gap-2 px-0.5">
31
+ <h3 className="font-medium text-foreground text-xs">{title}</h3>
32
+ {hint ? <span className="truncate text-[11px] text-muted-foreground">{hint}</span> : null}
33
+ </div>
34
+ {children}
35
+ </section>
36
+ )
37
+ }
38
+
39
+ const ADVANCED_OPEN_KEY = 'pascal-build-advanced-open'
40
+
41
+ function readAdvancedOpen(): boolean {
42
+ try {
43
+ return window.localStorage.getItem(ADVANCED_OPEN_KEY) === '1'
44
+ } catch {
45
+ return false
46
+ }
47
+ }
48
+
49
+ function writeAdvancedOpen(open: boolean) {
50
+ try {
51
+ window.localStorage.setItem(ADVANCED_OPEN_KEY, open ? '1' : '0')
52
+ } catch {
53
+ // Storage blocked (private window, preview): the section just starts closed.
54
+ }
55
+ }
56
+
57
+ /**
58
+ * The collapsible Advanced group. Remembers its open state per viewer, and
59
+ * opens itself when one of its tools becomes the armed one (e.g. by shortcut)
60
+ * so the lit tile is never hidden.
61
+ */
62
+ export function BuildPanelAdvancedSection({
63
+ children,
64
+ containsActiveTool,
65
+ description,
66
+ hint,
67
+ }: {
68
+ children: ReactNode
69
+ containsActiveTool: boolean
70
+ description?: string
71
+ hint?: string
72
+ }) {
73
+ const [open, setOpen] = useState(false)
74
+ useEffect(() => setOpen(readAdvancedOpen()), [])
75
+ useEffect(() => {
76
+ if (containsActiveTool) setOpen(true)
77
+ }, [containsActiveTool])
78
+
79
+ return (
80
+ <section className="flex flex-col" data-build-group="advanced">
81
+ <button
82
+ aria-expanded={open}
83
+ className="-mx-1.5 flex items-center gap-2 rounded-lg px-1.5 py-1.5 text-left transition-colors hover:bg-muted/60"
84
+ onClick={() => {
85
+ const next = !open
86
+ setOpen(next)
87
+ writeAdvancedOpen(next)
88
+ }}
89
+ type="button"
90
+ >
91
+ <ChevronRight
92
+ className={cn(
93
+ 'size-3 shrink-0 text-muted-foreground transition-transform duration-150',
94
+ open && 'rotate-90',
95
+ )}
96
+ />
97
+ <h3 className="font-medium text-foreground text-xs">Advanced</h3>
98
+ {hint ? (
99
+ <span className="ml-auto truncate text-[11px] text-muted-foreground">{hint}</span>
100
+ ) : null}
101
+ </button>
102
+ {open ? (
103
+ <div className="flex flex-col gap-2.5 pt-1.5">
104
+ {description ? (
105
+ <p className="px-0.5 text-[11px] text-muted-foreground leading-relaxed">
106
+ {description}
107
+ </p>
108
+ ) : null}
109
+ {children}
110
+ </div>
111
+ ) : null}
112
+ </section>
113
+ )
114
+ }
115
+
116
+ export function BuildToolGrid({ children, columns }: { children: ReactNode; columns: 3 | 4 }) {
117
+ return (
118
+ <div className={cn('grid gap-1.5', columns === 3 ? 'grid-cols-3' : 'grid-cols-4')}>
119
+ {children}
120
+ </div>
121
+ )
122
+ }
123
+
124
+ type BuildToolTileProps = Omit<ComponentPropsWithoutRef<'button'>, 'children' | 'type'> & {
125
+ active: boolean
126
+ /** Corner mark, e.g. the snap-target badge of wall-hosted tools. */
127
+ badge?: ReactNode
128
+ iconSrc: string
129
+ label: string
130
+ size?: 'lg' | 'sm'
131
+ tone?: 'primary' | 'neutral'
132
+ }
133
+
134
+ /**
135
+ * One tool tile: the isometric icon over its name. Inactive icons stay grey and
136
+ * take their colour on hover. Neutral tiles keep that behavior when armed.
137
+ */
138
+ export function BuildToolTile({
139
+ active,
140
+ badge,
141
+ className,
142
+ iconSrc,
143
+ label,
144
+ size = 'sm',
145
+ tone = 'primary',
146
+ ...buttonProps
147
+ }: BuildToolTileProps) {
148
+ return (
149
+ <button
150
+ aria-pressed={active}
151
+ className={cn(
152
+ 'group relative flex min-w-0 flex-col items-center justify-end rounded-xl transition-all duration-200',
153
+ // Small tiles grow to two label lines ("Spawn point"); a grid row's
154
+ // tiles stretch together, so a row keeps one height.
155
+ size === 'lg' ? 'h-[104px] gap-1.5 px-1.5 pt-2 pb-2.5' : 'min-h-[74px] gap-1 px-1 pt-1.5 pb-2',
156
+ active
157
+ ? tone === 'neutral'
158
+ ? 'bg-black/8 ring-1 ring-foreground/30 dark:bg-muted'
159
+ : 'bg-primary/10 ring-1 ring-primary/50'
160
+ : 'bg-black/4 hover:bg-black/8 dark:bg-muted/40 dark:hover:bg-muted',
161
+ className,
162
+ )}
163
+ type="button"
164
+ {...buttonProps}
165
+ >
166
+ <img
167
+ alt=""
168
+ className={cn(
169
+ 'min-h-0 object-contain transition duration-200 group-hover:scale-110',
170
+ size === 'lg' ? 'size-14' : 'size-9',
171
+ (!active || tone === 'neutral') &&
172
+ 'opacity-70 grayscale group-hover:opacity-100 group-hover:grayscale-0',
173
+ )}
174
+ draggable={false}
175
+ src={iconSrc}
176
+ />
177
+ <span
178
+ className={cn(
179
+ 'line-clamp-2 max-w-full text-center font-medium leading-tight',
180
+ size === 'lg' ? 'text-xs' : 'text-[11px]',
181
+ active ? 'text-foreground' : 'text-muted-foreground group-hover:text-foreground',
182
+ )}
183
+ >
184
+ {label}
185
+ </span>
186
+ {badge ? <span className="absolute top-1 right-1">{badge}</span> : null}
187
+ </button>
188
+ )
189
+ }
190
+
191
+ /** The line under the Rooms tiles: what the armed shape does, lead in bold. */
192
+ export function BuildPanelCaption({ lead, rest }: { lead?: string; rest: string }) {
193
+ return (
194
+ <p className="mt-2 min-h-[2lh] px-0.5 text-[11px] text-muted-foreground leading-relaxed">
195
+ {lead ? <span className="font-medium text-foreground">{lead} </span> : null}
196
+ {rest}
197
+ </p>
198
+ )
199
+ }
200
+
201
+ /**
202
+ * The Rooms group: one large tile per wall drawing variant, and the armed
203
+ * variant's caption. `activeVariant` is null while the wall tool is not armed.
204
+ */
205
+ export function BuildPanelRoomsSection({
206
+ activeVariant,
207
+ onHover,
208
+ onSelect,
209
+ tileProps,
210
+ }: {
211
+ activeVariant: WallDrawVariant | null
212
+ onHover?: (variant: WallDrawVariant) => void
213
+ onSelect: (variant: WallDrawVariant) => void
214
+ /** Extra attributes per tile (e.g. a host's guide-target hook). */
215
+ tileProps?: (variant: WallDrawVariant) => Record<string, string | undefined>
216
+ }) {
217
+ const active = WALL_DRAW_VARIANTS.find((variant) => variant.id === activeVariant)
218
+ return (
219
+ <BuildPanelSection hint="walls · floor · ceiling" id="rooms" title="Rooms">
220
+ <BuildToolGrid columns={3}>
221
+ {WALL_DRAW_VARIANTS.map((variant) => (
222
+ <BuildToolTile
223
+ active={variant.id === activeVariant}
224
+ data-build-tool={`room-${variant.id}`}
225
+ iconSrc={variant.iconSrc}
226
+ key={variant.id}
227
+ label={variant.label}
228
+ onClick={() => onSelect(variant.id)}
229
+ onMouseEnter={() => onHover?.(variant.id)}
230
+ size="lg"
231
+ title={variant.title}
232
+ {...tileProps?.(variant.id)}
233
+ />
234
+ ))}
235
+ </BuildToolGrid>
236
+ <BuildPanelCaption
237
+ lead={active?.caption.lead}
238
+ rest={active ? active.caption.rest : WALL_DRAW_IDLE_CAPTION}
239
+ />
240
+ </BuildPanelSection>
241
+ )
242
+ }
@@ -1,7 +1,6 @@
1
1
  'use client'
2
2
 
3
- import type { AnyNodeId } from '@pascal-app/core'
4
- import { DEFAULT_LEVEL_HEIGHT, LevelNode, type UnitNode, useScene } from '@pascal-app/core'
3
+ import { type AnyNodeId, type LevelNode, type UnitNode, useScene } from '@pascal-app/core'
5
4
  import { useViewer } from '@pascal-app/viewer'
6
5
  import {
7
6
  AppWindow,
@@ -31,15 +30,16 @@ import {
31
30
  Redo2,
32
31
  Sparkles,
33
32
  Square,
34
- SquareStack,
35
33
  Sun,
36
34
  Trash2,
37
35
  Undo2,
38
36
  Video,
39
37
  } from 'lucide-react'
40
38
  import { useEffect } from 'react'
39
+ import { addStoreyOnTop } from '../../../lib/add-storey'
41
40
  import { getHistoryCommandState, runRedo, runUndo } from '../../../lib/history'
42
41
  import { deleteLevelWithFallbackSelection } from '../../../lib/level-selection'
42
+ import { getSavedSceneDocument } from '../../../lib/scene'
43
43
  import { createUnitInBuilding, enterUnitFocus, leaveUnitFocus } from '../../../lib/units'
44
44
  import { useCommandRegistry } from '../../../store/use-command-registry'
45
45
  import type { StructureTool } from '../../../store/use-editor'
@@ -50,10 +50,8 @@ export function EditorCommands() {
50
50
  const register = useCommandRegistry((s) => s.register)
51
51
  const { navigateTo, setInputValue, setOpen } = useCommandPalette()
52
52
 
53
- const setPhase = useEditor((s) => s.setPhase)
54
53
  const armToolMode = useEditor((s) => s.armToolMode)
55
54
  const armMaterialPaint = useEditor((s) => s.armMaterialPaint)
56
- const setStructureLayer = useEditor((s) => s.setStructureLayer)
57
55
  const isPreviewMode = useEditor((s) => s.isPreviewMode)
58
56
  const setPreviewMode = useEditor((s) => s.setPreviewMode)
59
57
 
@@ -79,12 +77,7 @@ export function EditorCommands() {
79
77
  )
80
78
 
81
79
  const activateTool = (tool: StructureTool) => {
82
- run(() => {
83
- setPhase('structure')
84
- if (tool === 'zone') setStructureLayer('zones')
85
- else setStructureLayer('elements')
86
- armToolMode({ mode: 'build', tool })
87
- })
80
+ run(() => armToolMode({ mode: 'build', tool }))
88
81
  }
89
82
 
90
83
  return register([
@@ -175,11 +168,7 @@ export function EditorCommands() {
175
168
  keywords: ['paint', 'material', 'texture', 'bucket', 'surface'],
176
169
  shortcut: ['P'],
177
170
  execute: () =>
178
- run(() => {
179
- setPhase('structure')
180
- setStructureLayer('elements')
181
- armMaterialPaint()
182
- }),
171
+ run(() => armMaterialPaint()),
183
172
  },
184
173
  {
185
174
  id: 'editor.mode.terrain-sculpt',
@@ -214,16 +203,7 @@ export function EditorCommands() {
214
203
  const { nodes } = useScene.getState()
215
204
  const building = Object.values(nodes).find((n) => n.type === 'building')
216
205
  if (!building) return
217
- const levelCount = building.children.filter(
218
- (childId) => nodes[childId as keyof typeof nodes]?.type === 'level',
219
- ).length
220
- const newLevel = LevelNode.parse({
221
- level: levelCount,
222
- height: DEFAULT_LEVEL_HEIGHT,
223
- children: [],
224
- parentId: building.id,
225
- })
226
- useScene.getState().createNode(newLevel, building.id)
206
+ const newLevel = addStoreyOnTop(building.id)
227
207
  useViewer.getState().setSelection({ levelId: newLevel.id })
228
208
  }),
229
209
  },
@@ -315,19 +295,6 @@ export function EditorCommands() {
315
295
  navigate: true,
316
296
  execute: () => navigateTo('wall-mode'),
317
297
  },
318
- {
319
- id: 'editor.viewer.level-mode',
320
- label: 'Level Mode',
321
- group: 'Viewer Controls',
322
- icon: <SquareStack className="h-4 w-4" />,
323
- keywords: ['level', 'floor', 'exploded', 'stacked', 'solo'],
324
- badge: () => {
325
- const mode = useViewer.getState().levelMode
326
- return { manual: 'Manual', stacked: 'Stacked', exploded: 'Exploded', solo: 'Solo' }[mode]
327
- },
328
- navigate: true,
329
- execute: () => navigateTo('level-mode'),
330
- },
331
298
  {
332
299
  id: 'editor.viewer.camera-mode',
333
300
  label: () => {
@@ -422,7 +389,7 @@ export function EditorCommands() {
422
389
  keywords: ['export', 'download', 'json', 'save', 'data'],
423
390
  execute: () =>
424
391
  run(() => {
425
- const { nodes, rootNodeIds } = useScene.getState()
392
+ const { nodes, rootNodeIds } = getSavedSceneDocument()
426
393
  const blob = new Blob([JSON.stringify({ nodes, rootNodeIds }, null, 2)], {
427
394
  type: 'application/json',
428
395
  })
@@ -476,10 +443,8 @@ export function EditorCommands() {
476
443
  navigateTo,
477
444
  setInputValue,
478
445
  setOpen,
479
- setPhase,
480
446
  armToolMode,
481
447
  armMaterialPaint,
482
- setStructureLayer,
483
448
  isPreviewMode,
484
449
  setPreviewMode,
485
450
  exportScene,
@@ -3,15 +3,15 @@
3
3
  import type { AnyNodeId, LevelNode } from '@pascal-app/core'
4
4
  import { useScene } from '@pascal-app/core'
5
5
  import { markPerfAction, useViewer } from '@pascal-app/viewer'
6
- import { Command, useCommandState } from 'cmdk'
6
+ import { Command, defaultFilter, useCommandState } from 'cmdk'
7
7
  import { ChevronRight, Search } from 'lucide-react'
8
- import type { ReactNode } from 'react'
9
- import { useEffect, useState } from 'react'
8
+ import type { ReactNode, RefObject } from 'react'
9
+ import { useEffect, useLayoutEffect, useRef, useState } from 'react'
10
10
  import { create } from 'zustand'
11
11
  import { useShallow } from 'zustand/shallow'
12
12
  import { Dialog, DialogContent, DialogTitle } from './../../../components/ui/primitives/dialog'
13
13
  import { getLevelDisplayName } from '@pascal-app/core'
14
- import { useCommandRegistry } from '../../../store/use-command-registry'
14
+ import { type CommandAction, useCommandRegistry } from '../../../store/use-command-registry'
15
15
  import { usePaletteViewRegistry } from '../../../store/use-palette-view-registry'
16
16
 
17
17
  // ---------------------------------------------------------------------------
@@ -54,6 +54,28 @@ function resolve(value: string | (() => string)): string {
54
54
  return typeof value === 'function' ? value() : value
55
55
  }
56
56
 
57
+ type ActionGroup = [group: string, actions: CommandAction[]]
58
+
59
+ /**
60
+ * The registered actions by group, in the order they were registered. A search puts the group
61
+ * holding the best match first: cmdk ranks the items inside a group but leaves the groups where
62
+ * they are, so a strong match could sit below a screen of weak ones.
63
+ */
64
+ export function rankedGroups(actions: CommandAction[], search: string): ActionGroup[] {
65
+ const groups = new Map<string, CommandAction[]>()
66
+ for (const action of actions) {
67
+ groups.set(action.group, [...(groups.get(action.group) ?? []), action])
68
+ }
69
+ const ordered: ActionGroup[] = [...groups]
70
+ if (!search) return ordered
71
+ const bestMatch = ([, list]: ActionGroup) =>
72
+ Math.max(...list.map((action) => defaultFilter(resolve(action.label), search, action.keywords)))
73
+ return ordered
74
+ .map((entry) => ({ entry, score: bestMatch(entry) }))
75
+ .sort((a, b) => b.score - a.score)
76
+ .map(({ entry }) => entry)
77
+ }
78
+
57
79
  function Shortcut({ keys }: { keys: string[] }) {
58
80
  return (
59
81
  <span className="ml-auto flex shrink-0 items-center gap-0.5">
@@ -149,7 +171,6 @@ function OptionItem({
149
171
  // ---------------------------------------------------------------------------
150
172
  const PAGE_LABEL: Record<string, string> = {
151
173
  'wall-mode': 'Wall Mode',
152
- 'level-mode': 'Level Mode',
153
174
  'rename-level': 'Rename Level',
154
175
  'goto-level': 'Go to Level',
155
176
  }
@@ -163,6 +184,20 @@ export interface CommandPaletteEmptyAction {
163
184
  onSelect: (query: string) => void
164
185
  }
165
186
 
187
+ /**
188
+ * A search selects the first item; this scrolls the list back to the top to show it. cmdk scrolls
189
+ * before the list re-renders the selection, so once a search has moved the groups it scrolls to
190
+ * wherever the item selected before went. Rendered after the list, whose ref is set by then.
191
+ */
192
+ function TopOnFirstItem({ list }: { list: RefObject<HTMLDivElement | null> }) {
193
+ const value = useCommandState((s) => s.value)
194
+ useLayoutEffect(() => {
195
+ const first = list.current?.querySelector('[cmdk-item]:not([aria-disabled="true"])')
196
+ if (value && first?.getAttribute('aria-selected') === 'true') list.current?.scrollTo({ top: 0 })
197
+ }, [list, value])
198
+ return null
199
+ }
200
+
166
201
  function EmptyActionItem({ action }: { action: CommandPaletteEmptyAction }) {
167
202
  const count = useCommandState((s) => s.filtered.count)
168
203
  const search = useCommandState((s) => s.search)
@@ -194,6 +229,7 @@ export function CommandPalette({ emptyAction }: { emptyAction?: CommandPaletteEm
194
229
 
195
230
  const [meta, setMeta] = useState('⌘')
196
231
  const [isFullscreen, setIsFullscreen] = useState(false)
232
+ const listRef = useRef<HTMLDivElement>(null)
197
233
 
198
234
  const page = pages[pages.length - 1]
199
235
 
@@ -204,8 +240,6 @@ export function CommandPalette({ emptyAction }: { emptyAction?: CommandPaletteEm
204
240
 
205
241
  const wallMode = useViewer((s) => s.wallMode)
206
242
  const setWallMode = useViewer((s) => s.setWallMode)
207
- const levelMode = useViewer((s) => s.levelMode)
208
- const setLevelMode = useViewer((s) => s.setLevelMode)
209
243
 
210
244
  const allLevels = useScene(
211
245
  useShallow((s) =>
@@ -250,12 +284,6 @@ export function CommandPalette({ emptyAction }: { emptyAction?: CommandPaletteEm
250
284
  down: 'Down',
251
285
  translucent: 'Translucent',
252
286
  }
253
- const levelModeLabel: Record<'manual' | 'stacked' | 'exploded' | 'solo', string> = {
254
- manual: 'Manual',
255
- stacked: 'Stacked',
256
- exploded: 'Exploded',
257
- solo: 'Solo',
258
- }
259
287
 
260
288
  const confirmRename = () => {
261
289
  if (!(activeLevelId && inputValue.trim())) return
@@ -264,15 +292,7 @@ export function CommandPalette({ emptyAction }: { emptyAction?: CommandPaletteEm
264
292
  })
265
293
  }
266
294
 
267
- // ---------------------------------------------------------------------------
268
- // Group registered actions by group (preserving insertion order)
269
- // ---------------------------------------------------------------------------
270
- const grouped = actions.reduce<Map<string, typeof actions>>((acc, action) => {
271
- const list = acc.get(action.group) ?? []
272
- list.push(action)
273
- acc.set(action.group, list)
274
- return acc
275
- }, new Map())
295
+ const grouped = rankedGroups(actions, inputValue)
276
296
 
277
297
  const onClose = () => setOpen(false)
278
298
  const onBack = () => {
@@ -335,7 +355,7 @@ export function CommandPalette({ emptyAction }: { emptyAction?: CommandPaletteEm
335
355
  />
336
356
  </div>
337
357
 
338
- <Command.List className="max-h-100 overflow-y-auto p-1.5">
358
+ <Command.List className="max-h-100 overflow-y-auto p-1.5" ref={listRef}>
339
359
  {(!emptyAction || page) && (
340
360
  <Command.Empty className="py-8 text-center text-muted-foreground text-sm">
341
361
  No commands found.
@@ -353,7 +373,7 @@ export function CommandPalette({ emptyAction }: { emptyAction?: CommandPaletteEm
353
373
 
354
374
  {/* ── Root view: render from registry ───────────────────────── */}
355
375
  {!page &&
356
- Array.from(grouped.entries()).map(([group, groupActions]) => (
376
+ grouped.map(([group, groupActions]) => (
357
377
  <Command.Group heading={group} key={group}>
358
378
  {groupActions.map((action) => (
359
379
  <Item
@@ -364,7 +384,11 @@ export function CommandPalette({ emptyAction }: { emptyAction?: CommandPaletteEm
364
384
  keywords={action.keywords}
365
385
  label={action.label}
366
386
  navigate={action.navigate}
367
- onSelect={() => action.execute()}
387
+ // A registered command that does not navigate within the
388
+ // palette closes it, like the built-ins — otherwise the
389
+ // dialog overlay keeps swallowing clicks under a full-screen
390
+ // surface the command opened (Sheets).
391
+ onSelect={() => (action.navigate ? action.execute() : run(() => action.execute()))}
368
392
  shortcut={action.shortcut}
369
393
  />
370
394
  ))}
@@ -385,20 +409,6 @@ export function CommandPalette({ emptyAction }: { emptyAction?: CommandPaletteEm
385
409
  </Command.Group>
386
410
  )}
387
411
 
388
- {/* ── Level Mode sub-page ───────────────────────────────────── */}
389
- {page === 'level-mode' && (
390
- <Command.Group heading="Level Mode">
391
- {(['stacked', 'exploded', 'solo'] as const).map((mode) => (
392
- <OptionItem
393
- isActive={levelMode === mode}
394
- key={mode}
395
- label={levelModeLabel[mode]}
396
- onSelect={() => run(() => setLevelMode(mode))}
397
- />
398
- ))}
399
- </Command.Group>
400
- )}
401
-
402
412
  {/* ── Go to Level sub-page ──────────────────────────────────── */}
403
413
  {page === 'goto-level' && (
404
414
  <Command.Group heading="Go to Level">
@@ -458,6 +468,7 @@ export function CommandPalette({ emptyAction }: { emptyAction?: CommandPaletteEm
458
468
  </Command.Group>
459
469
  )}
460
470
  </Command.List>
471
+ <TopOnFirstItem list={listRef} />
461
472
 
462
473
  {/* Footer hint */}
463
474
  <div className="flex items-center justify-between border-border/50 border-t px-3 py-2">
@@ -0,0 +1,31 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+ import { rankedGroups } from '.'
3
+
4
+ const action = (group: string, label: string, keywords?: string[]) => ({
5
+ id: label,
6
+ group,
7
+ label,
8
+ keywords,
9
+ execute: () => {},
10
+ })
11
+
12
+ // What a host's palette holds: weak fuzzy matches for "theme" registered before the strong one.
13
+ const actions = [
14
+ action('Generate', 'Generate house from template: The Poppy', ['template', 'adu', 'poppy']),
15
+ action('Viewer Controls', 'Switch to Solid', ['solid', 'shading', 'render', 'mode']),
16
+ action('View', 'Use dark theme', ['theme', 'appearance', 'interface']),
17
+ action('Export & Share', 'Take Screenshot', ['screenshot', 'capture', 'image']),
18
+ ]
19
+
20
+ const order = (search: string) => rankedGroups(actions, search).map(([group]) => group)
21
+
22
+ describe('command palette groups', () => {
23
+ test('a search lists the group with the best match first', () => {
24
+ expect(order('theme')[0]).toBe('View')
25
+ expect(order('solid')[0]).toBe('Viewer Controls')
26
+ })
27
+
28
+ test('with no search the groups keep the order they were registered in', () => {
29
+ expect(order('')).toEqual(['Generate', 'Viewer Controls', 'View', 'Export & Share'])
30
+ })
31
+ })
@@ -12,12 +12,15 @@ export function ActionButton({ icon, label, className, ...props }: ActionButtonP
12
12
  <button
13
13
  {...props}
14
14
  className={cn(
15
- 'flex h-9 flex-1 items-center justify-center gap-1.5 rounded-lg border border-border/50 bg-[#2C2C2E] px-3 font-medium text-foreground text-xs transition-colors hover:bg-[#3e3e3e] active:bg-[#3e3e3e]',
15
+ // The fill runs through custom properties so a caller's `bg-*` or
16
+ // `hover:bg-*` replaces it in both themes; tailwind-merge would keep a
17
+ // `dark:bg-*` fill alive under the caller's class.
18
+ 'flex min-h-9 min-w-0 flex-1 shrink-0 items-center justify-center gap-1.5 rounded-lg border border-border/50 bg-(--action-fill) px-3 py-2 font-medium text-foreground text-xs leading-4 transition-colors [&>svg]:shrink-0 [--action-fill-hover:rgb(0_0_0/0.08)] [--action-fill:rgb(0_0_0/0.04)] hover:bg-(--action-fill-hover) active:bg-(--action-fill-hover) dark:[--action-fill-hover:#3e3e3e] dark:[--action-fill:#2C2C2E]',
16
19
  className,
17
20
  )}
18
21
  >
19
22
  {icon}
20
- <span>{label}</span>
23
+ <span className="min-w-0 whitespace-normal text-center">{label}</span>
21
24
  </button>
22
25
  )
23
26
  }