@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
@@ -2,10 +2,10 @@
2
2
 
3
3
  import { X } from 'lucide-react'
4
4
  import { AnimatePresence, motion } from 'motion/react'
5
- import Image from 'next/image'
6
5
  import { type ReactNode, useEffect, useState } from 'react'
7
6
  import { createPortal } from 'react-dom'
8
7
  import useEditor from '../../../store/use-editor'
8
+ import { NodeIconImage } from '../node-icon-image'
9
9
 
10
10
  interface MobilePanelSheetProps {
11
11
  open: boolean
@@ -56,7 +56,7 @@ export function MobilePanelSheet({ open, onClose, icon, title, children }: Mobil
56
56
  {open && (
57
57
  <motion.div
58
58
  animate={{ y: 0 }}
59
- className="dark fixed right-0 bottom-0 left-0 z-[60] flex flex-col overflow-hidden rounded-t-2xl bg-sidebar text-sidebar-foreground shadow-[0_-8px_24px_rgba(0,0,0,0.24)]"
59
+ className="fixed right-0 bottom-0 left-0 z-[60] flex flex-col overflow-hidden rounded-t-2xl bg-sidebar text-sidebar-foreground shadow-[0_-8px_24px_rgba(0,0,0,0.24)]"
60
60
  drag="y"
61
61
  dragConstraints={{ top: 0, bottom: 0 }}
62
62
  dragElastic={{ top: 0, bottom: 0.4 }}
@@ -75,7 +75,7 @@ export function MobilePanelSheet({ open, onClose, icon, title, children }: Mobil
75
75
  <div className="flex shrink-0 items-center justify-between border-border/50 border-b px-3 pt-1 pb-3">
76
76
  <div className="flex min-w-0 items-center gap-2">
77
77
  {icon && (
78
- <Image
78
+ <NodeIconImage
79
79
  alt=""
80
80
  className="shrink-0 object-contain"
81
81
  height={18}
@@ -89,7 +89,7 @@ export function MobilePanelSheet({ open, onClose, icon, title, children }: Mobil
89
89
  </div>
90
90
  <button
91
91
  aria-label="Close"
92
- className="flex h-8 w-8 items-center justify-center rounded-md bg-[#2C2C2E] text-muted-foreground transition-colors hover:bg-[#3e3e3e] hover:text-foreground"
92
+ className="flex h-8 w-8 items-center justify-center rounded-md bg-black/4 dark:bg-[#2C2C2E] text-muted-foreground transition-colors hover:bg-black/8 dark:hover:bg-[#3e3e3e] hover:text-foreground"
93
93
  onClick={onClose}
94
94
  type="button"
95
95
  >
@@ -2,9 +2,9 @@
2
2
 
3
3
  import type { AnyNode } from '@pascal-app/core'
4
4
  import { Copy, Move, SlidersHorizontal, Trash2 } from 'lucide-react'
5
- import Image from 'next/image'
6
5
  import type { MouseEventHandler } from 'react'
7
6
  import { cn } from '../../../lib/utils'
7
+ import { NodeIconImage } from '../node-icon-image'
8
8
  import { getNodeDisplay } from './node-display'
9
9
 
10
10
  interface MobileSelectionBarProps {
@@ -18,7 +18,7 @@ interface MobileSelectionBarProps {
18
18
  }
19
19
 
20
20
  const ACTION_BTN =
21
- 'flex h-9 w-9 items-center justify-center rounded-lg text-muted-foreground transition-colors hover:bg-white/8 hover:text-foreground'
21
+ 'flex h-9 w-9 items-center justify-center rounded-lg text-muted-foreground transition-colors hover:bg-black/5 dark:hover:bg-white/8 hover:text-foreground'
22
22
 
23
23
  export function MobileSelectionBar({
24
24
  node,
@@ -40,12 +40,12 @@ export function MobileSelectionBar({
40
40
  <button
41
41
  aria-label={`Edit ${resolvedLabel}`}
42
42
  className={cn(
43
- 'flex min-w-0 flex-1 items-center gap-2 rounded-lg px-2 text-left transition-colors hover:bg-white/8',
43
+ 'flex min-w-0 flex-1 items-center gap-2 rounded-lg px-2 text-left transition-colors hover:bg-black/5 dark:hover:bg-white/8',
44
44
  )}
45
45
  onClick={onEdit}
46
46
  type="button"
47
47
  >
48
- <Image
48
+ <NodeIconImage
49
49
  alt=""
50
50
  className="shrink-0 rounded object-contain"
51
51
  height={20}
@@ -80,7 +80,10 @@ export function MobileSelectionBar({
80
80
  </button>
81
81
  <button
82
82
  aria-label="Delete"
83
- className={cn(ACTION_BTN, 'hover:bg-red-500/15 hover:text-red-400')}
83
+ className={cn(
84
+ ACTION_BTN,
85
+ 'hover:bg-red-500/15 hover:text-red-600 dark:hover:bg-red-500/15 dark:hover:text-red-400',
86
+ )}
84
87
  onClick={(e) => {
85
88
  stop(e)
86
89
  onDelete()
@@ -5,6 +5,9 @@ import {
5
5
  BuildingNode,
6
6
  clearSceneHistory,
7
7
  LevelNode,
8
+ planStairFlightHeightEdit,
9
+ StairNode,
10
+ StairSegmentNode,
8
11
  useLiveNodeOverrides,
9
12
  useScene,
10
13
  WallNode,
@@ -124,9 +127,7 @@ describe('buildMultiNodePatches', () => {
124
127
  nodes,
125
128
  {
126
129
  derive: (next) => ({ thickness: (next as { height: number }).height / 10 }),
127
- reconcile: (prev) => [
128
- { id: `${prev.id}_follow` as AnyNodeId, data: { height: 4 } },
129
- ],
130
+ reconcile: (prev) => [{ id: `${prev.id}_follow` as AnyNodeId, data: { height: 4 } }],
130
131
  },
131
132
  )
132
133
  expect(patches).toEqual([
@@ -207,3 +208,25 @@ describe('commitMultiNodeFields', () => {
207
208
  expect((useScene.getState().nodes[WALL_C] as { height?: number }).height).toBe(2.7)
208
209
  })
209
210
  })
211
+
212
+ test('multi-flight edits reconcile their parent from the complete proposed scene', () => {
213
+ const first = StairSegmentNode.parse({ height: 1 })
214
+ const second = StairSegmentNode.parse({ height: 3 })
215
+ const stair = StairNode.parse({ totalRise: 4, children: [first.id, second.id] })
216
+ first.parentId = stair.id
217
+ second.parentId = stair.id
218
+ const nodes: Record<string, AnyNode> = Object.fromEntries(
219
+ [stair, first, second].map((node) => [node.id, node]),
220
+ )
221
+ const patches = buildMultiNodePatches([first.id, second.id], () => ({ height: 2 }), nodes, {
222
+ reconcile: (_prev, next, proposed) =>
223
+ next.type === 'stair-segment'
224
+ ? planStairFlightHeightEdit(next, next.height, proposed!).slice(1)
225
+ : [],
226
+ })
227
+ const result = { ...nodes }
228
+ for (const patch of patches) result[patch.id] = { ...result[patch.id], ...patch.data } as AnyNode
229
+ expect((result[first.id] as StairSegmentNode).height).toBe(2)
230
+ expect((result[second.id] as StairSegmentNode).height).toBe(2)
231
+ expect((result[stair.id] as StairNode).totalRise).toBe(4)
232
+ })
@@ -125,9 +125,12 @@ export function buildMultiNodePatches(
125
125
  patch = { ...patch, ...parametrics.derive(next, patch, node) } as Partial<AnyNode>
126
126
  }
127
127
  updates.push({ id, data: patch })
128
- if (parametrics?.reconcile) {
129
- const next = { ...node, ...patch } as AnyNode
130
- followUps.push(...parametrics.reconcile(node, next))
128
+ }
129
+ if (parametrics?.reconcile) {
130
+ const proposed = { ...nodes }
131
+ for (const { id, data } of updates) proposed[id] = { ...nodes[id], ...data } as AnyNode
132
+ for (const { id } of updates) {
133
+ followUps.push(...parametrics.reconcile(nodes[id]!, proposed[id]!, proposed))
131
134
  }
132
135
  }
133
136
  return [...updates, ...followUps]
@@ -11,9 +11,11 @@ import {
11
11
  import { useViewer } from '@pascal-app/viewer'
12
12
  import { useCallback, useMemo } from 'react'
13
13
  import { useShallow } from 'zustand/react/shallow'
14
+ import { getNodePanelModel } from '../../../lib/panel-rows'
14
15
  import { selectionMatchesSessionGroup } from '../../../lib/session-groups'
15
16
  import useSessionGroups from '../../../store/use-session-groups'
16
17
  import { PanelSection } from '../controls/panel-section'
18
+ import { SegmentedControl } from '../controls/segmented-control'
17
19
  import { SliderControl } from '../controls/slider-control'
18
20
  import { resolveUniqueSelectionIds } from './homogeneous-selection'
19
21
  import {
@@ -52,6 +54,7 @@ export function MultiParametricInspector({ footer }: { footer?: React.ReactNode
52
54
 
53
55
  const def = nodeType ? nodeRegistry.get(nodeType) : undefined
54
56
  const parametrics = def?.parametrics as ParametricDescriptor<AnyNode> | undefined
57
+ const hasKindControls = !!getNodePanelModel(def)?.multiControls
55
58
 
56
59
  const handleClose = useCallback(() => {
57
60
  setSelection({ selectedIds: [] })
@@ -79,7 +82,11 @@ export function MultiParametricInspector({ footer }: { footer?: React.ReactNode
79
82
  nodeType={nodeType}
80
83
  parametrics={parametrics}
81
84
  title={group.label}
82
- />
85
+ >
86
+ {hasKindControls ? (
87
+ <MultiKindControls nodeIds={nodeIds} nodeType={nodeType} section={group.label} />
88
+ ) : null}
89
+ </MultiGroupFields>
83
90
  ))}
84
91
  <div className="border-border/50 border-t p-3">
85
92
  <MultiSelectionActions />
@@ -88,6 +95,46 @@ export function MultiParametricInspector({ footer }: { footer?: React.ReactNode
88
95
  )
89
96
  }
90
97
 
98
+ /**
99
+ * The kind's own multi-selection controls for one section (the wall's
100
+ * Reference), from its panel model — the same segmented control the single
101
+ * panel shows, mixed when the selection disagrees.
102
+ */
103
+ function MultiKindControls({
104
+ nodeIds,
105
+ nodeType,
106
+ section,
107
+ }: {
108
+ nodeIds: AnyNodeId[]
109
+ nodeType: AnyNode['type']
110
+ section: string
111
+ }) {
112
+ const nodes = useScene((s) => s.nodes)
113
+ const controls = useMemo(() => {
114
+ const model = getNodePanelModel(nodeRegistry.get(nodeType))
115
+ if (!model?.multiControls) return []
116
+ const selection = nodeIds.flatMap((id) => (nodes[id] ? [nodes[id]!] : []))
117
+ return model.multiControls({ selection, nodes }).filter((control) => control.section === section)
118
+ }, [nodeIds, nodeType, nodes, section])
119
+ return (
120
+ <>
121
+ {controls.map((control) => (
122
+ <div className="flex flex-col gap-1" key={control.id}>
123
+ <div className="px-1 font-medium text-[10px] text-muted-foreground/80">
124
+ {control.label}
125
+ </div>
126
+ <SegmentedControl
127
+ mixed={control.value === null}
128
+ onChange={control.onChange}
129
+ options={control.options.map((option) => ({ ...option }))}
130
+ value={control.value ?? ''}
131
+ />
132
+ </div>
133
+ ))}
134
+ </>
135
+ )
136
+ }
137
+
91
138
  function MultiGroupFields({
92
139
  defaultExpanded,
93
140
  title,
@@ -95,7 +142,9 @@ function MultiGroupFields({
95
142
  nodeIds,
96
143
  nodeType,
97
144
  parametrics,
145
+ children,
98
146
  }: {
147
+ children?: React.ReactNode
99
148
  defaultExpanded?: boolean
100
149
  title: string
101
150
  fields: ParamField<AnyNode>[]
@@ -109,7 +158,7 @@ function MultiGroupFields({
109
158
  fieldVisibleForAll(nodeIds, (field as { visibleIf?: (n: AnyNode) => boolean }).visibleIf, s.nodes),
110
159
  ),
111
160
  )
112
- if (genericFields.length === 0 || !anyVisible) return null
161
+ if ((genericFields.length === 0 || !anyVisible) && !children) return null
113
162
  return (
114
163
  <PanelSection defaultExpanded={defaultExpanded} title={title}>
115
164
  {genericFields.map((field, fi) => {
@@ -139,6 +188,7 @@ function MultiGroupFields({
139
188
  />
140
189
  )
141
190
  })}
191
+ {children}
142
192
  </PanelSection>
143
193
  )
144
194
  }
@@ -1,10 +1,22 @@
1
1
  'use client'
2
2
 
3
- import { type AnyNodeId, useScene } from '@pascal-app/core'
3
+ import {
4
+ type AnyNode,
5
+ type AnyNodeId,
6
+ type BuildingNode,
7
+ resolveBuildingForLevel,
8
+ runAsSingleSceneHistoryStep,
9
+ useScene,
10
+ } from '@pascal-app/core'
11
+ import { unwrapCorner, windowMergeOffer } from '@pascal-app/core/building'
4
12
  import { useViewer } from '@pascal-app/viewer'
5
- import { Copy, Group, Trash2, Ungroup } from 'lucide-react'
13
+ import { Building2, Copy, Group, Merge, Split, Trash2, Ungroup } from 'lucide-react'
6
14
  import { useMemo } from 'react'
7
15
  import { deleteSelection, duplicateSelectionAndPickUp } from '../../editor/group-actions'
16
+ import { formatLinearMeasurement } from '../../../lib/measurements'
17
+ import { triggerSFX } from '../../../lib/sfx-bus'
18
+ import { writeWindowChanges } from '../../../lib/window-changes'
19
+ import { collectSelectableCandidateIds } from '../../tools/select/select-candidates'
8
20
  import {
9
21
  canCreateSessionGroup,
10
22
  selectionIntersectsSessionGroup,
@@ -18,6 +30,135 @@ import { ActionButton, ActionGroup } from '../controls/action-button'
18
30
  import { PanelWrapper } from './panel-wrapper'
19
31
  import { formatSelectionBreakdown } from './selection-breakdown'
20
32
 
33
+ /** The building holding the level, when the selection is everything selectable on it. */
34
+ function useWholeLevelBuildingId(selectedIds: readonly string[]) {
35
+ const levelId = useViewer((s) => s.selection.levelId)
36
+ const nodes = useScene((s) => s.nodes)
37
+ return useMemo(() => {
38
+ if (!levelId) return null
39
+ const candidates = collectSelectableCandidateIds()
40
+ if (candidates.length === 0) return null
41
+ const selected = new Set(selectedIds)
42
+ if (!candidates.every((id) => selected.has(id))) return null
43
+ return resolveBuildingForLevel(levelId as AnyNodeId, nodes) ?? null
44
+ }, [levelId, nodes, selectedIds])
45
+ }
46
+
47
+ /** The building's own selection (no level): its floating pill offers the whole-building Move. */
48
+ function selectBuilding(buildingId: AnyNodeId) {
49
+ useViewer.getState().setSelection({ buildingId: buildingId as BuildingNode['id'] })
50
+ }
51
+
52
+ /**
53
+ * Why two selected windows will not merge, for the person who selected them: the refusal's own
54
+ * message is the agents' (node ids, metres), so the panel words its code, in the editor's units.
55
+ */
56
+ function mergeRefusalNote(
57
+ refusal: { code: string; details: Record<string, unknown> },
58
+ nodes: Record<string, AnyNode>,
59
+ length: (meters: number) => string,
60
+ ): string {
61
+ const { blockingId, limit } = refusal.details as { blockingId?: string; limit?: number }
62
+ const reach = length(limit ?? 0)
63
+ const blocking = nodes[blockingId ?? '']
64
+ const blockingKind = !blocking
65
+ ? 'something'
66
+ : blocking.type === 'item'
67
+ ? 'an item'
68
+ : `a ${blocking.type}`
69
+ switch (refusal.code) {
70
+ case 'window_already_joined':
71
+ return 'One of them already wraps another corner. Unwrap it first.'
72
+ case 'window_not_on_wall':
73
+ return 'A roof or dormer window cannot join another window.'
74
+ case 'windows_not_adjacent':
75
+ return `They are too far apart. Windows merge with at most ${reach} between them.`
76
+ case 'opening_between':
77
+ return `There is ${blockingKind} between them.`
78
+ case 'walls_not_meeting':
79
+ return 'Their walls do not meet at a corner.'
80
+ case 'corner_angle':
81
+ return 'Their walls meet at too sharp or too flat an angle for a corner window.'
82
+ case 'window_not_at_corner':
83
+ return `One of them is too far from the corner. A window joins one within ${reach} of it.`
84
+ case 'height_exceeds_wall':
85
+ return 'The window selected first is taller than the other wall. Select the other one first.'
86
+ case 'opening_overlap':
87
+ return `Sliding them to the corner would overlap ${blockingKind}.`
88
+ default:
89
+ return 'These windows cannot be merged.'
90
+ }
91
+ }
92
+
93
+ /**
94
+ * Merge windows: with exactly two windows selected, one window across both, or one corner window
95
+ * when they sit on two walls that meet. Windows ride their wall, so they have no floating group
96
+ * menu, and this is where the wall merge's twin lives. Two that already wrap their corner get
97
+ * Unwrap instead; two that cannot join, the reason.
98
+ */
99
+ function MergeWindowsAction() {
100
+ const selectedIds = useViewer((s) => s.selection.selectedIds)
101
+ const nodes = useScene((s) => s.nodes)
102
+ const readOnly = useScene((s) => s.readOnly)
103
+ const unit = useViewer((s) => s.unit)
104
+ const metricNotation = useViewer((s) => s.metricNotation)
105
+ const offer = useMemo(() => windowMergeOffer(nodes, selectedIds), [nodes, selectedIds])
106
+ if (!offer) return null
107
+ if (offer.refusal?.code === 'windows_already_joined')
108
+ return (
109
+ <ActionButton
110
+ className="basis-full whitespace-nowrap disabled:cursor-not-allowed disabled:opacity-40"
111
+ disabled={readOnly}
112
+ icon={<Split className="h-4 w-4" />}
113
+ label="Unwrap the corner"
114
+ onClick={() => {
115
+ writeWindowChanges({
116
+ create: null,
117
+ updates: unwrapCorner(useScene.getState().nodes, selectedIds[0]!),
118
+ remove: [],
119
+ })
120
+ triggerSFX('sfx:structure-build')
121
+ }}
122
+ title="Unwrap the corner: both windows stay, each framed at the corner"
123
+ />
124
+ )
125
+ const { merge } = offer
126
+ const note = offer.refusal
127
+ ? mergeRefusalNote(offer.refusal, nodes, (meters) =>
128
+ formatLinearMeasurement(meters, unit, metricNotation),
129
+ )
130
+ : null
131
+ return (
132
+ <>
133
+ <ActionButton
134
+ className="basis-full whitespace-nowrap disabled:cursor-not-allowed disabled:opacity-40"
135
+ disabled={readOnly || !merge}
136
+ icon={<Merge className="h-4 w-4" />}
137
+ label="Merge windows"
138
+ onClick={() => {
139
+ if (!merge) return
140
+ runAsSingleSceneHistoryStep(useScene, () =>
141
+ writeWindowChanges({
142
+ create: null,
143
+ updates: Object.fromEntries(merge.changes.update.map(({ id, data }) => [id, data])),
144
+ remove: merge.changes.delete,
145
+ }),
146
+ )
147
+ triggerSFX('sfx:structure-build')
148
+ useViewer.getState().setSelection({ selectedIds: merge.windowIds as AnyNodeId[] })
149
+ }}
150
+ title={
151
+ note ??
152
+ (merge?.kind === 'corner'
153
+ ? 'Merge windows into a corner window: both take the height, sill and type of the one you selected first'
154
+ : 'Merge windows into one across both: it keeps the type and style of the one you selected first')
155
+ }
156
+ />
157
+ {note ? <p className="basis-full px-1 text-muted-foreground text-xs">{note}</p> : null}
158
+ </>
159
+ )
160
+ }
161
+
21
162
  export function MultiSelectionActions() {
22
163
  const selectedIds = useViewer((s) => s.selection.selectedIds)
23
164
  const sessionGroups = useSessionGroups((s) => s.groups)
@@ -32,10 +173,23 @@ export function MultiSelectionActions() {
32
173
  [sessionGroups, selectedIds, liveIds],
33
174
  )
34
175
 
176
+ const buildingId = useWholeLevelBuildingId(selectedIds)
177
+
35
178
  return (
36
- <ActionGroup>
179
+ <ActionGroup className="flex-wrap">
180
+ <MergeWindowsAction />
181
+ {buildingId && (
182
+ <ActionButton
183
+ className="basis-full whitespace-nowrap"
184
+ icon={<Building2 className="h-4 w-4" />}
185
+ label="Select building"
186
+ onClick={() => selectBuilding(buildingId)}
187
+ title="Select the whole building to move all its levels"
188
+ />
189
+ )}
37
190
  {showGroup && (
38
191
  <ActionButton
192
+ className="min-w-20 px-2"
39
193
  icon={<Group className="h-4 w-4" />}
40
194
  label="Group"
41
195
  onClick={() => groupCurrentSelection()}
@@ -44,6 +198,7 @@ export function MultiSelectionActions() {
44
198
  )}
45
199
  {showUngroup && (
46
200
  <ActionButton
201
+ className="min-w-20 px-2"
47
202
  icon={<Ungroup className="h-4 w-4" />}
48
203
  label="Ungroup"
49
204
  onClick={() => ungroupCurrentSelection()}
@@ -51,13 +206,14 @@ export function MultiSelectionActions() {
51
206
  />
52
207
  )}
53
208
  <ActionButton
209
+ className="min-w-20 px-2"
54
210
  icon={<Copy className="h-4 w-4" />}
55
211
  label="Duplicate"
56
212
  onClick={() => duplicateSelectionAndPickUp()}
57
213
  />
58
214
  <ActionButton
59
- className="border-red-500/40 text-red-200 hover:bg-red-500/15"
60
- icon={<Trash2 className="h-4 w-4 text-red-400" />}
215
+ className="min-w-20 px-2 border-red-500/40 text-red-700 dark:text-red-200 hover:bg-red-500/15"
216
+ icon={<Trash2 className="h-4 w-4 text-red-600 dark:text-red-400" />}
61
217
  label="Delete"
62
218
  onClick={() => deleteSelection()}
63
219
  />
@@ -1,4 +1,4 @@
1
- import type { AnyNode } from '@pascal-app/core'
1
+ import { type AnyNode, scriptImages } from '@pascal-app/core'
2
2
 
3
3
  export type NodeDisplay = {
4
4
  icon: string
@@ -21,6 +21,7 @@ const TYPE_DEFAULTS: Record<string, NodeDisplay> = {
21
21
  'stair-segment': { icon: '/icons/stairs.webp', label: 'Stair segment' },
22
22
  scan: { icon: '/icons/mesh.webp', label: '3D Scan' },
23
23
  guide: { icon: '/icons/floorplan.webp', label: 'Guide image' },
24
+ zone: { icon: '/icons/zone.webp', label: 'Zone' },
24
25
  }
25
26
 
26
27
  export function getTypeDisplay(type: string): NodeDisplay {
@@ -30,15 +31,19 @@ export function getTypeDisplay(type: string): NodeDisplay {
30
31
  export function getNodeDisplay(node: AnyNode | null | undefined): NodeDisplay {
31
32
  if (!node) return { icon: '/icons/select.webp', label: 'Selection' }
32
33
  const fallback = TYPE_DEFAULTS[node.type] ?? { icon: '/icons/select.webp', label: node.type }
34
+ const scripted = scriptImages(node)?.thumbnail
33
35
  // Item nodes carry an asset with its own thumbnail/name
34
36
  if (node.type === 'item') {
35
37
  return {
36
- icon: node.asset?.thumbnail || fallback.icon,
38
+ icon: scripted ?? (node.asset?.thumbnail || fallback.icon),
37
39
  label: node.name || node.asset?.name || fallback.label,
38
40
  }
39
41
  }
42
+ if (node.type === 'zone' && node.spaceRole === 'room') {
43
+ return { icon: '/icons/kitchen.webp', label: node.name || 'Room' }
44
+ }
40
45
  return {
41
- icon: fallback.icon,
46
+ icon: scripted ?? fallback.icon,
42
47
  label: node.name || fallback.label,
43
48
  }
44
49
  }