@pascal-app/editor 0.9.1 → 1.0.0-beta.1

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 (366) hide show
  1. package/package.json +14 -9
  2. package/src/components/editor/alignment-3d-guide-layer.tsx +10 -2
  3. package/src/components/editor/bake-exporter.tsx +49 -0
  4. package/src/components/editor/bake-thumbnail.tsx +95 -0
  5. package/src/components/editor/camera-dragging-lifecycle.test.ts +49 -0
  6. package/src/components/editor/camera-dragging-lifecycle.ts +41 -0
  7. package/src/components/editor/custom-camera-controls.tsx +332 -199
  8. package/src/components/editor/delete-confirmation-dialog.tsx +54 -0
  9. package/src/components/editor/editor-layout-mobile.tsx +5 -0
  10. package/src/components/editor/editor-layout-v2.tsx +18 -0
  11. package/src/components/editor/elevation-3d-guide-layer.tsx +120 -0
  12. package/src/components/editor/export-manager.tsx +71 -63
  13. package/src/components/editor/fence-tangent-lines-3d.tsx +87 -0
  14. package/src/components/editor/first-person/build-collider-world.test.ts +102 -5
  15. package/src/components/editor/first-person/build-collider-world.ts +52 -5
  16. package/src/components/editor/first-person/terrain-collider.test.ts +132 -0
  17. package/src/components/editor/first-person/terrain-collider.ts +212 -0
  18. package/src/components/editor/first-person-controls.tsx +410 -136
  19. package/src/components/editor/floating-action-menu.tsx +435 -64
  20. package/src/components/editor/floating-building-action-menu.tsx +1 -1
  21. package/src/components/editor/floorplan-background-selection.test.ts +72 -0
  22. package/src/components/editor/floorplan-background-selection.ts +7 -20
  23. package/src/components/editor/floorplan-camera-sync.test.ts +249 -0
  24. package/src/components/editor/floorplan-camera-sync.ts +213 -0
  25. package/src/components/editor/floorplan-mode-coordinator.tsx +82 -0
  26. package/src/components/editor/floorplan-navigation-presentation.test.ts +142 -0
  27. package/src/components/editor/floorplan-navigation-presentation.ts +156 -0
  28. package/src/components/editor/floorplan-panel.tsx +2517 -1492
  29. package/src/components/editor/grid.tsx +166 -39
  30. package/src/components/editor/group-actions.ts +597 -0
  31. package/src/components/editor/group-floating-action-menu.tsx +130 -0
  32. package/src/components/editor/group-move-3d.ts +397 -0
  33. package/src/components/editor/group-rotate-handle.tsx +102 -64
  34. package/src/components/editor/group-selection-box-3d.tsx +173 -0
  35. package/src/components/editor/group-transform-shared.test.ts +192 -1
  36. package/src/components/editor/group-transform-shared.ts +240 -7
  37. package/src/components/editor/handles/handle-arrow.tsx +81 -23
  38. package/src/components/editor/handles/handle-drag-history.test.ts +96 -0
  39. package/src/components/editor/handles/handle-drag-history.ts +14 -0
  40. package/src/components/editor/handles/preview-overrides.test.ts +26 -0
  41. package/src/components/editor/handles/preview-overrides.ts +13 -0
  42. package/src/components/editor/handles/resize-snap.test.ts +52 -0
  43. package/src/components/editor/handles/resize-snap.ts +23 -0
  44. package/src/components/editor/handles/use-handle-drag.ts +49 -6
  45. package/src/components/editor/index.tsx +289 -35
  46. package/src/components/editor/measurement-pill.tsx +68 -23
  47. package/src/components/editor/node-action-menu.tsx +14 -1
  48. package/src/components/editor/node-arrow-handles.tsx +409 -180
  49. package/src/components/editor/opening-guides-3d-layer.tsx +144 -0
  50. package/src/components/editor/quick-measurement-card.tsx +76 -0
  51. package/src/components/editor/quick-measurement-hud.tsx +34 -0
  52. package/src/components/editor/riser-diagram-panel.tsx +137 -0
  53. package/src/components/editor/selection-manager.tsx +814 -466
  54. package/src/components/editor/site-edge-labels.tsx +28 -6
  55. package/src/components/editor/slab-hole-highlights.tsx +13 -5
  56. package/src/components/editor/snapshot-capture-overlay.tsx +258 -115
  57. package/src/components/editor/three-context-bridge.ts +22 -0
  58. package/src/components/editor/thumbnail-generator.tsx +118 -253
  59. package/src/components/editor/use-floorplan-background-placement.ts +113 -45
  60. package/src/components/editor/use-floorplan-hit-testing.test.ts +82 -0
  61. package/src/components/editor/use-floorplan-hit-testing.ts +144 -161
  62. package/src/components/editor/use-floorplan-scene-data.ts +5 -5
  63. package/src/components/editor/use-mesh-settle-epoch.ts +27 -0
  64. package/src/components/editor/wall-measurement-label.tsx +10 -28
  65. package/src/components/editor/wall-move-side-handles.tsx +229 -39
  66. package/src/components/editor/wall-opening-highlights.tsx +63 -32
  67. package/src/components/editor/wall-snap-beacon-layer.tsx +162 -6
  68. package/src/components/editor-2d/floorplan-action-menu-layer.tsx +9 -5
  69. package/src/components/editor-2d/floorplan-alignment-guide-layer.tsx +2 -1
  70. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +40 -9
  71. package/src/components/editor-2d/floorplan-cursor-indicator-position.test.ts +31 -0
  72. package/src/components/editor-2d/floorplan-cursor-indicator-position.ts +39 -0
  73. package/src/components/editor-2d/floorplan-group-action-menu.tsx +87 -0
  74. package/src/components/editor-2d/floorplan-group-move.tsx +744 -0
  75. package/src/components/editor-2d/floorplan-measurement-tool-layer.test.ts +113 -0
  76. package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +1672 -0
  77. package/src/components/editor-2d/floorplan-quick-measure-layer.tsx +212 -0
  78. package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +72 -0
  79. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +219 -20
  80. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +327 -59
  81. package/src/components/editor-2d/floorplan-render-context.test.ts +27 -0
  82. package/src/components/editor-2d/floorplan-render-context.tsx +113 -8
  83. package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +5 -3
  84. package/src/components/editor-2d/renderers/floorplan-annotation-layout.test.ts +285 -0
  85. package/src/components/editor-2d/renderers/floorplan-annotation-layout.ts +516 -0
  86. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.test.tsx +236 -0
  87. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +618 -0
  88. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.test.tsx +369 -0
  89. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +431 -32
  90. package/src/components/editor-2d/renderers/floorplan-label-angle.test.ts +89 -0
  91. package/src/components/editor-2d/renderers/floorplan-label-angle.ts +106 -0
  92. package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +94 -26
  93. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +587 -0
  94. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +2280 -578
  95. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +20 -10
  96. package/src/components/editor-2d/renderers/floorplan-voronoi-layer.tsx +128 -0
  97. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +425 -61
  98. package/src/components/systems/ceiling/ceiling-system.tsx +97 -4
  99. package/src/components/systems/roof/roof-edit-system.tsx +1768 -13
  100. package/src/components/systems/selection-affordance-manager.tsx +38 -0
  101. package/src/components/systems/zone/zone-label-editor-system.tsx +14 -1
  102. package/src/components/systems/zone/zone-system.tsx +42 -13
  103. package/src/components/tools/elevator/elevator-tool.tsx +34 -8
  104. package/src/components/tools/elevator/move-elevator-tool.tsx +13 -2
  105. package/src/components/tools/fence/fence-drafting.ts +107 -8
  106. package/src/components/tools/item/move-tool.tsx +17 -13
  107. package/src/components/tools/item/placement-math.test.ts +13 -1
  108. package/src/components/tools/item/placement-math.ts +28 -2
  109. package/src/components/tools/item/placement-strategies.ts +246 -5
  110. package/src/components/tools/item/placement-types.ts +13 -1
  111. package/src/components/tools/item/use-draft-node.ts +146 -57
  112. package/src/components/tools/item/use-placement-coordinator.tsx +813 -125
  113. package/src/components/tools/registry/move-registry-node-tool.tsx +636 -103
  114. package/src/components/tools/roof/roof-tool.tsx +336 -94
  115. package/src/components/tools/select/box-select-state.ts +18 -7
  116. package/src/components/tools/select/box-select-tool.tsx +176 -40
  117. package/src/components/tools/select/marquee-geometry.test.ts +161 -0
  118. package/src/components/tools/select/marquee-geometry.ts +155 -0
  119. package/src/components/tools/select/plane-box-select-tool.tsx +1 -8
  120. package/src/components/tools/select/select-candidates.ts +1 -1
  121. package/src/components/tools/shared/cursor-sphere.tsx +62 -26
  122. package/src/components/tools/shared/drag-bounding-box.tsx +28 -4
  123. package/src/components/tools/shared/facing-indicator.tsx +87 -0
  124. package/src/components/tools/shared/facing-pose-indicator.tsx +54 -0
  125. package/src/components/tools/shared/floor-stack-preview.ts +4 -0
  126. package/src/components/tools/shared/horizontal-construction-plane.ts +123 -0
  127. package/src/components/tools/shared/placement-box.tsx +183 -1
  128. package/src/components/tools/shared/pointer-support-cap.test.ts +98 -0
  129. package/src/components/tools/shared/pointer-support-cap.ts +273 -0
  130. package/src/components/tools/shared/polygon-editor.tsx +276 -35
  131. package/src/components/tools/site/site-boundary-editor.tsx +101 -42
  132. package/src/components/tools/site/stroke-pointer-ownership.test.ts +91 -0
  133. package/src/components/tools/site/stroke-pointer-ownership.ts +57 -0
  134. package/src/components/tools/site/terrain-brush-cursor.tsx +247 -0
  135. package/src/components/tools/site/terrain-sculpt-grid.tsx +192 -0
  136. package/src/components/tools/site/terrain-sculpt-tool.tsx +356 -0
  137. package/src/components/tools/stair/stair-click-guard.test.ts +58 -0
  138. package/src/components/tools/stair/stair-click-guard.ts +72 -0
  139. package/src/components/tools/stair/stair-defaults.ts +1 -0
  140. package/src/components/tools/stair/stair-tool.tsx +164 -42
  141. package/src/components/tools/tool-manager.tsx +149 -30
  142. package/src/components/tools/wall/wall-drafting.test.ts +447 -0
  143. package/src/components/tools/wall/wall-drafting.ts +304 -108
  144. package/src/components/tools/wall/wall-snap-geometry.test.ts +52 -0
  145. package/src/components/tools/wall/wall-snap-geometry.ts +137 -9
  146. package/src/components/tools/zone/zone-boundary-editor.tsx +5 -1
  147. package/src/components/tools/zone/zone-tool.tsx +98 -88
  148. package/src/components/ui/action-menu/camera-actions.tsx +24 -17
  149. package/src/components/ui/action-menu/control-modes.tsx +44 -94
  150. package/src/components/ui/action-menu/furnish-tools.tsx +5 -5
  151. package/src/components/ui/action-menu/index.tsx +2 -4
  152. package/src/components/ui/action-menu/measurement-control.tsx +267 -0
  153. package/src/components/ui/action-menu/structure-tools.tsx +22 -13
  154. package/src/components/ui/action-menu/view-toggles.tsx +42 -90
  155. package/src/components/ui/command-palette/editor-commands.tsx +16 -3
  156. package/src/components/ui/command-palette/index.tsx +4 -3
  157. package/src/components/ui/controls/material-paint-panel.tsx +93 -18
  158. package/src/components/ui/controls/material-picker.tsx +162 -152
  159. package/src/components/ui/controls/material-properties-editor.tsx +108 -0
  160. package/src/components/ui/controls/metric-control.tsx +118 -44
  161. package/src/components/ui/controls/scene-material-list.tsx +247 -0
  162. package/src/components/ui/controls/slider-control.tsx +88 -38
  163. package/src/components/ui/controls/terrain-sculpt-panel.tsx +193 -0
  164. package/src/components/ui/floating-level-selector.tsx +74 -20
  165. package/src/components/ui/helpers/building-helper.tsx +10 -23
  166. package/src/components/ui/helpers/contextual-helper-panel.tsx +428 -0
  167. package/src/components/ui/helpers/helper-manager.tsx +273 -14
  168. package/src/components/ui/helpers/item-helper.tsx +28 -32
  169. package/src/components/ui/helpers/registered-tool-helper.tsx +45 -11
  170. package/src/components/ui/helpers/roof-helper.tsx +10 -12
  171. package/src/components/ui/icon-ref.tsx +47 -0
  172. package/src/components/ui/item-catalog/catalog-items.tsx +39 -1
  173. package/src/components/ui/item-catalog/item-catalog.tsx +13 -36
  174. package/src/components/ui/level-duplicate-dialog.tsx +1 -1
  175. package/src/components/ui/panels/multi-selection-panel.tsx +57 -0
  176. package/src/components/ui/panels/node-display.ts +17 -17
  177. package/src/components/ui/panels/panel-manager.tsx +36 -14
  178. package/src/components/ui/panels/panel-wrapper.tsx +24 -3
  179. package/src/components/ui/panels/parametric-inspector.tsx +19 -4
  180. package/src/components/ui/panels/reference-panel.tsx +54 -19
  181. package/src/components/ui/panels/selection-breakdown.test.ts +24 -0
  182. package/src/components/ui/panels/selection-breakdown.ts +20 -0
  183. package/src/components/ui/primitives/shortcut-token.tsx +39 -3
  184. package/src/components/ui/primitives/sidebar.tsx +1 -0
  185. package/src/components/ui/sidebar/app-sidebar.tsx +5 -1
  186. package/src/components/ui/sidebar/icon-rail.tsx +50 -31
  187. package/src/components/ui/sidebar/panels/plugins-panel.tsx +218 -0
  188. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +82 -41
  189. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +76 -6
  190. package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +12 -4
  191. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +2 -1
  192. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -3
  193. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +10 -7
  194. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -1
  195. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +7 -2
  196. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +10 -7
  197. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -1
  198. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +1 -1
  199. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +10 -7
  200. package/src/components/ui/sidebar/panels/site-panel/index.tsx +42 -17
  201. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +31 -17
  202. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +1 -1
  203. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +72 -25
  204. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +7 -3
  205. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +12 -8
  206. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +4 -3
  207. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +10 -7
  208. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -1
  209. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +2 -2
  210. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +44 -3
  211. package/src/components/ui/sidebar/panels/site-panel/tree-structure.ts +36 -0
  212. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -1
  213. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +7 -2
  214. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +3 -3
  215. package/src/components/ui/sidebar/tab-bar.tsx +42 -28
  216. package/src/components/ui/sidebar/use-plugin-panels.tsx +132 -0
  217. package/src/components/ui/snap-target-badge.test.tsx +40 -0
  218. package/src/components/ui/snap-target-badge.tsx +88 -0
  219. package/src/components/viewer/viewer-controls-bar.tsx +466 -0
  220. package/src/components/viewer/viewer-scene-header.tsx +235 -0
  221. package/src/components/viewer-overlay.tsx +36 -596
  222. package/src/components/viewer-zone-system.tsx +6 -1
  223. package/src/components/walkthrough-hud.tsx +111 -0
  224. package/src/hooks/use-auto-save.test.ts +14 -0
  225. package/src/hooks/use-auto-save.ts +54 -7
  226. package/src/hooks/use-ceiling-events.ts +3 -2
  227. package/src/hooks/use-drag-action.ts +4 -1
  228. package/src/hooks/use-grid-events.ts +24 -0
  229. package/src/hooks/use-keyboard.ts +438 -70
  230. package/src/index.tsx +355 -9
  231. package/src/lib/active-placement-surface.test.ts +24 -0
  232. package/src/lib/active-placement-surface.ts +42 -0
  233. package/src/lib/camera-pose.test.ts +237 -0
  234. package/src/lib/camera-pose.ts +172 -0
  235. package/src/lib/ceiling-plan-snap.ts +24 -0
  236. package/src/lib/contextual-help.test.ts +112 -0
  237. package/src/lib/contextual-help.ts +144 -0
  238. package/src/lib/continuation.ts +64 -0
  239. package/src/lib/direct-manipulation.test.ts +275 -0
  240. package/src/lib/direct-manipulation.ts +118 -0
  241. package/src/lib/door-interaction.ts +1 -1
  242. package/src/lib/editor-api.ts +23 -35
  243. package/src/lib/elevation-guides.test.ts +107 -0
  244. package/src/lib/elevation-guides.ts +236 -0
  245. package/src/lib/floorplan/annotation-visibility.test.ts +327 -0
  246. package/src/lib/floorplan/annotation-visibility.ts +133 -0
  247. package/src/lib/floorplan/apply-alignment.test.ts +25 -0
  248. package/src/lib/floorplan/apply-alignment.ts +18 -12
  249. package/src/lib/floorplan/drawing-coordination.test.ts +46 -0
  250. package/src/lib/floorplan/drawing-coordination.ts +13 -0
  251. package/src/lib/floorplan/floorplan-export.test.ts +319 -0
  252. package/src/lib/floorplan/floorplan-export.tsx +978 -0
  253. package/src/lib/floorplan/floorplan-extension.test.ts +43 -0
  254. package/src/lib/floorplan/floorplan-extension.ts +181 -0
  255. package/src/lib/floorplan/floorplan-mode.test.ts +120 -0
  256. package/src/lib/floorplan/floorplan-mode.ts +73 -0
  257. package/src/lib/floorplan/floorplan-pdfkit-document.ts +194 -0
  258. package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +107 -0
  259. package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +680 -0
  260. package/src/lib/floorplan/geometry.ts +52 -0
  261. package/src/lib/floorplan/index.ts +3 -0
  262. package/src/lib/floorplan/items.ts +5 -2
  263. package/src/lib/floorplan/plan-coords.ts +21 -0
  264. package/src/lib/fresh-planar-placement.test.ts +283 -3
  265. package/src/lib/fresh-planar-placement.ts +85 -1
  266. package/src/lib/glb-export.test.ts +556 -0
  267. package/src/lib/glb-export.ts +1071 -0
  268. package/src/lib/ground-surface.ts +95 -0
  269. package/src/lib/history.test.ts +96 -0
  270. package/src/lib/history.ts +31 -0
  271. package/src/lib/interaction/hot-set.test.ts +133 -0
  272. package/src/lib/interaction/hot-set.ts +67 -0
  273. package/src/lib/interaction/overlay-policy.test.ts +52 -0
  274. package/src/lib/interaction/overlay-policy.ts +59 -0
  275. package/src/lib/interaction/scope.ts +243 -0
  276. package/src/lib/level-duplication.test.ts +2 -1
  277. package/src/lib/level-selection.ts +2 -2
  278. package/src/lib/material-paint.ts +36 -49
  279. package/src/lib/measurement-kind.test.ts +30 -0
  280. package/src/lib/measurement-kind.ts +19 -0
  281. package/src/lib/measurement-label.test.ts +47 -0
  282. package/src/lib/measurement-label.ts +68 -0
  283. package/src/lib/measurement-parser.ts +116 -0
  284. package/src/lib/measurements.test.ts +188 -0
  285. package/src/lib/measurements.ts +210 -0
  286. package/src/lib/paint-scope.test.ts +454 -0
  287. package/src/lib/paint-scope.ts +461 -0
  288. package/src/lib/placement-drag-release.ts +23 -0
  289. package/src/lib/planar-cursor-placement.test.ts +57 -0
  290. package/src/lib/plugin-panels.test.ts +19 -0
  291. package/src/lib/plugin-panels.ts +91 -0
  292. package/src/lib/quick-action-feedback.ts +33 -0
  293. package/src/lib/quick-action-nodes.test.ts +94 -0
  294. package/src/lib/quick-action-nodes.ts +47 -0
  295. package/src/lib/quick-measurement.test.ts +77 -0
  296. package/src/lib/quick-measurement.ts +109 -0
  297. package/src/lib/roof-duplication.ts +6 -6
  298. package/src/lib/roof-hover-outline-proxy.ts +22 -0
  299. package/src/lib/roof-wall-hit.ts +164 -0
  300. package/src/lib/scene-clipboard.test.ts +325 -0
  301. package/src/lib/scene-clipboard.ts +310 -21
  302. package/src/lib/scene.ts +28 -5
  303. package/src/lib/selection-routing.test.ts +313 -0
  304. package/src/lib/selection-routing.ts +180 -0
  305. package/src/lib/sfx/index.ts +1 -0
  306. package/src/lib/sfx/movement-tick.test.ts +65 -0
  307. package/src/lib/sfx/movement-tick.ts +18 -0
  308. package/src/lib/sfx-bus.ts +82 -14
  309. package/src/lib/sfx-player.test.ts +266 -0
  310. package/src/lib/sfx-player.ts +262 -69
  311. package/src/lib/site-boundary.test.ts +53 -0
  312. package/src/lib/site-boundary.ts +27 -0
  313. package/src/lib/slab-plan-snap.test.ts +93 -0
  314. package/src/lib/slab-plan-snap.ts +108 -0
  315. package/src/lib/snapping-mode.test.ts +165 -0
  316. package/src/lib/snapping-mode.ts +191 -0
  317. package/src/lib/stair-duplication.test.ts +103 -0
  318. package/src/lib/stair-duplication.ts +58 -86
  319. package/src/lib/stair-levels.ts +2 -0
  320. package/src/lib/surface-plan-snap.test.ts +71 -0
  321. package/src/lib/surface-plan-snap.ts +256 -0
  322. package/src/lib/terrain-sculpt.test.ts +429 -0
  323. package/src/lib/terrain-sculpt.ts +301 -0
  324. package/src/lib/terrain-verb-color.test.ts +51 -0
  325. package/src/lib/terrain-verb-color.ts +58 -0
  326. package/src/lib/touch-gesture-priority.test.ts +45 -0
  327. package/src/lib/touch-gesture-priority.ts +30 -0
  328. package/src/lib/use-linear-display.ts +40 -0
  329. package/src/lib/window-interaction.ts +1 -1
  330. package/src/lib/world-grid-snap.ts +37 -5
  331. package/src/store/camera-pose-store.test.ts +29 -0
  332. package/src/store/camera-pose-store.ts +27 -0
  333. package/src/store/live-draft-preview-stores.test.ts +136 -0
  334. package/src/store/navigation-sync-pose-store.test.ts +34 -0
  335. package/src/store/navigation-sync-pose-store.ts +27 -0
  336. package/src/store/terrain-sculpt-mode.test.ts +223 -0
  337. package/src/store/use-delete-confirmation.ts +27 -0
  338. package/src/store/use-direct-manipulation-feedback.ts +20 -0
  339. package/src/store/use-drawing-view.test.ts +23 -0
  340. package/src/store/use-drawing-view.ts +84 -0
  341. package/src/store/use-editor.tsx +665 -126
  342. package/src/store/use-elevation-guides.ts +40 -0
  343. package/src/store/use-facing-pose.ts +44 -0
  344. package/src/store/use-fence-curve-draft.ts +51 -0
  345. package/src/store/use-first-person-hud.ts +40 -0
  346. package/src/store/use-floorplan-annotation-visibility.ts +60 -0
  347. package/src/store/use-floorplan-draft-preview.ts +151 -0
  348. package/src/store/use-floorplan-marquee.ts +50 -0
  349. package/src/store/use-floorplan-mode.ts +110 -0
  350. package/src/store/use-interaction-scope.test.ts +206 -0
  351. package/src/store/use-interaction-scope.ts +140 -0
  352. package/src/store/use-measurement-draft.test.ts +559 -0
  353. package/src/store/use-measurement-draft.ts +570 -0
  354. package/src/store/use-opening-guides.ts +41 -0
  355. package/src/store/use-path-draft-preview.ts +90 -0
  356. package/src/store/use-placement-preview.ts +11 -3
  357. package/src/store/use-quick-measurement-hud.test.ts +53 -0
  358. package/src/store/use-quick-measurement-hud.ts +77 -0
  359. package/src/store/use-segment-draft-chain.ts +28 -0
  360. package/src/store/use-stair-build-preview.ts +53 -0
  361. package/src/store/use-wall-snap-indicator.ts +2 -0
  362. package/src/components/editor/first-person/bvh-ecctrl.tsx +0 -860
  363. package/src/components/editor/group-move-handle.tsx +0 -316
  364. package/src/components/ui/panels/paint-panel.tsx +0 -163
  365. package/src/lib/floorplan/selection-tool.ts +0 -271
  366. package/src/lib/level-name.ts +0 -11
@@ -1,15 +1,15 @@
1
1
  'use client'
2
2
 
3
3
  import { Icon } from '@iconify/react'
4
- import { type LevelNode, useScene } from '@pascal-app/core'
5
- import { useViewer } from '@pascal-app/viewer'
6
4
  import { type LucideIcon, Trash2 } from 'lucide-react'
7
5
  import Image from 'next/image'
6
+ import { Fragment } from 'react'
8
7
  import { cn } from './../../../lib/utils'
9
- import useEditor, { selectSiteFloorplanContext } from './../../../store/use-editor'
8
+ import useEditor from './../../../store/use-editor'
10
9
  import { ActionButton } from './action-button'
10
+ import { MeasurementControl } from './measurement-control'
11
11
 
12
- type ControlId = 'select' | 'box-select' | 'site-edit' | 'zone' | 'delete'
12
+ type ControlId = 'select' | 'box-select' | 'zone' | 'delete'
13
13
 
14
14
  type ControlConfig = {
15
15
  id: ControlId
@@ -26,22 +26,15 @@ type ControlConfig = {
26
26
  const controls: ControlConfig[] = [
27
27
  {
28
28
  id: 'select',
29
- imageSrc: '/icons/select.png',
29
+ imageSrc: '/icons/select.webp',
30
30
  label: 'Select',
31
31
  shortcut: 'V',
32
32
  color: 'hover:bg-blue-500/20 hover:text-blue-400',
33
33
  activeColor: 'bg-blue-500/20 text-blue-400',
34
34
  },
35
- {
36
- id: 'site-edit',
37
- imageSrc: '/icons/site-flag.png',
38
- label: 'Edit site',
39
- color: 'hover:bg-white/5',
40
- activeColor: 'bg-white/10 hover:bg-white/10',
41
- },
42
35
  {
43
36
  id: 'zone',
44
- imageSrc: '/icons/zone.png',
37
+ imageSrc: '/icons/zone.webp',
45
38
  label: 'Zone',
46
39
  shortcut: 'Z',
47
40
  color: 'hover:bg-green-500/20 hover:text-green-400',
@@ -65,48 +58,24 @@ export function ControlModes() {
65
58
  const setPhase = useEditor((state) => state.setPhase)
66
59
  const setStructureLayer = useEditor((state) => state.setStructureLayer)
67
60
  const setSelectionTool = useEditor((state) => state.setFloorplanSelectionTool)
68
- const levelId = useViewer((s) => s.selection.levelId)
69
-
70
- // Only subscribe to the primitive `level` number — when walls are added to
71
- // this level the object ref changes but this number doesn't, so Object.is
72
- // dedupes and we avoid a re-render.
73
- const levelIndex = useScene((state) => {
74
- if (!levelId) return null
75
- const node = state.nodes[levelId]
76
- return node?.type === 'level' ? (node as LevelNode).level : null
77
- })
78
61
 
79
62
  const isSiteEditing = phase === 'site'
80
- const isGroundFloor = levelIndex === 0
81
- const canEnterSiteEdit = isGroundFloor || isSiteEditing
82
63
 
83
64
  const structureLayer = useEditor((state) => state.structureLayer)
84
65
 
85
66
  const getIsActive = (id: ControlId): boolean => {
86
- if (isSiteEditing) return id === 'site-edit'
87
67
  if (id === 'select') return mode === 'select' && selectionTool === 'click'
88
68
  if (id === 'box-select') return mode === 'select' && selectionTool === 'marquee'
89
- if (id === 'site-edit') return false
90
69
  if (id === 'zone')
91
70
  return mode === 'build' && phase === 'structure' && structureLayer === 'zones'
92
71
  return mode === id
93
72
  }
94
73
 
95
74
  const handleClick = (id: ControlId) => {
96
- if (id === 'site-edit') {
97
- if (isSiteEditing) {
98
- // Toggle off back to structure/select
99
- setPhase('structure')
100
- setMode('select')
101
- setStructureLayer('elements')
102
- } else if (isGroundFloor) {
103
- useEditor.setState({ phase: 'site', mode: 'select', tool: null, catalogCategory: null })
104
- selectSiteFloorplanContext()
105
- }
106
- return
107
- }
108
-
109
- // Exit site editing first if needed
75
+ // Exit site editing first if needed. Sculpting is a site-phase mode, so
76
+ // leaving the phase is exactly the right way to leave the brush — but the
77
+ // order matters: `setPhase` resets the mode, and setting the mode first
78
+ // would have it overwritten below.
110
79
  if (isSiteEditing) {
111
80
  setPhase('structure')
112
81
  setStructureLayer('elements')
@@ -136,64 +105,45 @@ export function ControlModes() {
136
105
  {controls.map((c) => {
137
106
  const ModeIcon = c.icon
138
107
  const isImageMode = Boolean(c.imageSrc)
139
- const isSiteButton = c.id === 'site-edit'
140
108
  const isActive = getIsActive(c.id)
141
- const isDisabled = isSiteButton && !canEnterSiteEdit
142
109
 
143
110
  return (
144
- <ActionButton
145
- className={cn(
146
- 'group text-muted-foreground',
147
- isSiteButton
148
- ? isActive
149
- ? c.activeColor
150
- : canEnterSiteEdit
151
- ? 'opacity-60 grayscale hover:bg-white/5 hover:opacity-100 hover:grayscale-0'
152
- : 'cursor-not-allowed opacity-35 grayscale'
153
- : !(isImageMode || isActive) && c.color,
154
- !(isSiteButton || isImageMode) && isActive && c.activeColor,
155
- !isSiteButton && isImageMode && isActive && 'bg-white/10 hover:bg-white/10',
156
- !isSiteButton && isImageMode && !isActive && 'hover:bg-white/5',
157
- )}
158
- disabled={isDisabled}
159
- key={c.id}
160
- label={
161
- isSiteButton
162
- ? isActive
163
- ? 'Exit site editing'
164
- : canEnterSiteEdit
165
- ? 'Edit site'
166
- : 'Site editing (ground level only)'
167
- : c.label
168
- }
169
- onClick={() => handleClick(c.id)}
170
- shortcut={c.shortcut}
171
- size="icon"
172
- variant="ghost"
173
- >
174
- {c.imageSrc ? (
175
- <Image
176
- alt={c.label}
177
- className={cn(
178
- 'h-[28px] w-[28px] object-contain transition-[opacity,filter] duration-200',
179
- isSiteButton
180
- ? isActive
181
- ? 'opacity-100 grayscale-0'
182
- : ''
183
- : isActive
111
+ <Fragment key={c.id}>
112
+ {c.id === 'delete' ? <MeasurementControl /> : null}
113
+ <ActionButton
114
+ className={cn(
115
+ 'group text-muted-foreground',
116
+ !(isImageMode || isActive) && c.color,
117
+ !isImageMode && isActive && c.activeColor,
118
+ isImageMode && isActive && 'bg-white/10 hover:bg-white/10',
119
+ isImageMode && !isActive && 'hover:bg-white/5',
120
+ )}
121
+ label={c.label}
122
+ onClick={() => handleClick(c.id)}
123
+ shortcut={c.shortcut}
124
+ size="icon"
125
+ variant="ghost"
126
+ >
127
+ {c.imageSrc ? (
128
+ <Image
129
+ alt={c.label}
130
+ className={cn(
131
+ 'h-[28px] w-[28px] object-contain transition-[opacity,filter] duration-200',
132
+ isActive
184
133
  ? 'opacity-100 grayscale-0'
185
134
  : 'opacity-60 grayscale group-hover:opacity-100 group-hover:grayscale-0',
186
- )}
187
- height={28}
188
- src={c.imageSrc}
189
- width={28}
190
- />
191
- ) : c.iconifyIcon ? (
192
- <Icon color="currentColor" height={18} icon={c.iconifyIcon} width={18} />
193
- ) : (
194
- ModeIcon && <ModeIcon className="h-5 w-5" />
195
- )}
196
- </ActionButton>
135
+ )}
136
+ height={28}
137
+ src={c.imageSrc}
138
+ width={28}
139
+ />
140
+ ) : c.iconifyIcon ? (
141
+ <Icon color="currentColor" height={18} icon={c.iconifyIcon} width={18} />
142
+ ) : (
143
+ ModeIcon && <ModeIcon className="h-5 w-5" />
144
+ )}
145
+ </ActionButton>
146
+ </Fragment>
197
147
  )
198
148
  })}
199
149
  </div>
@@ -8,9 +8,9 @@ export type FurnishToolConfig = {
8
8
  }
9
9
 
10
10
  export const furnishTools: FurnishToolConfig[] = [
11
- { id: 'item', iconSrc: '/icons/couch.png', label: 'Furniture', catalogCategory: 'furniture' },
12
- { id: 'item', iconSrc: '/icons/appliance.png', label: 'Appliance', catalogCategory: 'appliance' },
13
- { id: 'item', iconSrc: '/icons/kitchen.png', label: 'Kitchen', catalogCategory: 'kitchen' },
14
- { id: 'item', iconSrc: '/icons/bathroom.png', label: 'Bathroom', catalogCategory: 'bathroom' },
15
- { id: 'item', iconSrc: '/icons/tree.png', label: 'Outdoor', catalogCategory: 'outdoor' },
11
+ { id: 'item', iconSrc: '/icons/couch.webp', label: 'Furniture', catalogCategory: 'furniture' },
12
+ { id: 'item', iconSrc: '/icons/appliance.webp', label: 'Appliance', catalogCategory: 'appliance' },
13
+ { id: 'item', iconSrc: '/icons/kitchen.webp', label: 'Kitchen', catalogCategory: 'kitchen' },
14
+ { id: 'item', iconSrc: '/icons/bathroom.webp', label: 'Bathroom', catalogCategory: 'bathroom' },
15
+ { id: 'item', iconSrc: '/icons/tree.webp', label: 'Outdoor', catalogCategory: 'outdoor' },
16
16
  ]
@@ -9,7 +9,7 @@ import { cn } from './../../../lib/utils'
9
9
  import useEditor from './../../../store/use-editor'
10
10
  import { CameraActions } from './camera-actions'
11
11
  import { ControlModes } from './control-modes'
12
- import { GridSnapControl, SecondaryToggles } from './view-toggles'
12
+ import { SecondaryToggles } from './view-toggles'
13
13
 
14
14
  // Mobile bottom offset matches the viewer's overlap behind the sheet's
15
15
  // rounded corners (SHEET_OVERLAP_PX in editor-layout-mobile) so the menu sits
@@ -57,9 +57,8 @@ export function ActionMenu({ className }: { className?: string }) {
57
57
  <div className="flex items-center justify-center gap-1">
58
58
  <ControlModes />
59
59
  </div>
60
- {/* Row 2: grid snap + secondary toggles (orbit + top view hidden) */}
60
+ {/* Row 2: secondary toggles (orbit + top view hidden) */}
61
61
  <div className="flex items-center justify-center gap-1 border-border/50 border-t pt-1">
62
- <GridSnapControl />
63
62
  <SecondaryToggles />
64
63
  </div>
65
64
  </div>
@@ -67,7 +66,6 @@ export function ActionMenu({ className }: { className?: string }) {
67
66
  <div className="flex items-center justify-center gap-1 px-2 py-1.5">
68
67
  <ControlModes />
69
68
  <div className="mx-1 h-5 w-px bg-border" />
70
- <GridSnapControl />
71
69
  <SecondaryToggles />
72
70
  <div className="mx-1 h-5 w-px bg-border" />
73
71
  <CameraActions />
@@ -0,0 +1,267 @@
1
+ 'use client'
2
+
3
+ import type {
4
+ ConstructionDimensionChainMode,
5
+ ConstructionDimensionMode,
6
+ } from '@pascal-app/core'
7
+ import {
8
+ Box,
9
+ Check,
10
+ ChevronDown,
11
+ CircleIcon,
12
+ Crosshair,
13
+ Grid2X2,
14
+ Minus,
15
+ Ruler,
16
+ ScanSearch,
17
+ Square,
18
+ Triangle,
19
+ Waypoints,
20
+ } from 'lucide-react'
21
+ import { useState } from 'react'
22
+ import type { CreatableMeasurementKind } from '../../../lib/measurement-kind'
23
+ import { cn } from '../../../lib/utils'
24
+ import useEditor from '../../../store/use-editor'
25
+ import useFloorplanMode from '../../../store/use-floorplan-mode'
26
+ import { Popover, PopoverContent, PopoverTrigger } from '../primitives/popover'
27
+ import { ActionButton } from './action-button'
28
+
29
+ const measurementOptions = [
30
+ { kind: 'distance', label: 'Distance', icon: Ruler },
31
+ { kind: 'angle', label: 'Angle', icon: Triangle },
32
+ { kind: 'area', label: 'Area', icon: Square },
33
+ { kind: 'perimeter', label: 'Perimeter', icon: Waypoints },
34
+ { kind: 'volume', label: 'Volume', icon: Box },
35
+ ] as const satisfies readonly {
36
+ kind: CreatableMeasurementKind
37
+ label: string
38
+ icon: typeof Ruler
39
+ }[]
40
+
41
+ const measurementMenuOptions = [
42
+ { kind: 'smart', label: 'Smart', icon: ScanSearch },
43
+ ...measurementOptions,
44
+ ] as const
45
+
46
+ const constructionDimensionOptions = [
47
+ { mode: 'linear', chainMode: 'point-to-point', label: 'Linear dimension', icon: Ruler },
48
+ { mode: 'linear', chainMode: 'continuous', label: 'Continuous dimension', icon: Waypoints },
49
+ { mode: 'radius', chainMode: 'point-to-point', label: 'Radius dimension', icon: CircleIcon },
50
+ { mode: 'diameter', chainMode: 'point-to-point', label: 'Diameter dimension', icon: CircleIcon },
51
+ { mode: 'center-mark', chainMode: 'point-to-point', label: 'Center mark', icon: Crosshair },
52
+ { mode: 'chord', chainMode: 'point-to-point', label: 'Chord dimension', icon: Minus },
53
+ { mode: 'arc-length', chainMode: 'point-to-point', label: 'Arc length', icon: CircleIcon },
54
+ { mode: 'angular', chainMode: 'point-to-point', label: 'Angular dimension', icon: Triangle },
55
+ { mode: 'coordinate', chainMode: 'continuous', label: 'Coordinate dimensions', icon: Grid2X2 },
56
+ ] as const satisfies readonly {
57
+ mode: ConstructionDimensionMode
58
+ chainMode: ConstructionDimensionChainMode
59
+ label: string
60
+ icon: typeof Ruler
61
+ }[]
62
+
63
+ export function MeasurementControl() {
64
+ const [isOpen, setIsOpen] = useState(false)
65
+ const mode = useEditor((state) => state.mode)
66
+ const tool = useEditor((state) => state.tool)
67
+ const floorplanMode = useFloorplanMode((state) => state.mode)
68
+ const selectedKind = useEditor((state) => state.lastMeasurementKind)
69
+ const activeToolKind = useEditor((state) => state.toolDefaults.measurement?.kind)
70
+ const constructionDimensionChainMode = useEditor(
71
+ (state) => state.toolDefaults['construction-dimension']?.chainMode,
72
+ )
73
+ const constructionDimensionMode = useEditor(
74
+ (state) => state.toolDefaults['construction-dimension']?.mode,
75
+ )
76
+ const setMode = useEditor((state) => state.setMode)
77
+ const setPhase = useEditor((state) => state.setPhase)
78
+ const setLastMeasurementKind = useEditor((state) => state.setLastMeasurementKind)
79
+ const setStructureLayer = useEditor((state) => state.setStructureLayer)
80
+ const setTool = useEditor((state) => state.setTool)
81
+ const setToolDefaults = useEditor((state) => state.setToolDefaults)
82
+ const setViewMode = useEditor((state) => state.setViewMode)
83
+
84
+ const selectedOption =
85
+ measurementOptions.find((option) => option.kind === selectedKind) ?? measurementOptions[0]
86
+ const isActive = mode === 'build' && tool === 'measurement'
87
+ const isConstructionDimensionActive = mode === 'build' && tool === 'construction-dimension'
88
+ const activeConstructionDimensionOption = constructionDimensionOptions.find(
89
+ (option) =>
90
+ option.mode === (constructionDimensionMode ?? 'linear') &&
91
+ option.chainMode === (constructionDimensionChainMode ?? 'point-to-point'),
92
+ )
93
+ const isControlActive = isActive || isConstructionDimensionActive
94
+ const isSmartActive = isActive && activeToolKind === 'smart'
95
+ const SelectedIcon = isConstructionDimensionActive
96
+ ? (activeConstructionDimensionOption?.icon ?? Ruler)
97
+ : isSmartActive
98
+ ? ScanSearch
99
+ : selectedOption.icon
100
+ const selectedLabel = isConstructionDimensionActive
101
+ ? (activeConstructionDimensionOption?.label ?? 'Linear dimension')
102
+ : isSmartActive
103
+ ? 'Smart'
104
+ : selectedOption.label
105
+
106
+ const activateMeasurement = (kind: CreatableMeasurementKind) => {
107
+ setPhase('structure')
108
+ setStructureLayer('elements')
109
+ setLastMeasurementKind(kind)
110
+ setToolDefaults('measurement', { kind })
111
+ setMode('build')
112
+ setTool('measurement')
113
+ }
114
+
115
+ const handlePrimaryClick = () => {
116
+ if (isControlActive) {
117
+ setMode('select')
118
+ return
119
+ }
120
+ activateMeasurement(selectedKind)
121
+ }
122
+
123
+ const activateSmartMeasurement = () => {
124
+ setPhase('structure')
125
+ setStructureLayer('elements')
126
+ setToolDefaults('measurement', { kind: 'smart' })
127
+ setMode('build')
128
+ setTool('measurement')
129
+ }
130
+
131
+ const activateConstructionDimension = (
132
+ dimensionMode: ConstructionDimensionMode,
133
+ chainMode: ConstructionDimensionChainMode,
134
+ ) => {
135
+ if (useFloorplanMode.getState().mode !== 'expert') {
136
+ useFloorplanMode.getState().showExpertModeNotice('Construction Dimension')
137
+ return
138
+ }
139
+ setPhase('structure')
140
+ setStructureLayer('elements')
141
+ setViewMode('2d')
142
+ setToolDefaults('construction-dimension', { chainMode, mode: dimensionMode })
143
+ setMode('build')
144
+ setTool('construction-dimension')
145
+ }
146
+
147
+ return (
148
+ <Popover onOpenChange={setIsOpen} open={isOpen}>
149
+ <div className="flex items-center">
150
+ <ActionButton
151
+ aria-label={`Measure: ${selectedLabel}`}
152
+ aria-pressed={isControlActive}
153
+ className={cn(
154
+ 'rounded-r-none p-0 text-muted-foreground',
155
+ isControlActive
156
+ ? 'bg-cyan-500/20 text-cyan-400 hover:bg-cyan-500/20'
157
+ : 'hover:bg-cyan-500/15 hover:text-cyan-400',
158
+ )}
159
+ label={`Measure: ${selectedLabel}`}
160
+ onClick={handlePrimaryClick}
161
+ shortcut="M"
162
+ size="icon"
163
+ variant="ghost"
164
+ >
165
+ <SelectedIcon aria-hidden="true" className="h-5 w-5" />
166
+ </ActionButton>
167
+
168
+ <PopoverTrigger asChild>
169
+ <button
170
+ aria-expanded={isOpen}
171
+ aria-haspopup="menu"
172
+ aria-label="Measurement options"
173
+ className={cn(
174
+ 'flex h-11 w-6 items-center justify-center rounded-r-lg text-muted-foreground transition-colors',
175
+ isOpen
176
+ ? 'bg-cyan-500/15 text-cyan-400'
177
+ : 'hover:bg-cyan-500/10 hover:text-cyan-400',
178
+ )}
179
+ type="button"
180
+ >
181
+ <ChevronDown
182
+ aria-hidden="true"
183
+ className={cn('h-3 w-3 transition-transform', isOpen && 'rotate-180')}
184
+ />
185
+ </button>
186
+ </PopoverTrigger>
187
+ </div>
188
+
189
+ <PopoverContent
190
+ align="center"
191
+ className="max-h-[70vh] w-64 overflow-y-auto rounded-lg border-border/45 bg-background/96 p-2 shadow-elevation-3 backdrop-blur-xl"
192
+ side="top"
193
+ sideOffset={14}
194
+ >
195
+ <div aria-label="Measurement type" className="space-y-1" role="menu">
196
+ {measurementMenuOptions.map((option) => {
197
+ const OptionIcon = option.icon
198
+ const isSmart = option.kind === 'smart'
199
+ const isSelected = isSmart
200
+ ? isSmartActive
201
+ : !isConstructionDimensionActive && !isSmartActive && option.kind === selectedKind
202
+ return (
203
+ <button
204
+ aria-checked={isSelected}
205
+ className={cn(
206
+ 'flex h-9 w-full items-center gap-2 rounded-md px-2.5 text-left text-sm transition-colors',
207
+ isSelected
208
+ ? 'bg-white/10 text-foreground'
209
+ : 'text-muted-foreground hover:bg-white/8 hover:text-foreground',
210
+ )}
211
+ key={option.kind}
212
+ onClick={() => {
213
+ if (isSmart) activateSmartMeasurement()
214
+ else activateMeasurement(option.kind)
215
+ setIsOpen(false)
216
+ }}
217
+ role="menuitemradio"
218
+ type="button"
219
+ >
220
+ <OptionIcon aria-hidden="true" className="h-4 w-4" />
221
+ <span>{option.label}</span>
222
+ {isSelected ? <Check aria-hidden="true" className="ml-auto h-4 w-4" /> : null}
223
+ </button>
224
+ )
225
+ })}
226
+
227
+ {floorplanMode === 'expert' ? (
228
+ <>
229
+ <div className="my-1.5 h-px bg-border/60" />
230
+ <div className="px-2.5 pt-1 pb-0.5 font-semibold text-[10px] text-muted-foreground uppercase tracking-wider">
231
+ Floor plan
232
+ </div>
233
+
234
+ {constructionDimensionOptions.map((option) => {
235
+ const OptionIcon = option.icon
236
+ const isSelected =
237
+ isConstructionDimensionActive && activeConstructionDimensionOption === option
238
+ return (
239
+ <button
240
+ aria-checked={isSelected}
241
+ className={cn(
242
+ 'flex h-9 w-full items-center gap-2 rounded-md px-2.5 text-left text-sm transition-colors',
243
+ isSelected
244
+ ? 'bg-white/10 text-foreground'
245
+ : 'text-muted-foreground hover:bg-white/8 hover:text-foreground',
246
+ )}
247
+ key={`${option.mode}-${option.chainMode}`}
248
+ onClick={() => {
249
+ activateConstructionDimension(option.mode, option.chainMode)
250
+ setIsOpen(false)
251
+ }}
252
+ role="menuitemradio"
253
+ type="button"
254
+ >
255
+ <OptionIcon aria-hidden="true" className="h-4 w-4" />
256
+ <span>{option.label}</span>
257
+ {isSelected ? <Check aria-hidden="true" className="ml-auto h-4 w-4" /> : null}
258
+ </button>
259
+ )
260
+ })}
261
+ </>
262
+ ) : null}
263
+ </div>
264
+ </PopoverContent>
265
+ </Popover>
266
+ )
267
+ }
@@ -12,17 +12,26 @@ export type ToolConfig = {
12
12
  // for cursor/floorplan indicators. Roof-mounted accessories are intentionally
13
13
  // absent — they're placed from the roof inspector's "Add element" section.
14
14
  export const tools: ToolConfig[] = [
15
- { id: 'wall', iconSrc: '/icons/wall.png', label: 'Wall' },
16
- { id: 'door', iconSrc: '/icons/door.png', label: 'Door' },
17
- { id: 'window', iconSrc: '/icons/window.png', label: 'Window' },
18
- { id: 'stair', iconSrc: '/icons/stairs.png', label: 'Stairs' },
19
- { id: 'roof', iconSrc: '/icons/roof.png', label: 'Gable Roof' },
20
- { id: 'fence', iconSrc: '/icons/fence.png', label: 'Fence' },
21
- { id: 'column', iconSrc: '/icons/column.png', label: 'Column' },
22
- { id: 'elevator', iconSrc: '/icons/elevator.png', label: 'Elevator' },
23
- { id: 'slab', iconSrc: '/icons/floor.png', label: 'Slab' },
24
- { id: 'ceiling', iconSrc: '/icons/ceiling.png', label: 'Ceiling' },
25
- { id: 'zone', iconSrc: '/icons/zone.png', label: 'Zone' },
26
- { id: 'spawn', iconSrc: '/icons/spawn-point.png', label: 'Spawn Point' },
27
- { id: 'shelf', iconSrc: '/icons/shelf.png', label: 'Shelf' },
15
+ { id: 'wall', iconSrc: '/icons/wall.webp', label: 'Wall' },
16
+ { id: 'door', iconSrc: '/icons/door.webp', label: 'Door' },
17
+ { id: 'window', iconSrc: '/icons/window.webp', label: 'Window' },
18
+ { id: 'stair', iconSrc: '/icons/stairs.webp', label: 'Stairs' },
19
+ { id: 'roof', iconSrc: '/icons/roof.webp', label: 'Gable Roof' },
20
+ { id: 'fence', iconSrc: '/icons/fence.webp', label: 'Fence' },
21
+ { id: 'column', iconSrc: '/icons/column.webp', label: 'Column' },
22
+ { id: 'elevator', iconSrc: '/icons/elevator.webp', label: 'Elevator' },
23
+ { id: 'slab', iconSrc: '/icons/floor.webp', label: 'Slab' },
24
+ { id: 'ceiling', iconSrc: '/icons/ceiling.webp', label: 'Ceiling' },
25
+ { id: 'zone', iconSrc: '/icons/zone.webp', label: 'Zone' },
26
+ { id: 'spawn', iconSrc: '/icons/spawn-point.webp', label: 'Spawn Point' },
27
+ { id: 'shelf', iconSrc: '/icons/shelf.webp', label: 'Shelf' },
28
+ { id: 'duct-segment', iconSrc: '/icons/duct.webp', label: 'Duct' },
29
+ { id: 'duct-fitting', iconSrc: '/icons/duct-fitting.webp', label: 'Duct Fitting' },
30
+ { id: 'duct-terminal', iconSrc: '/icons/registers.webp', label: 'Register' },
31
+ { id: 'hvac-equipment', iconSrc: '/icons/HVAC.webp', label: 'HVAC Unit' },
32
+ { id: 'pipe-segment', iconSrc: '/icons/dwv-pipes.webp', label: 'DWV Pipe' },
33
+ { id: 'pipe-trap', iconSrc: '/icons/dwv-pipes.webp', label: 'Trap' },
34
+ { id: 'pipe-fitting', iconSrc: '/icons/duct-fitting.webp', label: 'Pipe Fitting' },
35
+ { id: 'lineset', iconSrc: '/icons/lineset.webp', label: 'Lineset' },
36
+ { id: 'liquid-line', iconSrc: '/icons/lineset.webp', label: 'Liquid Line' },
28
37
  ]