@pascal-app/editor 0.8.0 → 0.9.2

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 (350) hide show
  1. package/package.json +15 -13
  2. package/src/components/editor/alignment-3d-guide-layer.tsx +168 -0
  3. package/src/components/editor/bake-exporter.tsx +47 -0
  4. package/src/components/editor/custom-camera-controls.tsx +795 -28
  5. package/src/components/editor/editor-layout-mobile.tsx +8 -3
  6. package/src/components/editor/editor-layout-v2.tsx +76 -51
  7. package/src/components/editor/export-manager.tsx +69 -32
  8. package/src/components/editor/fence-tangent-lines-3d.tsx +87 -0
  9. package/src/components/editor/first-person/build-collider-world.test.ts +182 -0
  10. package/src/components/editor/first-person/build-collider-world.ts +228 -83
  11. package/src/components/editor/first-person-controls.tsx +943 -27
  12. package/src/components/editor/floating-action-menu.tsx +676 -208
  13. package/src/components/editor/floating-building-action-menu.tsx +2 -2
  14. package/src/components/editor/floorplan-background-selection.test.ts +58 -0
  15. package/src/components/editor/floorplan-background-selection.ts +3 -2
  16. package/src/components/editor/floorplan-panel.tsx +6916 -12137
  17. package/src/components/editor/grid.tsx +197 -36
  18. package/src/components/editor/group-actions.ts +457 -0
  19. package/src/components/editor/group-floating-action-menu.tsx +130 -0
  20. package/src/components/editor/group-move-3d.ts +397 -0
  21. package/src/components/editor/group-rotate-handle.tsx +420 -0
  22. package/src/components/editor/group-selection-box-3d.tsx +173 -0
  23. package/src/components/editor/group-transform-shared.test.ts +396 -0
  24. package/src/components/editor/group-transform-shared.ts +489 -0
  25. package/src/components/editor/handles/handle-arrow.tsx +581 -0
  26. package/src/components/editor/handles/use-handle-drag.ts +246 -0
  27. package/src/components/editor/index.tsx +412 -162
  28. package/src/components/editor/measurement-pill.tsx +115 -0
  29. package/src/components/editor/node-action-menu.tsx +14 -1
  30. package/src/components/editor/node-arrow-handles.tsx +1646 -0
  31. package/src/components/editor/opening-guides-3d-layer.tsx +144 -0
  32. package/src/components/editor/quick-measurement-card.tsx +70 -0
  33. package/src/components/editor/quick-measurement-hud.tsx +28 -0
  34. package/src/components/editor/riser-diagram-panel.tsx +137 -0
  35. package/src/components/editor/selection-manager.tsx +1106 -483
  36. package/src/components/editor/site-edge-labels.tsx +45 -21
  37. package/src/components/editor/slab-hole-highlights.tsx +474 -0
  38. package/src/components/editor/snapshot-capture-overlay.tsx +369 -155
  39. package/src/components/editor/three-context-bridge.ts +22 -0
  40. package/src/components/editor/thumbnail-generator.tsx +140 -41
  41. package/src/components/editor/use-floorplan-background-placement.ts +190 -35
  42. package/src/components/editor/use-floorplan-hit-testing.ts +24 -0
  43. package/src/components/editor/use-floorplan-scene-data.ts +19 -7
  44. package/src/components/editor/use-mesh-settle-epoch.ts +26 -0
  45. package/src/components/editor/wall-measurement-label.tsx +14 -19
  46. package/src/components/editor/wall-move-side-handles.tsx +870 -0
  47. package/src/components/editor/wall-opening-highlights.tsx +181 -0
  48. package/src/components/editor/wall-snap-beacon-layer.tsx +304 -0
  49. package/src/components/editor-2d/floorplan-action-menu-layer.tsx +15 -6
  50. package/src/components/editor-2d/floorplan-alignment-guide-layer.tsx +142 -0
  51. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +5 -3
  52. package/src/components/editor-2d/floorplan-group-action-menu.tsx +87 -0
  53. package/src/components/editor-2d/floorplan-group-move.tsx +701 -0
  54. package/src/components/editor-2d/floorplan-measurement-tool-layer.test.ts +113 -0
  55. package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +1656 -0
  56. package/src/components/editor-2d/floorplan-quick-measure-layer.tsx +212 -0
  57. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +392 -0
  58. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +857 -0
  59. package/src/components/editor-2d/floorplan-render-context.tsx +62 -0
  60. package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +77 -0
  61. package/src/components/editor-2d/floorplan-wall-move-ghost-layer.tsx +47 -0
  62. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +266 -0
  63. package/src/components/editor-2d/renderers/floorplan-label-angle.test.ts +15 -0
  64. package/src/components/editor-2d/renderers/floorplan-label-angle.ts +14 -0
  65. package/src/components/editor-2d/renderers/floorplan-measurements-layer.tsx +108 -66
  66. package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +62 -0
  67. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +308 -0
  68. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +3325 -0
  69. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +24 -9
  70. package/src/components/editor-2d/renderers/floorplan-voronoi-layer.tsx +128 -0
  71. package/src/components/editor-2d/svg-paths.ts +10 -2
  72. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +439 -57
  73. package/src/components/systems/ceiling/ceiling-system.tsx +109 -12
  74. package/src/components/systems/roof/roof-edit-system.tsx +1851 -22
  75. package/src/components/systems/selection-affordance-manager.tsx +38 -0
  76. package/src/components/systems/stair/stair-edit-system.tsx +1 -1
  77. package/src/components/systems/zone/zone-label-editor-system.tsx +14 -1
  78. package/src/components/systems/zone/zone-system.tsx +42 -13
  79. package/src/components/tools/elevator/elevator-defaults.ts +8 -0
  80. package/src/components/tools/elevator/elevator-tool.tsx +375 -0
  81. package/src/components/tools/elevator/move-elevator-tool.tsx +263 -0
  82. package/src/components/tools/fence/fence-drafting.ts +141 -27
  83. package/src/components/tools/item/move-tool.tsx +45 -105
  84. package/src/components/tools/item/placement-math.test.ts +22 -0
  85. package/src/components/tools/item/placement-math.ts +59 -7
  86. package/src/components/tools/item/placement-strategies.ts +432 -16
  87. package/src/components/tools/item/placement-types.ts +22 -1
  88. package/src/components/tools/item/use-draft-node.ts +51 -2
  89. package/src/components/tools/item/use-placement-coordinator.tsx +1399 -256
  90. package/src/components/tools/registry/move-registry-node-tool.tsx +1064 -0
  91. package/src/components/tools/roof/roof-tool.tsx +350 -28
  92. package/src/components/tools/select/box-select-state.ts +88 -0
  93. package/src/components/tools/select/box-select-tool.tsx +442 -520
  94. package/src/components/tools/select/marquee-geometry.test.ts +161 -0
  95. package/src/components/tools/select/marquee-geometry.ts +155 -0
  96. package/src/components/tools/select/plane-box-select-tool.tsx +570 -0
  97. package/src/components/tools/select/screen-rectangle-selection.ts +84 -0
  98. package/src/components/tools/select/select-candidates.test.ts +109 -0
  99. package/src/components/tools/select/select-candidates.ts +85 -0
  100. package/src/components/tools/shared/affordance-dispatch.ts +30 -0
  101. package/src/components/tools/shared/cursor-sphere.tsx +62 -26
  102. package/src/components/tools/shared/drag-bounding-box.tsx +195 -0
  103. package/src/components/tools/shared/facing-indicator.tsx +87 -0
  104. package/src/components/tools/shared/facing-pose-indicator.tsx +54 -0
  105. package/src/components/tools/shared/floor-stack-preview.ts +25 -0
  106. package/src/components/tools/shared/fresh-placement-visibility.ts +61 -0
  107. package/src/components/tools/shared/placement-box-geometry.ts +123 -0
  108. package/src/components/tools/shared/placement-box.tsx +305 -0
  109. package/src/components/tools/shared/polygon-editor.tsx +866 -175
  110. package/src/components/tools/shared/segment-angle.ts +56 -0
  111. package/src/components/tools/site/site-boundary-editor.tsx +520 -16
  112. package/src/components/tools/site/site-flag-model.ts +18 -0
  113. package/src/components/tools/stair/stair-defaults.ts +2 -1
  114. package/src/components/tools/stair/stair-tool.tsx +342 -37
  115. package/src/components/tools/tool-manager.tsx +239 -62
  116. package/src/components/tools/wall/wall-drafting.test.ts +330 -0
  117. package/src/components/tools/wall/wall-drafting.ts +248 -171
  118. package/src/components/tools/wall/wall-snap-geometry.test.ts +123 -0
  119. package/src/components/tools/wall/wall-snap-geometry.ts +331 -0
  120. package/src/components/tools/zone/zone-boundary-editor.tsx +5 -1
  121. package/src/components/tools/zone/zone-tool.tsx +86 -82
  122. package/src/components/ui/action-menu/action-button.tsx +21 -1
  123. package/src/components/ui/action-menu/camera-actions.tsx +24 -17
  124. package/src/components/ui/action-menu/control-modes.tsx +40 -170
  125. package/src/components/ui/action-menu/furnish-tools.tsx +5 -5
  126. package/src/components/ui/action-menu/index.tsx +3 -117
  127. package/src/components/ui/action-menu/measurement-control.tsx +188 -0
  128. package/src/components/ui/action-menu/structure-tools.tsx +27 -93
  129. package/src/components/ui/action-menu/view-toggles.tsx +279 -90
  130. package/src/components/ui/command-palette/editor-commands.tsx +20 -15
  131. package/src/components/ui/command-palette/index.tsx +5 -3
  132. package/src/components/ui/controls/material-paint-panel.tsx +152 -0
  133. package/src/components/ui/controls/material-picker.tsx +87 -177
  134. package/src/components/ui/controls/material-properties-editor.tsx +108 -0
  135. package/src/components/ui/controls/metric-control.tsx +142 -46
  136. package/src/components/ui/controls/scene-material-list.tsx +247 -0
  137. package/src/components/ui/controls/slider-control.tsx +88 -38
  138. package/src/components/ui/floating-level-selector.tsx +47 -10
  139. package/src/components/ui/helpers/building-helper.tsx +10 -23
  140. package/src/components/ui/helpers/contextual-helper-panel.tsx +428 -0
  141. package/src/components/ui/helpers/helper-manager.tsx +235 -23
  142. package/src/components/ui/helpers/item-helper.tsx +28 -32
  143. package/src/components/ui/helpers/registered-tool-helper.tsx +59 -0
  144. package/src/components/ui/helpers/roof-helper.tsx +10 -12
  145. package/src/components/ui/icon-ref.tsx +47 -0
  146. package/src/components/ui/item-catalog/catalog-items.tsx +40 -1
  147. package/src/components/ui/item-catalog/item-catalog.tsx +16 -36
  148. package/src/components/ui/level-duplicate-dialog.tsx +2 -1
  149. package/src/components/ui/panels/node-display.ts +17 -16
  150. package/src/components/ui/panels/panel-manager.tsx +55 -58
  151. package/src/components/ui/panels/panel-wrapper.tsx +238 -12
  152. package/src/components/ui/panels/parametric-inspector.tsx +482 -0
  153. package/src/components/ui/panels/reference-panel.tsx +54 -19
  154. package/src/components/ui/primitives/dropdown-menu.tsx +12 -8
  155. package/src/components/ui/primitives/shortcut-token.tsx +39 -3
  156. package/src/components/ui/primitives/sidebar.tsx +1 -0
  157. package/src/components/ui/scene-loader.tsx +1 -3
  158. package/src/components/ui/sidebar/app-sidebar.tsx +5 -1
  159. package/src/components/ui/sidebar/icon-rail.tsx +50 -31
  160. package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +263 -0
  161. package/src/components/ui/sidebar/panels/items-panel/index.tsx +82 -8
  162. package/src/components/ui/sidebar/panels/plugins-panel.tsx +218 -0
  163. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +122 -41
  164. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +66 -7
  165. package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +267 -0
  166. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +3 -1
  167. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +9 -5
  168. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +86 -0
  169. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -1
  170. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +7 -2
  171. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +92 -0
  172. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +75 -0
  173. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +3 -3
  174. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +86 -0
  175. package/src/components/ui/sidebar/panels/site-panel/index.tsx +42 -21
  176. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +31 -17
  177. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +2 -1
  178. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +140 -0
  179. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +48 -8
  180. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +132 -0
  181. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +9 -5
  182. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +86 -0
  183. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +7 -1
  184. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +2 -2
  185. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +112 -39
  186. package/src/components/ui/sidebar/panels/site-panel/tree-structure.ts +36 -0
  187. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -1
  188. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +7 -2
  189. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +3 -3
  190. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +51 -2
  191. package/src/components/ui/sidebar/tab-bar.tsx +75 -1
  192. package/src/components/ui/sidebar/use-plugin-panels.tsx +132 -0
  193. package/src/components/ui/snap-target-badge.test.tsx +40 -0
  194. package/src/components/ui/snap-target-badge.tsx +88 -0
  195. package/src/components/viewer-overlay.tsx +253 -61
  196. package/src/components/viewer-zone-system.tsx +7 -2
  197. package/src/hooks/use-auto-save.test.ts +14 -0
  198. package/src/hooks/use-auto-save.ts +64 -10
  199. package/src/hooks/use-ceiling-events.ts +176 -0
  200. package/src/hooks/use-drag-action.ts +127 -0
  201. package/src/hooks/use-keyboard.ts +493 -95
  202. package/src/hooks/use-selection.ts +64 -0
  203. package/src/index.tsx +451 -6
  204. package/src/lib/active-placement-surface.ts +35 -0
  205. package/src/lib/ceiling-plan-snap.ts +24 -0
  206. package/src/lib/constants.ts +6 -2
  207. package/src/lib/contextual-help.test.ts +112 -0
  208. package/src/lib/contextual-help.ts +144 -0
  209. package/src/lib/continuation.ts +64 -0
  210. package/src/lib/direct-manipulation.test.ts +192 -0
  211. package/src/lib/direct-manipulation.ts +109 -0
  212. package/src/lib/editor-api.ts +50 -0
  213. package/src/lib/elevator-support.ts +96 -0
  214. package/src/lib/floorplan/apply-alignment.test.ts +25 -0
  215. package/src/lib/floorplan/apply-alignment.ts +117 -0
  216. package/src/lib/floorplan/floorplan-export.tsx +364 -0
  217. package/src/lib/floorplan/geometry.ts +53 -0
  218. package/src/lib/floorplan/index.ts +10 -0
  219. package/src/lib/floorplan/items.ts +5 -2
  220. package/src/lib/floorplan/plan-coords.ts +21 -0
  221. package/src/lib/floorplan/selection-tool.ts +40 -0
  222. package/src/lib/floorplan/stairs.ts +12 -1
  223. package/src/lib/fresh-planar-placement.test.ts +339 -0
  224. package/src/lib/fresh-planar-placement.ts +130 -0
  225. package/src/lib/glb-export.test.ts +441 -0
  226. package/src/lib/glb-export.ts +874 -0
  227. package/src/lib/history.ts +11 -1
  228. package/src/lib/interaction/hot-set.test.ts +133 -0
  229. package/src/lib/interaction/hot-set.ts +67 -0
  230. package/src/lib/interaction/overlay-policy.test.ts +51 -0
  231. package/src/lib/interaction/overlay-policy.ts +59 -0
  232. package/src/lib/interaction/scope.ts +211 -0
  233. package/src/lib/level-duplication.ts +54 -42
  234. package/src/lib/level-selection.ts +2 -2
  235. package/src/lib/material-paint.ts +227 -48
  236. package/src/lib/measurement-kind.test.ts +30 -0
  237. package/src/lib/measurement-kind.ts +19 -0
  238. package/src/lib/measurement-label.test.ts +47 -0
  239. package/src/lib/measurement-label.ts +68 -0
  240. package/src/lib/measurement-parser.ts +116 -0
  241. package/src/lib/measurements.test.ts +183 -0
  242. package/src/lib/measurements.ts +199 -0
  243. package/src/lib/paint-scope.test.ts +454 -0
  244. package/src/lib/paint-scope.ts +461 -0
  245. package/src/lib/placement-drag-release.ts +23 -0
  246. package/src/lib/placement-metadata.ts +29 -0
  247. package/src/lib/planar-cursor-placement.test.ts +100 -0
  248. package/src/lib/planar-cursor-placement.ts +42 -0
  249. package/src/lib/plugin-panels.test.ts +19 -0
  250. package/src/lib/plugin-panels.ts +91 -0
  251. package/src/lib/quick-measurement.test.ts +77 -0
  252. package/src/lib/quick-measurement.ts +109 -0
  253. package/src/lib/roof-duplication.ts +7 -7
  254. package/src/lib/roof-hover-outline-proxy.ts +22 -0
  255. package/src/lib/roof-wall-hit.ts +164 -0
  256. package/src/lib/scene-clipboard.test.ts +196 -0
  257. package/src/lib/scene-clipboard.ts +332 -0
  258. package/src/lib/scene.ts +40 -8
  259. package/src/lib/selection-routing.test.ts +298 -0
  260. package/src/lib/selection-routing.ts +175 -0
  261. package/src/lib/sfx/index.ts +1 -0
  262. package/src/lib/sfx/movement-tick.test.ts +65 -0
  263. package/src/lib/sfx/movement-tick.ts +18 -0
  264. package/src/lib/sfx-bus.ts +69 -10
  265. package/src/lib/sfx-player.test.ts +124 -0
  266. package/src/lib/sfx-player.ts +148 -55
  267. package/src/lib/site-boundary.ts +1 -0
  268. package/src/lib/slab-plan-snap.test.ts +93 -0
  269. package/src/lib/slab-plan-snap.ts +108 -0
  270. package/src/lib/snapping-mode.test.ts +138 -0
  271. package/src/lib/snapping-mode.ts +179 -0
  272. package/src/lib/stair-duplication.ts +4 -4
  273. package/src/lib/stair-levels.test.ts +146 -0
  274. package/src/lib/stair-levels.ts +177 -0
  275. package/src/lib/surface-plan-snap.test.ts +71 -0
  276. package/src/lib/surface-plan-snap.ts +256 -0
  277. package/src/lib/use-linear-display.ts +40 -0
  278. package/src/lib/world-grid-snap.ts +231 -0
  279. package/src/lib/zone-content.ts +130 -0
  280. package/src/store/use-alignment-guides.ts +21 -0
  281. package/src/store/use-direct-manipulation-feedback.ts +20 -0
  282. package/src/store/use-editor.tsx +767 -114
  283. package/src/store/use-facing-pose.ts +44 -0
  284. package/src/store/use-fence-curve-draft.ts +21 -0
  285. package/src/store/use-floorplan-draft-preview.ts +101 -0
  286. package/src/store/use-floorplan-marquee.ts +50 -0
  287. package/src/store/use-interaction-scope.test.ts +186 -0
  288. package/src/store/use-interaction-scope.ts +132 -0
  289. package/src/store/use-measurement-draft.test.ts +530 -0
  290. package/src/store/use-measurement-draft.ts +543 -0
  291. package/src/store/use-opening-guides.ts +41 -0
  292. package/src/store/use-placement-preview.ts +39 -0
  293. package/src/store/use-quick-measurement-hud.test.ts +53 -0
  294. package/src/store/use-quick-measurement-hud.ts +77 -0
  295. package/src/store/use-segment-draft-chain.ts +28 -0
  296. package/src/store/use-stair-build-preview.ts +43 -0
  297. package/src/store/use-wall-move-ghosts.ts +36 -0
  298. package/src/store/use-wall-snap-indicator.ts +34 -0
  299. package/src/components/editor/first-person/bvh-ecctrl.tsx +0 -860
  300. package/src/components/editor/preset-thumbnail-generator.tsx +0 -125
  301. package/src/components/editor-2d/renderers/floorplan-roof-layer.tsx +0 -113
  302. package/src/components/tools/building/move-building-tool.tsx +0 -157
  303. package/src/components/tools/ceiling/ceiling-boundary-editor.tsx +0 -43
  304. package/src/components/tools/ceiling/ceiling-hole-editor.tsx +0 -49
  305. package/src/components/tools/ceiling/ceiling-tool.tsx +0 -465
  306. package/src/components/tools/ceiling/move-ceiling-tool.tsx +0 -264
  307. package/src/components/tools/column/column-tool.tsx +0 -97
  308. package/src/components/tools/column/move-column-tool.tsx +0 -105
  309. package/src/components/tools/door/door-math.ts +0 -110
  310. package/src/components/tools/door/door-tool.tsx +0 -324
  311. package/src/components/tools/door/move-door-tool.tsx +0 -412
  312. package/src/components/tools/fence/curve-fence-tool.tsx +0 -178
  313. package/src/components/tools/fence/fence-tool.tsx +0 -346
  314. package/src/components/tools/fence/move-fence-endpoint-tool.tsx +0 -441
  315. package/src/components/tools/fence/move-fence-tool.tsx +0 -302
  316. package/src/components/tools/item/item-tool.tsx +0 -26
  317. package/src/components/tools/roof/move-roof-tool.tsx +0 -364
  318. package/src/components/tools/slab/move-slab-tool.tsx +0 -182
  319. package/src/components/tools/slab/slab-boundary-editor.tsx +0 -43
  320. package/src/components/tools/slab/slab-hole-editor.tsx +0 -49
  321. package/src/components/tools/slab/slab-tool.tsx +0 -322
  322. package/src/components/tools/spawn/move-spawn-tool.tsx +0 -101
  323. package/src/components/tools/spawn/spawn-tool.tsx +0 -130
  324. package/src/components/tools/wall/curve-wall-tool.tsx +0 -178
  325. package/src/components/tools/wall/move-wall-endpoint-tool.tsx +0 -426
  326. package/src/components/tools/wall/move-wall-tool.tsx +0 -358
  327. package/src/components/tools/wall/wall-tool.tsx +0 -332
  328. package/src/components/tools/window/move-window-tool.tsx +0 -447
  329. package/src/components/tools/window/window-math.ts +0 -117
  330. package/src/components/tools/window/window-tool.tsx +0 -332
  331. package/src/components/ui/helpers/ceiling-helper.tsx +0 -20
  332. package/src/components/ui/helpers/slab-helper.tsx +0 -20
  333. package/src/components/ui/helpers/wall-helper.tsx +0 -20
  334. package/src/components/ui/panels/ceiling-panel.tsx +0 -249
  335. package/src/components/ui/panels/column-panel.tsx +0 -759
  336. package/src/components/ui/panels/door-panel.tsx +0 -1299
  337. package/src/components/ui/panels/fence-panel.tsx +0 -222
  338. package/src/components/ui/panels/item-panel.tsx +0 -306
  339. package/src/components/ui/panels/paint-panel.tsx +0 -163
  340. package/src/components/ui/panels/presets/presets-popover.tsx +0 -511
  341. package/src/components/ui/panels/roof-panel.tsx +0 -280
  342. package/src/components/ui/panels/roof-segment-panel.tsx +0 -311
  343. package/src/components/ui/panels/slab-panel.tsx +0 -250
  344. package/src/components/ui/panels/spawn-panel.tsx +0 -161
  345. package/src/components/ui/panels/stair-panel.tsx +0 -577
  346. package/src/components/ui/panels/stair-segment-panel.tsx +0 -325
  347. package/src/components/ui/panels/wall-panel.tsx +0 -273
  348. package/src/components/ui/panels/window-panel.tsx +0 -968
  349. package/src/contexts/presets-context.tsx +0 -121
  350. package/src/hooks/use-contextual-tools.ts +0 -61
@@ -0,0 +1,482 @@
1
+ 'use client'
2
+
3
+ import {
4
+ type AnyNode,
5
+ type AnyNodeId,
6
+ type IconRef,
7
+ nodeRegistry,
8
+ type ParamAction,
9
+ type ParamField,
10
+ useScene,
11
+ type ZoneNode,
12
+ } from '@pascal-app/core'
13
+ import { useViewer } from '@pascal-app/viewer'
14
+ import { Icon } from '@iconify/react'
15
+ import { Move, Trash2 } from 'lucide-react'
16
+ import { type ComponentType, lazy, Suspense, useCallback } from 'react'
17
+ import { sfxEmitter } from '../../../lib/sfx-bus'
18
+ import { collectZoneContentIds } from '../../../lib/zone-content'
19
+ import useEditor from '../../../store/use-editor'
20
+ import { ActionButton, ActionGroup } from '../controls/action-button'
21
+ import { PanelSection } from '../controls/panel-section'
22
+ import { SegmentedControl } from '../controls/segmented-control'
23
+ import { SliderControl } from '../controls/slider-control'
24
+ import { ToggleControl } from '../controls/toggle-control'
25
+ import { InspectorFooterContext, PanelWrapper } from './panel-wrapper'
26
+
27
+ /**
28
+ * Auto-derived right-panel inspector for any registry-backed node.
29
+ *
30
+ * Reads `definition.parametrics` from the registry and renders one
31
+ * `<PanelSection>` per group, one control per field. Field kinds supported:
32
+ * - `number` → SliderControl with min/max/step/unit from the descriptor
33
+ * - `enum` → dark-themed `<select>`
34
+ * - `color` → native color picker + hex input
35
+ * - `vec3` → three SliderControls for X / Y / Z
36
+ *
37
+ * Generic Actions section appends Move / Delete based on `capabilities`.
38
+ *
39
+ * Phase 4 will expand this with per-field `customEditor` support and a
40
+ * `parametrics.customPanel?` escape hatch for kinds whose parametric editor
41
+ * can't be auto-generated (topology editors etc.).
42
+ */
43
+ export function ParametricInspector({
44
+ footer,
45
+ nodeId,
46
+ onClose,
47
+ }: { footer?: React.ReactNode; nodeId?: AnyNodeId; onClose?: () => void } = {}) {
48
+ const selectedIdFromSelection = useViewer((s) => s.selection.selectedIds[0]) as
49
+ | AnyNodeId
50
+ | undefined
51
+ const selectedId = nodeId ?? selectedIdFromSelection
52
+ const setSelection = useViewer((s) => s.setSelection)
53
+ // Subscribe only to the *type* — a string primitive that doesn't change
54
+ // when slider values change. Without this, every updateNode tick during
55
+ // a drag re-renders the entire panel + every field + every SliderControl.
56
+ // Per-field subscriptions live on FieldRenderer below.
57
+ const nodeType = useScene((s) => (selectedId ? (s.nodes[selectedId]?.type ?? null) : null))
58
+
59
+ const def = nodeType ? nodeRegistry.get(nodeType) : undefined
60
+ const parametrics = def?.parametrics
61
+
62
+ const handleUpdate = useCallback(
63
+ (patch: Partial<AnyNode>) => {
64
+ if (!selectedId) return
65
+ const scene = useScene.getState()
66
+ const node = scene.nodes[selectedId]
67
+ if (parametrics?.derive && node) {
68
+ const next = { ...node, ...patch } as AnyNode
69
+ patch = { ...patch, ...parametrics.derive(next, patch) }
70
+ }
71
+ // Bundle the edited node + any reconcile follow-ups into ONE
72
+ // updateNodes call so a single inspector edit is a single undo step.
73
+ const updates: { id: AnyNodeId; data: Partial<AnyNode> }[] = [{ id: selectedId, data: patch }]
74
+ if (parametrics?.reconcile && node) {
75
+ const next = { ...node, ...patch } as AnyNode
76
+ updates.push(...parametrics.reconcile(node as AnyNode, next))
77
+ }
78
+ scene.updateNodes(updates)
79
+ },
80
+ [selectedId, parametrics],
81
+ )
82
+
83
+ const clearSelection = useCallback(() => {
84
+ if (onClose) {
85
+ onClose()
86
+ return
87
+ }
88
+ setSelection({ selectedIds: [] })
89
+ }, [onClose, setSelection])
90
+
91
+ const handleMove = useCallback(() => {
92
+ if (!selectedId) return
93
+ const node = useScene.getState().nodes[selectedId]
94
+ if (!node) return
95
+ sfxEmitter.emit('sfx:item-pick')
96
+ useEditor.getState().setMovingNode(node as any)
97
+ clearSelection()
98
+ }, [selectedId, clearSelection])
99
+
100
+ const handleDelete = useCallback(
101
+ (withZoneContent = false) => {
102
+ if (!selectedId) return
103
+ const scene = useScene.getState()
104
+ const node = scene.nodes[selectedId]
105
+ if (!node) return
106
+
107
+ const ids =
108
+ withZoneContent && node.type === 'zone'
109
+ ? [selectedId, ...collectZoneContentIds(scene.nodes, node as ZoneNode)]
110
+ : [selectedId]
111
+
112
+ sfxEmitter.emit('sfx:structure-delete')
113
+ scene.deleteNodes(Array.from(new Set(ids)))
114
+ clearSelection()
115
+ },
116
+ [selectedId, clearSelection],
117
+ )
118
+
119
+ if (!selectedId || !def || !parametrics) return null
120
+
121
+ // `parametrics.customPanel` escape hatch — kind owns its panel
122
+ // entirely (loaded lazily so the bundle isn't eager). Used by kinds
123
+ // whose editor has non-parametric concerns (slab holes list, ceiling
124
+ // height presets, etc.) until per-field `customEditor` + missing
125
+ // field kinds (list/action/computed) graduate the auto-derived
126
+ // panel to cover them.
127
+ if (parametrics.customPanel) {
128
+ const CustomPanel = resolveCustomPanel(parametrics.customPanel)
129
+ // Custom panels render their own `<PanelWrapper>` and don't thread a
130
+ // `footer` prop, so hand the host footer down via context.
131
+ return (
132
+ <InspectorFooterContext.Provider value={footer}>
133
+ <Suspense fallback={null}>
134
+ <CustomPanel />
135
+ </Suspense>
136
+ </InspectorFooterContext.Provider>
137
+ )
138
+ }
139
+
140
+ const presentation = def.presentation
141
+ const title = presentation?.label ?? nodeType ?? ''
142
+ const iconNode = renderIcon(presentation?.icon)
143
+ const canMove = !!def.capabilities.movable
144
+ const canDelete = def.capabilities.deletable !== false
145
+ const isZone = nodeType === 'zone'
146
+
147
+ const TrailingSection = parametrics.trailingSection
148
+ ? resolveCustomPanel(parametrics.trailingSection)
149
+ : null
150
+
151
+ return (
152
+ <PanelWrapper
153
+ footer={footer}
154
+ icon={iconNode}
155
+ onClose={clearSelection}
156
+ title={title}
157
+ width={320}
158
+ >
159
+ {parametrics.groups.map((group, gi) => (
160
+ <PanelSection key={`group-${gi}`} title={group.label}>
161
+ {group.fields.map((field, fi) => (
162
+ <FieldRenderer
163
+ key={`field-${gi}-${fi}-${String(field.key)}`}
164
+ field={field as ParamField<AnyNode>}
165
+ nodeId={selectedId}
166
+ onUpdate={handleUpdate}
167
+ />
168
+ ))}
169
+ </PanelSection>
170
+ ))}
171
+ {TrailingSection && (
172
+ <Suspense fallback={null}>
173
+ <TrailingSection />
174
+ </Suspense>
175
+ )}
176
+ {(canMove || canDelete || (parametrics.actions && parametrics.actions.length > 0)) && (
177
+ <PanelSection title="Actions">
178
+ <ActionGroup className={isZone ? 'flex-col' : undefined}>
179
+ {canMove && (
180
+ <ActionButton icon={<Move className="h-4 w-4" />} label="Move" onClick={handleMove} />
181
+ )}
182
+ {parametrics.actions?.map((action, i) => (
183
+ <ParamActionButton action={action} key={`paramaction-${i}`} nodeId={selectedId} />
184
+ ))}
185
+ {canDelete &&
186
+ (isZone ? (
187
+ <>
188
+ <ActionButton
189
+ className="w-full flex-none"
190
+ icon={<Trash2 className="h-4 w-4 text-red-400" />}
191
+ label="Delete"
192
+ onClick={() => handleDelete(false)}
193
+ />
194
+ <ActionButton
195
+ className="w-full flex-none"
196
+ icon={<Trash2 className="h-4 w-4 text-red-400" />}
197
+ label="Delete with contents"
198
+ onClick={() => handleDelete(true)}
199
+ />
200
+ </>
201
+ ) : (
202
+ <ActionButton
203
+ className="border-red-500/40 text-red-200 hover:bg-red-500/15"
204
+ icon={<Trash2 className="h-4 w-4" />}
205
+ label="Delete"
206
+ onClick={() => handleDelete()}
207
+ />
208
+ ))}
209
+ </ActionGroup>
210
+ </PanelSection>
211
+ )}
212
+ </PanelWrapper>
213
+ )
214
+ }
215
+
216
+ // One inspector action button. Subscribes to `enabledIf`'s boolean result
217
+ // (same pattern as FieldRenderer's `visible`) so the disabled state stays
218
+ // live as the scene mutates — `===` on the boolean keeps unrelated ticks
219
+ // from re-rendering it. The click handler re-reads the live node so the
220
+ // handler always acts on current state.
221
+ function ParamActionButton({ action, nodeId }: { action: ParamAction<AnyNode>; nodeId: AnyNodeId }) {
222
+ const disabled = useScene((s) => {
223
+ if (!action.enabledIf) return false
224
+ const n = s.nodes[nodeId]
225
+ return n ? !action.enabledIf(n as AnyNode) : false
226
+ })
227
+ return (
228
+ <ActionButton
229
+ className={disabled ? 'opacity-40 pointer-events-none' : ''}
230
+ icon={
231
+ action.iconSrc ? (
232
+ <img alt="" className="h-4 w-4 shrink-0 object-contain" src={action.iconSrc} />
233
+ ) : undefined
234
+ }
235
+ label={action.label}
236
+ onClick={() => {
237
+ const live = useScene.getState().nodes[nodeId]
238
+ if (live) action.onClick(live as AnyNode)
239
+ }}
240
+ />
241
+ )
242
+ }
243
+
244
+ function renderIcon(ref: IconRef | undefined): React.ReactNode | undefined {
245
+ if (!ref) return undefined
246
+ if (ref.kind === 'url') {
247
+ // Plain <img> here so the inspector doesn't pull in next/image's
248
+ // server-only requirements (the file is `'use client'`). Same
249
+ // 16x16 box the legacy panels use.
250
+ return <img alt="" className="h-4 w-4 shrink-0 object-contain" src={ref.src} />
251
+ }
252
+ if (ref.kind === 'iconify') {
253
+ return <Icon height={16} icon={ref.name} width={16} />
254
+ }
255
+ if (ref.kind === 'svg') {
256
+ return (
257
+ <svg height={16} viewBox={ref.viewBox} width={16}>
258
+ <path d={ref.path} fill="currentColor" />
259
+ </svg>
260
+ )
261
+ }
262
+ // `component`: lazy-loaded custom icon component. Suspense-safe.
263
+ const LazyIcon = lazy(ref.module)
264
+ return (
265
+ <Suspense fallback={null}>
266
+ <LazyIcon />
267
+ </Suspense>
268
+ )
269
+ }
270
+
271
+ // Cache lazy custom panel components by their loader so React.lazy isn't
272
+ // re-invoked across renders.
273
+ const customPanelCache = new WeakMap<() => Promise<unknown>, ComponentType>()
274
+
275
+ function resolveCustomPanel(loader: () => Promise<{ default: ComponentType<any> }>): ComponentType {
276
+ const cached = customPanelCache.get(loader)
277
+ if (cached) return cached
278
+ const Comp = lazy(loader)
279
+ customPanelCache.set(loader, Comp as ComponentType)
280
+ return Comp as ComponentType
281
+ }
282
+
283
+ // ─── Per-field renderers ─────────────────────────────────────────────
284
+
285
+ interface FieldRendererProps {
286
+ field: ParamField<AnyNode>
287
+ nodeId: AnyNodeId
288
+ onUpdate: (patch: Partial<AnyNode>) => void
289
+ }
290
+
291
+ function FieldRenderer({ field, nodeId, onUpdate }: FieldRendererProps) {
292
+ const key = String(field.key)
293
+ // Subscribe only to this field's value. Zustand compares with ===, so when
294
+ // another field on the same node changes (which produces a new node object
295
+ // reference), this primitive value stays equal and the field doesn't
296
+ // re-render. Vec3 arrays get a new reference only when the array itself
297
+ // changes — same outcome.
298
+ const value = useScene((s) => {
299
+ const n = s.nodes[nodeId]
300
+ return n ? (n as Record<string, unknown>)[key] : undefined
301
+ })
302
+ // visibleIf may consult other fields on the node — subscribe to its boolean
303
+ // result so we re-evaluate when relevant.
304
+ const visible = useScene((s) => {
305
+ const visibleIf = (field as { visibleIf?: (n: AnyNode) => boolean }).visibleIf
306
+ if (!visibleIf) return true
307
+ const n = s.nodes[nodeId]
308
+ return n ? visibleIf(n as AnyNode) : false
309
+ })
310
+ if (!visible) return null
311
+
312
+ switch (field.kind) {
313
+ case 'number': {
314
+ const num = typeof value === 'number' ? value : 0
315
+ const step = field.step ?? 0.01
316
+ const precision = precisionForStep(step)
317
+ return (
318
+ <SliderControl
319
+ label={prettifyKey(key)}
320
+ max={field.max}
321
+ min={field.min}
322
+ onChange={(next) => onUpdate({ [key]: next } as Partial<AnyNode>)}
323
+ precision={precision}
324
+ step={step}
325
+ unit={field.unit ?? ''}
326
+ value={num}
327
+ />
328
+ )
329
+ }
330
+
331
+ case 'boolean': {
332
+ const checked = value === true
333
+ return (
334
+ <ToggleControl
335
+ checked={checked}
336
+ label={prettifyKey(key)}
337
+ onChange={(next) => onUpdate({ [key]: next } as Partial<AnyNode>)}
338
+ />
339
+ )
340
+ }
341
+
342
+ case 'enum': {
343
+ const str = typeof value === 'string' ? value : (field.options[0] ?? '')
344
+ if (field.display === 'segmented') {
345
+ return (
346
+ <SegmentedControl
347
+ onChange={(next) => onUpdate({ [key]: next } as Partial<AnyNode>)}
348
+ options={field.options.map((opt) => ({ label: prettifyEnumValue(opt), value: opt }))}
349
+ value={str}
350
+ />
351
+ )
352
+ }
353
+ return (
354
+ <div className="flex items-center justify-between px-3 py-2">
355
+ <span className="text-foreground/80 text-xs">{prettifyKey(key)}</span>
356
+ <select
357
+ className="rounded-md border border-border/50 bg-[#2C2C2E] px-2 py-1 text-foreground text-xs focus:outline-none focus:ring-1 focus:ring-foreground/30"
358
+ onChange={(e) => onUpdate({ [key]: e.target.value } as Partial<AnyNode>)}
359
+ value={str}
360
+ >
361
+ {field.options.map((opt) => (
362
+ <option key={opt} value={opt}>
363
+ {prettifyEnumValue(opt)}
364
+ </option>
365
+ ))}
366
+ </select>
367
+ </div>
368
+ )
369
+ }
370
+
371
+ case 'color': {
372
+ const str = typeof value === 'string' ? value : '#888888'
373
+ return (
374
+ <div className="flex items-center justify-between px-3 py-2">
375
+ <span className="text-foreground/80 text-xs">{prettifyKey(key)}</span>
376
+ <div className="flex items-center gap-2">
377
+ <input
378
+ className="h-6 w-8 cursor-pointer rounded border border-border/50 bg-transparent"
379
+ onChange={(e) => onUpdate({ [key]: e.target.value } as Partial<AnyNode>)}
380
+ type="color"
381
+ value={str}
382
+ />
383
+ <input
384
+ className="w-20 rounded-md border border-border/50 bg-[#2C2C2E] px-2 py-1 text-foreground text-xs focus:outline-none focus:ring-1 focus:ring-foreground/30"
385
+ onChange={(e) => onUpdate({ [key]: e.target.value } as Partial<AnyNode>)}
386
+ type="text"
387
+ value={str}
388
+ />
389
+ </div>
390
+ </div>
391
+ )
392
+ }
393
+
394
+ case 'vec3': {
395
+ const v = Array.isArray(value) && value.length >= 3
396
+ ? (value as [number, number, number])
397
+ : [0, 0, 0]
398
+ const axes: Array<{ label: string; index: 0 | 1 | 2 }> = [
399
+ { label: 'X', index: 0 },
400
+ { label: 'Y', index: 1 },
401
+ { label: 'Z', index: 2 },
402
+ ]
403
+ return (
404
+ <>
405
+ {axes.map(({ label, index }) => {
406
+ // v is a [number, number, number] tuple; the explicit local
407
+ // resolves TS's noUncheckedIndexedAccess concern that v[index]
408
+ // could be undefined.
409
+ const axisValue = v[index] ?? 0
410
+ return (
411
+ <SliderControl
412
+ key={`${key}-${label}`}
413
+ label={label}
414
+ max={axisValue + 5}
415
+ min={axisValue - 5}
416
+ onChange={(next) => {
417
+ const updated = [...v] as [number, number, number]
418
+ updated[index] = next
419
+ onUpdate({ [key]: updated } as Partial<AnyNode>)
420
+ }}
421
+ precision={2}
422
+ step={0.05}
423
+ unit="m"
424
+ value={Math.round(axisValue * 100) / 100}
425
+ />
426
+ )
427
+ })}
428
+ </>
429
+ )
430
+ }
431
+
432
+ case 'custom':
433
+ // The field owns its rendering and update logic — used for
434
+ // derived values (length from start/end), dynamic-bounded
435
+ // sliders (curve sagitta), composed editors.
436
+ return <CustomFieldRenderer Comp={field.component} nodeId={nodeId} onUpdate={onUpdate} />
437
+
438
+ default:
439
+ // material / ref / unrecognized kinds — not implemented in v1.
440
+ return null
441
+ }
442
+ }
443
+
444
+ function CustomFieldRenderer({
445
+ Comp,
446
+ nodeId,
447
+ onUpdate,
448
+ }: {
449
+ Comp: ComponentType<{ node: AnyNode; onUpdate: (patch: Partial<AnyNode>) => void }>
450
+ nodeId: AnyNodeId
451
+ onUpdate: (patch: Partial<AnyNode>) => void
452
+ }) {
453
+ // Subscribe to the full node — the custom editor may read any
454
+ // field. Tools that don't want this churn should write narrower
455
+ // selectors inside Comp itself.
456
+ const node = useScene((s) => s.nodes[nodeId])
457
+ if (!node) return null
458
+ return <Comp node={node} onUpdate={onUpdate} />
459
+ }
460
+
461
+ // ─── helpers ─────────────────────────────────────────────────────────
462
+
463
+ function precisionForStep(step: number): number {
464
+ if (step <= 0) return 0
465
+ return Math.max(0, Math.ceil(-Math.log10(step)))
466
+ }
467
+
468
+ function prettifyKey(key: string): string {
469
+ // 'bracketStyle' → 'Bracket style'
470
+ const spaced = key.replace(/([A-Z])/g, ' $1').toLowerCase()
471
+ return spaced.charAt(0).toUpperCase() + spaced.slice(1)
472
+ }
473
+
474
+ function prettifyEnumValue(value: string): string {
475
+ // 'minimal' → 'Minimal'; 'roof-segment' → 'Roof segment'
476
+ return value
477
+ .split(/[-_\s]/)
478
+ .map((word, i) =>
479
+ i === 0 ? word.charAt(0).toUpperCase() + word.slice(1) : word.toLowerCase(),
480
+ )
481
+ .join(' ')
482
+ }
@@ -22,6 +22,7 @@ import {
22
22
  import { useCallback, useEffect, useRef, useState } from 'react'
23
23
  import { guideEmitter } from '../../../lib/guide-events'
24
24
  import { getGuideImageName } from '../../../lib/local-guide-image'
25
+ import { cn } from '../../../lib/utils'
25
26
  import useEditor from '../../../store/use-editor'
26
27
  import { ActionButton, ActionGroup } from '../controls/action-button'
27
28
  import { PanelSection } from '../controls/panel-section'
@@ -60,6 +61,9 @@ export function ReferencePanel() {
60
61
  ? (s.nodes[selectedReferenceId as AnyNode['id']] as ReferenceNode | undefined)
61
62
  : undefined,
62
63
  )
64
+ const isScaleFlowActive = useEditor(
65
+ (s) => s.referenceScaleActiveGuideId !== null && s.referenceScaleActiveGuideId === node?.id,
66
+ )
63
67
 
64
68
  const handleUpdate = useCallback(
65
69
  (updates: Partial<ReferenceNode>) => {
@@ -98,6 +102,9 @@ export function ReferencePanel() {
98
102
  } as Partial<GuideNode>,
99
103
  )
100
104
  setGuideScaleReferenceVisible(selectedReferenceId, true)
105
+ // The new image starts uncalibrated — drop the calibration auto-lock
106
+ // so it can be resized/rotated right away.
107
+ setGuideLocked(selectedReferenceId, false)
101
108
  } catch {
102
109
  setReplaceError('Could not replace that image.')
103
110
  } finally {
@@ -123,6 +130,13 @@ export function ReferencePanel() {
123
130
  return
124
131
  }
125
132
 
133
+ // The scale line is drawn on the 2D plan — starting from a 3D-only view
134
+ // would arm the flow invisibly inside the hidden floorplan panel.
135
+ const editor = useEditor.getState()
136
+ if (editor.viewMode === '3d') {
137
+ editor.setViewMode('2d')
138
+ }
139
+
126
140
  guideEmitter.emit('guide:set-reference-scale', { guideId: node.id })
127
141
  }, [node])
128
142
 
@@ -233,33 +247,54 @@ export function ReferencePanel() {
233
247
 
234
248
  <PanelSection title="Reference Scale">
235
249
  <div className="flex items-center gap-2 rounded-md border border-border/50 bg-background/40 px-2.5 py-2 text-sm">
236
- <Ruler className="h-4 w-4 shrink-0 text-primary" />
250
+ <Ruler
251
+ className={cn(
252
+ 'h-4 w-4 shrink-0',
253
+ node.scaleReference ? 'text-primary' : 'text-amber-600 dark:text-amber-400',
254
+ )}
255
+ />
237
256
  <span className="truncate text-muted-foreground">{scaleStatus}</span>
238
257
  </div>
239
258
 
240
- <ActionGroup>
241
- <ActionButton
242
- label={node.scaleReference ? 'Edit Scale' : 'Set Scale'}
243
- onClick={handleStartScale}
244
- />
245
- <ActionButton label="Cancel" onClick={handleCancelScale} />
246
- </ActionGroup>
259
+ {!node.scaleReference && (
260
+ <p className="px-0.5 text-muted-foreground text-xs leading-snug">
261
+ {isScaleFlowActive
262
+ ? 'Click both ends of a known distance on the plan, then type its real length.'
263
+ : 'Draw a line over a known dimension on the plan, then type its real length to scale the image exactly.'}
264
+ </p>
265
+ )}
247
266
 
248
267
  <ActionGroup>
249
268
  <ActionButton
250
- disabled={!node.scaleReference}
251
- label={scaleReferenceVisible ? 'Hide Scale' : 'Show Scale'}
252
- onClick={() => {
253
- if (!node.scaleReference) return
254
- setGuideScaleReferenceVisible(node.id, !scaleReferenceVisible)
255
- }}
256
- />
257
- <ActionButton
258
- disabled={!node.scaleReference}
259
- label="Clear Scale"
260
- onClick={() => handleUpdate({ scaleReference: null } as Partial<GuideNode>)}
269
+ className={cn(
270
+ !node.scaleReference &&
271
+ !isScaleFlowActive &&
272
+ 'border-primary/50 bg-primary/15 text-primary hover:bg-primary/25 active:bg-primary/25',
273
+ )}
274
+ label={
275
+ isScaleFlowActive ? 'Cancel' : node.scaleReference ? 'Edit Scale' : 'Set Scale'
276
+ }
277
+ onClick={isScaleFlowActive ? handleCancelScale : handleStartScale}
261
278
  />
262
279
  </ActionGroup>
280
+
281
+ {node.scaleReference && (
282
+ <ActionGroup>
283
+ <ActionButton
284
+ label={scaleReferenceVisible ? 'Hide Scale' : 'Show Scale'}
285
+ onClick={() => setGuideScaleReferenceVisible(node.id, !scaleReferenceVisible)}
286
+ />
287
+ <ActionButton
288
+ label="Clear Scale"
289
+ onClick={() => {
290
+ handleUpdate({ scaleReference: null } as Partial<GuideNode>)
291
+ // Calibrating auto-locked the guide; clearing the scale
292
+ // returns it to a freely-editable reference.
293
+ setGuideLocked(node.id, false)
294
+ }}
295
+ />
296
+ </ActionGroup>
297
+ )}
263
298
  </PanelSection>
264
299
 
265
300
  <PanelSection title="Quick Actions">
@@ -197,15 +197,19 @@ function DropdownMenuSubContent({
197
197
  className,
198
198
  ...props
199
199
  }: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
200
+ // Portalled so the side-opening submenu escapes the parent content's
201
+ // `overflow-x-hidden` clip (otherwise it renders but is invisible).
200
202
  return (
201
- <DropdownMenuPrimitive.SubContent
202
- className={cn(
203
- 'data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-32 origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=closed]:animate-out data-[state=open]:animate-in',
204
- className,
205
- )}
206
- data-slot="dropdown-menu-sub-content"
207
- {...props}
208
- />
203
+ <DropdownMenuPrimitive.Portal>
204
+ <DropdownMenuPrimitive.SubContent
205
+ className={cn(
206
+ 'data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-32 origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=closed]:animate-out data-[state=open]:animate-in',
207
+ className,
208
+ )}
209
+ data-slot="dropdown-menu-sub-content"
210
+ {...props}
211
+ />
212
+ </DropdownMenuPrimitive.Portal>
209
213
  )
210
214
  }
211
215