@pascal-app/editor 0.9.1 → 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 (262) hide show
  1. package/package.json +12 -9
  2. package/src/components/editor/alignment-3d-guide-layer.tsx +10 -2
  3. package/src/components/editor/bake-exporter.tsx +47 -0
  4. package/src/components/editor/custom-camera-controls.tsx +63 -25
  5. package/src/components/editor/editor-layout-mobile.tsx +5 -0
  6. package/src/components/editor/editor-layout-v2.tsx +18 -0
  7. package/src/components/editor/export-manager.tsx +71 -63
  8. package/src/components/editor/fence-tangent-lines-3d.tsx +87 -0
  9. package/src/components/editor/first-person/build-collider-world.test.ts +36 -3
  10. package/src/components/editor/first-person/build-collider-world.ts +32 -5
  11. package/src/components/editor/first-person-controls.tsx +118 -7
  12. package/src/components/editor/floating-action-menu.tsx +410 -33
  13. package/src/components/editor/floating-building-action-menu.tsx +1 -1
  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 +1804 -849
  17. package/src/components/editor/grid.tsx +166 -39
  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 +102 -64
  22. package/src/components/editor/group-selection-box-3d.tsx +173 -0
  23. package/src/components/editor/group-transform-shared.test.ts +192 -1
  24. package/src/components/editor/group-transform-shared.ts +240 -7
  25. package/src/components/editor/handles/handle-arrow.tsx +81 -23
  26. package/src/components/editor/handles/use-handle-drag.ts +37 -4
  27. package/src/components/editor/index.tsx +248 -27
  28. package/src/components/editor/measurement-pill.tsx +62 -22
  29. package/src/components/editor/node-action-menu.tsx +14 -1
  30. package/src/components/editor/node-arrow-handles.tsx +366 -173
  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 +812 -466
  36. package/src/components/editor/site-edge-labels.tsx +4 -4
  37. package/src/components/editor/slab-hole-highlights.tsx +13 -5
  38. package/src/components/editor/snapshot-capture-overlay.tsx +255 -115
  39. package/src/components/editor/three-context-bridge.ts +22 -0
  40. package/src/components/editor/thumbnail-generator.tsx +136 -43
  41. package/src/components/editor/use-floorplan-background-placement.ts +113 -45
  42. package/src/components/editor/use-floorplan-scene-data.ts +5 -5
  43. package/src/components/editor/use-mesh-settle-epoch.ts +26 -0
  44. package/src/components/editor/wall-measurement-label.tsx +3 -13
  45. package/src/components/editor/wall-move-side-handles.tsx +36 -21
  46. package/src/components/editor/wall-snap-beacon-layer.tsx +160 -6
  47. package/src/components/editor-2d/floorplan-action-menu-layer.tsx +9 -5
  48. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +1 -1
  49. package/src/components/editor-2d/floorplan-group-action-menu.tsx +87 -0
  50. package/src/components/editor-2d/floorplan-group-move.tsx +701 -0
  51. package/src/components/editor-2d/floorplan-measurement-tool-layer.test.ts +113 -0
  52. package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +1656 -0
  53. package/src/components/editor-2d/floorplan-quick-measure-layer.tsx +212 -0
  54. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +169 -4
  55. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +300 -59
  56. package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +5 -3
  57. package/src/components/editor-2d/renderers/floorplan-label-angle.test.ts +15 -0
  58. package/src/components/editor-2d/renderers/floorplan-label-angle.ts +14 -0
  59. package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +4 -1
  60. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +308 -0
  61. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +1704 -342
  62. package/src/components/editor-2d/renderers/floorplan-voronoi-layer.tsx +128 -0
  63. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +419 -60
  64. package/src/components/systems/ceiling/ceiling-system.tsx +97 -4
  65. package/src/components/systems/roof/roof-edit-system.tsx +1768 -13
  66. package/src/components/systems/selection-affordance-manager.tsx +38 -0
  67. package/src/components/systems/zone/zone-label-editor-system.tsx +14 -1
  68. package/src/components/systems/zone/zone-system.tsx +42 -13
  69. package/src/components/tools/elevator/elevator-tool.tsx +34 -8
  70. package/src/components/tools/elevator/move-elevator-tool.tsx +13 -2
  71. package/src/components/tools/fence/fence-drafting.ts +80 -8
  72. package/src/components/tools/item/move-tool.tsx +17 -13
  73. package/src/components/tools/item/placement-math.test.ts +13 -1
  74. package/src/components/tools/item/placement-math.ts +28 -2
  75. package/src/components/tools/item/placement-strategies.ts +246 -5
  76. package/src/components/tools/item/placement-types.ts +13 -1
  77. package/src/components/tools/item/use-draft-node.ts +31 -1
  78. package/src/components/tools/item/use-placement-coordinator.tsx +761 -121
  79. package/src/components/tools/registry/move-registry-node-tool.tsx +561 -99
  80. package/src/components/tools/roof/roof-tool.tsx +320 -94
  81. package/src/components/tools/select/box-select-state.ts +18 -7
  82. package/src/components/tools/select/box-select-tool.tsx +176 -40
  83. package/src/components/tools/select/marquee-geometry.test.ts +161 -0
  84. package/src/components/tools/select/marquee-geometry.ts +155 -0
  85. package/src/components/tools/select/plane-box-select-tool.tsx +1 -8
  86. package/src/components/tools/select/select-candidates.ts +1 -1
  87. package/src/components/tools/shared/cursor-sphere.tsx +62 -26
  88. package/src/components/tools/shared/drag-bounding-box.tsx +28 -4
  89. package/src/components/tools/shared/facing-indicator.tsx +87 -0
  90. package/src/components/tools/shared/facing-pose-indicator.tsx +54 -0
  91. package/src/components/tools/shared/placement-box.tsx +183 -1
  92. package/src/components/tools/shared/polygon-editor.tsx +276 -35
  93. package/src/components/tools/site/site-boundary-editor.tsx +88 -36
  94. package/src/components/tools/stair/stair-defaults.ts +1 -0
  95. package/src/components/tools/stair/stair-tool.tsx +82 -11
  96. package/src/components/tools/tool-manager.tsx +86 -25
  97. package/src/components/tools/wall/wall-drafting.test.ts +330 -0
  98. package/src/components/tools/wall/wall-drafting.ts +180 -103
  99. package/src/components/tools/wall/wall-snap-geometry.test.ts +44 -0
  100. package/src/components/tools/wall/wall-snap-geometry.ts +67 -6
  101. package/src/components/tools/zone/zone-boundary-editor.tsx +5 -1
  102. package/src/components/tools/zone/zone-tool.tsx +82 -88
  103. package/src/components/ui/action-menu/camera-actions.tsx +24 -17
  104. package/src/components/ui/action-menu/control-modes.tsx +40 -93
  105. package/src/components/ui/action-menu/furnish-tools.tsx +5 -5
  106. package/src/components/ui/action-menu/index.tsx +2 -4
  107. package/src/components/ui/action-menu/measurement-control.tsx +188 -0
  108. package/src/components/ui/action-menu/structure-tools.tsx +22 -13
  109. package/src/components/ui/action-menu/view-toggles.tsx +42 -90
  110. package/src/components/ui/command-palette/editor-commands.tsx +2 -2
  111. package/src/components/ui/command-palette/index.tsx +4 -3
  112. package/src/components/ui/controls/material-paint-panel.tsx +86 -17
  113. package/src/components/ui/controls/material-picker.tsx +83 -152
  114. package/src/components/ui/controls/material-properties-editor.tsx +108 -0
  115. package/src/components/ui/controls/metric-control.tsx +118 -44
  116. package/src/components/ui/controls/scene-material-list.tsx +247 -0
  117. package/src/components/ui/controls/slider-control.tsx +88 -38
  118. package/src/components/ui/floating-level-selector.tsx +1 -1
  119. package/src/components/ui/helpers/building-helper.tsx +10 -23
  120. package/src/components/ui/helpers/contextual-helper-panel.tsx +428 -0
  121. package/src/components/ui/helpers/helper-manager.tsx +227 -14
  122. package/src/components/ui/helpers/item-helper.tsx +28 -32
  123. package/src/components/ui/helpers/registered-tool-helper.tsx +45 -11
  124. package/src/components/ui/helpers/roof-helper.tsx +10 -12
  125. package/src/components/ui/icon-ref.tsx +47 -0
  126. package/src/components/ui/item-catalog/catalog-items.tsx +39 -1
  127. package/src/components/ui/item-catalog/item-catalog.tsx +13 -36
  128. package/src/components/ui/level-duplicate-dialog.tsx +1 -1
  129. package/src/components/ui/panels/node-display.ts +17 -17
  130. package/src/components/ui/panels/panel-manager.tsx +22 -13
  131. package/src/components/ui/panels/panel-wrapper.tsx +24 -3
  132. package/src/components/ui/panels/parametric-inspector.tsx +15 -2
  133. package/src/components/ui/panels/reference-panel.tsx +54 -19
  134. package/src/components/ui/primitives/shortcut-token.tsx +39 -3
  135. package/src/components/ui/primitives/sidebar.tsx +1 -0
  136. package/src/components/ui/sidebar/app-sidebar.tsx +5 -1
  137. package/src/components/ui/sidebar/icon-rail.tsx +50 -31
  138. package/src/components/ui/sidebar/panels/plugins-panel.tsx +218 -0
  139. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +71 -40
  140. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +60 -6
  141. package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +12 -4
  142. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -3
  143. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +10 -7
  144. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -1
  145. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +7 -2
  146. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +10 -7
  147. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -1
  148. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +1 -1
  149. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +10 -7
  150. package/src/components/ui/sidebar/panels/site-panel/index.tsx +40 -17
  151. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +31 -17
  152. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +1 -1
  153. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +72 -25
  154. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +7 -3
  155. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +12 -8
  156. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +4 -3
  157. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +10 -7
  158. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -1
  159. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +2 -2
  160. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +44 -3
  161. package/src/components/ui/sidebar/panels/site-panel/tree-structure.ts +36 -0
  162. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -1
  163. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +7 -2
  164. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +3 -3
  165. package/src/components/ui/sidebar/tab-bar.tsx +42 -28
  166. package/src/components/ui/sidebar/use-plugin-panels.tsx +132 -0
  167. package/src/components/ui/snap-target-badge.test.tsx +40 -0
  168. package/src/components/ui/snap-target-badge.tsx +88 -0
  169. package/src/components/viewer-overlay.tsx +85 -10
  170. package/src/components/viewer-zone-system.tsx +6 -1
  171. package/src/hooks/use-auto-save.test.ts +14 -0
  172. package/src/hooks/use-auto-save.ts +54 -7
  173. package/src/hooks/use-ceiling-events.ts +3 -2
  174. package/src/hooks/use-drag-action.ts +4 -1
  175. package/src/hooks/use-keyboard.ts +385 -40
  176. package/src/index.tsx +219 -6
  177. package/src/lib/active-placement-surface.ts +35 -0
  178. package/src/lib/ceiling-plan-snap.ts +24 -0
  179. package/src/lib/contextual-help.test.ts +112 -0
  180. package/src/lib/contextual-help.ts +144 -0
  181. package/src/lib/continuation.ts +64 -0
  182. package/src/lib/direct-manipulation.test.ts +192 -0
  183. package/src/lib/direct-manipulation.ts +109 -0
  184. package/src/lib/editor-api.ts +23 -35
  185. package/src/lib/floorplan/apply-alignment.test.ts +25 -0
  186. package/src/lib/floorplan/apply-alignment.ts +18 -12
  187. package/src/lib/floorplan/floorplan-export.tsx +364 -0
  188. package/src/lib/floorplan/geometry.ts +53 -0
  189. package/src/lib/floorplan/index.ts +3 -0
  190. package/src/lib/floorplan/items.ts +5 -2
  191. package/src/lib/floorplan/plan-coords.ts +21 -0
  192. package/src/lib/fresh-planar-placement.test.ts +240 -3
  193. package/src/lib/fresh-planar-placement.ts +68 -1
  194. package/src/lib/glb-export.test.ts +441 -0
  195. package/src/lib/glb-export.ts +874 -0
  196. package/src/lib/history.ts +9 -0
  197. package/src/lib/interaction/hot-set.test.ts +133 -0
  198. package/src/lib/interaction/hot-set.ts +67 -0
  199. package/src/lib/interaction/overlay-policy.test.ts +51 -0
  200. package/src/lib/interaction/overlay-policy.ts +59 -0
  201. package/src/lib/interaction/scope.ts +211 -0
  202. package/src/lib/level-selection.ts +2 -2
  203. package/src/lib/material-paint.ts +36 -49
  204. package/src/lib/measurement-kind.test.ts +30 -0
  205. package/src/lib/measurement-kind.ts +19 -0
  206. package/src/lib/measurement-label.test.ts +47 -0
  207. package/src/lib/measurement-label.ts +68 -0
  208. package/src/lib/measurement-parser.ts +116 -0
  209. package/src/lib/measurements.test.ts +183 -0
  210. package/src/lib/measurements.ts +199 -0
  211. package/src/lib/paint-scope.test.ts +454 -0
  212. package/src/lib/paint-scope.ts +461 -0
  213. package/src/lib/placement-drag-release.ts +23 -0
  214. package/src/lib/planar-cursor-placement.test.ts +57 -0
  215. package/src/lib/plugin-panels.test.ts +19 -0
  216. package/src/lib/plugin-panels.ts +91 -0
  217. package/src/lib/quick-measurement.test.ts +77 -0
  218. package/src/lib/quick-measurement.ts +109 -0
  219. package/src/lib/roof-duplication.ts +6 -6
  220. package/src/lib/roof-hover-outline-proxy.ts +22 -0
  221. package/src/lib/roof-wall-hit.ts +164 -0
  222. package/src/lib/scene-clipboard.test.ts +196 -0
  223. package/src/lib/scene-clipboard.ts +73 -8
  224. package/src/lib/scene.ts +28 -5
  225. package/src/lib/selection-routing.test.ts +298 -0
  226. package/src/lib/selection-routing.ts +175 -0
  227. package/src/lib/sfx/index.ts +1 -0
  228. package/src/lib/sfx/movement-tick.test.ts +65 -0
  229. package/src/lib/sfx/movement-tick.ts +18 -0
  230. package/src/lib/sfx-bus.ts +50 -13
  231. package/src/lib/sfx-player.test.ts +124 -0
  232. package/src/lib/sfx-player.ts +111 -66
  233. package/src/lib/slab-plan-snap.test.ts +93 -0
  234. package/src/lib/slab-plan-snap.ts +108 -0
  235. package/src/lib/snapping-mode.test.ts +138 -0
  236. package/src/lib/snapping-mode.ts +179 -0
  237. package/src/lib/stair-duplication.ts +4 -4
  238. package/src/lib/surface-plan-snap.test.ts +71 -0
  239. package/src/lib/surface-plan-snap.ts +256 -0
  240. package/src/lib/use-linear-display.ts +40 -0
  241. package/src/lib/world-grid-snap.ts +37 -5
  242. package/src/store/use-direct-manipulation-feedback.ts +20 -0
  243. package/src/store/use-editor.tsx +484 -114
  244. package/src/store/use-facing-pose.ts +44 -0
  245. package/src/store/use-fence-curve-draft.ts +21 -0
  246. package/src/store/use-floorplan-draft-preview.ts +101 -0
  247. package/src/store/use-floorplan-marquee.ts +50 -0
  248. package/src/store/use-interaction-scope.test.ts +186 -0
  249. package/src/store/use-interaction-scope.ts +132 -0
  250. package/src/store/use-measurement-draft.test.ts +530 -0
  251. package/src/store/use-measurement-draft.ts +543 -0
  252. package/src/store/use-opening-guides.ts +41 -0
  253. package/src/store/use-placement-preview.ts +11 -3
  254. package/src/store/use-quick-measurement-hud.test.ts +53 -0
  255. package/src/store/use-quick-measurement-hud.ts +77 -0
  256. package/src/store/use-segment-draft-chain.ts +28 -0
  257. package/src/store/use-stair-build-preview.ts +43 -0
  258. package/src/store/use-wall-snap-indicator.ts +2 -0
  259. package/src/components/editor/first-person/bvh-ecctrl.tsx +0 -860
  260. package/src/components/editor/group-move-handle.tsx +0 -316
  261. package/src/components/ui/panels/paint-panel.tsx +0 -163
  262. package/src/lib/level-name.ts +0 -11
@@ -6,26 +6,26 @@ export type NodeDisplay = {
6
6
  }
7
7
 
8
8
  const TYPE_DEFAULTS: Record<string, NodeDisplay> = {
9
- item: { icon: '/icons/furniture.png', label: 'Item' },
10
- wall: { icon: '/icons/wall.png', label: 'Wall' },
11
- door: { icon: '/icons/door.png', label: 'Door' },
12
- window: { icon: '/icons/window.png', label: 'Window' },
13
- slab: { icon: '/icons/floor.png', label: 'Slab' },
14
- ceiling: { icon: '/icons/ceiling.png', label: 'Ceiling' },
15
- column: { icon: '/icons/column.png', label: 'Column' },
16
- elevator: { icon: '/icons/elevator.png', label: 'Elevator' },
17
- fence: { icon: '/icons/fence.png', label: 'Fence' },
18
- roof: { icon: '/icons/roof.png', label: 'Roof' },
19
- 'roof-segment': { icon: '/icons/roof.png', label: 'Roof segment' },
20
- stair: { icon: '/icons/stair.png', label: 'Stair' },
21
- 'stair-segment': { icon: '/icons/stair.png', label: 'Stair segment' },
22
- scan: { icon: '/icons/mesh.png', label: '3D Scan' },
23
- guide: { icon: '/icons/floorplan.png', label: 'Guide image' },
9
+ item: { icon: '/icons/item.webp', label: 'Item' },
10
+ wall: { icon: '/icons/wall.webp', label: 'Wall' },
11
+ door: { icon: '/icons/door.webp', label: 'Door' },
12
+ window: { icon: '/icons/window.webp', label: 'Window' },
13
+ slab: { icon: '/icons/floor.webp', label: 'Slab' },
14
+ ceiling: { icon: '/icons/ceiling.webp', label: 'Ceiling' },
15
+ column: { icon: '/icons/column.webp', label: 'Column' },
16
+ elevator: { icon: '/icons/elevator.webp', label: 'Elevator' },
17
+ fence: { icon: '/icons/fence.webp', label: 'Fence' },
18
+ roof: { icon: '/icons/roof.webp', label: 'Roof' },
19
+ 'roof-segment': { icon: '/icons/roof.webp', label: 'Roof segment' },
20
+ stair: { icon: '/icons/stairs.webp', label: 'Stair' },
21
+ 'stair-segment': { icon: '/icons/stairs.webp', label: 'Stair segment' },
22
+ scan: { icon: '/icons/mesh.webp', label: '3D Scan' },
23
+ guide: { icon: '/icons/floorplan.webp', label: 'Guide image' },
24
24
  }
25
25
 
26
26
  export function getNodeDisplay(node: AnyNode | null | undefined): NodeDisplay {
27
- if (!node) return { icon: '/icons/select.png', label: 'Selection' }
28
- const fallback = TYPE_DEFAULTS[node.type] ?? { icon: '/icons/select.png', label: node.type }
27
+ if (!node) return { icon: '/icons/select.webp', label: 'Selection' }
28
+ const fallback = TYPE_DEFAULTS[node.type] ?? { icon: '/icons/select.webp', label: node.type }
29
29
  // Item nodes carry an asset with its own thumbnail/name
30
30
  if (node.type === 'item') {
31
31
  return {
@@ -29,7 +29,7 @@ import useEditor from '../../../store/use-editor'
29
29
  import { MobilePanelSheet } from './mobile-panel-sheet'
30
30
  import { MobileSelectionBar } from './mobile-selection-bar'
31
31
  import { getNodeDisplay } from './node-display'
32
- import { PaintPanel } from './paint-panel'
32
+ import { resetDesktopInspectorCollapsed } from './panel-wrapper'
33
33
  import { ParametricInspector } from './parametric-inspector'
34
34
  import { ReferencePanel } from './reference-panel'
35
35
 
@@ -174,9 +174,6 @@ export function PanelManager({ inspectorFooter }: { inspectorFooter?: React.Reac
174
174
  const selectedZoneId = useViewer((s) => s.selection.zoneId)
175
175
  const setSelection = useViewer((s) => s.setSelection)
176
176
  const selectedReferenceId = useEditor((s) => s.selectedReferenceId)
177
- const isPaintPanelOpen = useEditor((s) => s.isPaintPanelOpen)
178
- const mode = useEditor((s) => s.mode)
179
- const activePaintMaterial = useEditor((s) => s.activePaintMaterial)
180
177
  // Only subscribe to the *type* of the single-selected node — string primitive
181
178
  // so we don't re-render on unrelated scene mutations.
182
179
  const selectedNodeType = useScene((s) => {
@@ -190,6 +187,27 @@ export function PanelManager({ inspectorFooter }: { inspectorFooter?: React.Reac
190
187
  return id ? (s.nodes[id as AnyNodeId] ?? null) : null
191
188
  })
192
189
 
190
+ // Node and reference selection are mutually exclusive: selecting a guide
191
+ // clears the node selection (handleGuideSelect), but node selection never
192
+ // cleared a lingering reference — so clicking a wall with a floorplan
193
+ // selected kept showing the reference panel. Clear the stale reference the
194
+ // moment a scene selection appears.
195
+ const setSelectedReferenceId = useEditor((s) => s.setSelectedReferenceId)
196
+ useEffect(() => {
197
+ if (selectedIds.length > 0 || selectedZoneId) {
198
+ setSelectedReferenceId(null)
199
+ }
200
+ }, [selectedIds, selectedZoneId, setSelectedReferenceId])
201
+
202
+ // The inspector's expanded state is shared across panel swaps, but a fresh
203
+ // selection after everything was deselected should open collapsed again.
204
+ const hasAnySelection = selectedIds.length > 0 || Boolean(selectedZoneId) || Boolean(selectedReferenceId)
205
+ useEffect(() => {
206
+ if (!hasAnySelection) {
207
+ resetDesktopInspectorCollapsed()
208
+ }
209
+ }, [hasAnySelection])
210
+
193
211
  if (isMobile) {
194
212
  if (selectedReferenceId) {
195
213
  return <MobilePanelLayer isReference={true} node={null} panel={<ReferencePanel />} />
@@ -208,15 +226,6 @@ export function PanelManager({ inspectorFooter }: { inspectorFooter?: React.Reac
208
226
  return <ReferencePanel />
209
227
  }
210
228
 
211
- if (
212
- isPaintPanelOpen &&
213
- mode === 'material-paint' &&
214
- activePaintMaterial?.material?.properties &&
215
- !activePaintMaterial.materialPreset
216
- ) {
217
- return <PaintPanel />
218
- }
219
-
220
229
  if (selectedZoneId && selectedIds.length === 0) {
221
230
  return (
222
231
  <ParametricInspector
@@ -17,6 +17,14 @@ const DRAG_MARGIN = 8
17
17
  // Pointer travel (px) below which a header press is treated as a click
18
18
  // (toggles collapse) rather than a drag.
19
19
  const CLICK_SLOP = 4
20
+ let desktopInspectorCollapsed = true
21
+
22
+ /** Forget the shared expanded state. Called when the last selection clears so
23
+ * a fresh selection opens the inspector collapsed — the sharing is only meant
24
+ * to survive swaps between panels (roof ↔ segment), not a close/reopen. */
25
+ export function resetDesktopInspectorCollapsed() {
26
+ desktopInspectorCollapsed = true
27
+ }
20
28
 
21
29
  function clamp(value: number, min: number, max: number): number {
22
30
  return Math.min(Math.max(value, min), Math.max(min, max))
@@ -52,7 +60,7 @@ export const InspectorFooterContext = createContext<React.ReactNode>(null)
52
60
 
53
61
  interface PanelWrapperProps {
54
62
  title: string
55
- /** Either a URL path (legacy panels pass `/icons/floor.png` etc.,
63
+ /** Either a URL path (legacy panels pass `/icons/floor.webp` etc.,
56
64
  * rendered via next/image) OR a React node (registry-driven
57
65
  * inspector renders `<Icon icon="lucide:fence" />` from
58
66
  * `def.presentation.icon`). */
@@ -85,8 +93,21 @@ export function PanelWrapper({
85
93
  const panelRef = useRef<HTMLDivElement>(null)
86
94
 
87
95
  // The whole panel is collapsed to just its header by default; the chevron
88
- // expands it to reveal the inspector body.
89
- const [collapsed, setCollapsed] = useState(true)
96
+ // expands it to reveal the inspector body. Keep the desktop value shared
97
+ // across inspector swaps (roof ↔ segment, etc.) so navigating between
98
+ // related panels preserves whether the user left the inspector open.
99
+ const [collapsed, setCollapsedState] = useState(desktopInspectorCollapsed)
100
+
101
+ const setCollapsed = useCallback(
102
+ (next: boolean | ((previous: boolean) => boolean)) => {
103
+ setCollapsedState((previous) => {
104
+ const resolved = typeof next === 'function' ? next(previous) : next
105
+ desktopInspectorCollapsed = resolved
106
+ return resolved
107
+ })
108
+ },
109
+ [],
110
+ )
90
111
 
91
112
  // Drag-to-reposition from the header. `offset` is a translation applied on
92
113
  // top of the default `top-20 right-4` anchor; null until first dragged.
@@ -62,9 +62,22 @@ export function ParametricInspector({
62
62
  const handleUpdate = useCallback(
63
63
  (patch: Partial<AnyNode>) => {
64
64
  if (!selectedId) return
65
- useScene.getState().updateNode(selectedId, patch)
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)
66
79
  },
67
- [selectedId],
80
+ [selectedId, parametrics],
68
81
  )
69
82
 
70
83
  const clearSelection = useCallback(() => {
@@ -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">
@@ -22,6 +22,17 @@ const MOUSE_SHORTCUTS = {
22
22
  },
23
23
  } as const
24
24
 
25
+ // The platform-agnostic command modifier. Both Cmd and Ctrl bind the action; we
26
+ // render the symbol for the *current* device so the hint reads native (⌘ on Mac,
27
+ // Ctrl elsewhere) without implying only one of them works.
28
+ const COMMAND_VALUES = new Set(['Cmd/Ctrl', 'Cmd', 'Command', 'Meta'])
29
+
30
+ // Resolved once on the client at module load — the editor HUD is client-only, so
31
+ // there's no server render to mismatch against. `navigator.platform` is enough
32
+ // here and matches the detection used elsewhere (floorplan rotate hint).
33
+ const IS_MAC =
34
+ typeof navigator !== 'undefined' && navigator.platform.toUpperCase().includes('MAC')
35
+
25
36
  type ShortcutTokenProps = React.ComponentProps<'kbd'> & {
26
37
  value: string
27
38
  displayValue?: string
@@ -30,16 +41,23 @@ type ShortcutTokenProps = React.ComponentProps<'kbd'> & {
30
41
  function ShortcutToken({ className, displayValue, value, ...props }: ShortcutTokenProps) {
31
42
  const mouseShortcut =
32
43
  value in MOUSE_SHORTCUTS ? MOUSE_SHORTCUTS[value as keyof typeof MOUSE_SHORTCUTS] : null
44
+ const isCommand = COMMAND_VALUES.has(value)
45
+ const isShift = value === 'Shift'
46
+ const commandDisplay = IS_MAC ? '⌘' : 'Ctrl'
47
+ const commandLabel = IS_MAC ? 'Command' : 'Control'
33
48
 
34
49
  return (
35
50
  <kbd
36
- aria-label={mouseShortcut?.label ?? displayValue ?? value}
51
+ aria-label={
52
+ mouseShortcut?.label ??
53
+ (isCommand ? commandLabel : isShift ? 'Shift' : (displayValue ?? value))
54
+ }
37
55
  className={cn(
38
56
  'inline-flex h-6 items-center rounded border border-border bg-muted px-2 font-medium font-mono text-[11px] text-muted-foreground',
39
- mouseShortcut && 'justify-center px-1.5',
57
+ (mouseShortcut || isShift) && 'justify-center px-1.5',
40
58
  className,
41
59
  )}
42
- title={mouseShortcut?.label ?? value}
60
+ title={mouseShortcut?.label ?? (isCommand ? commandLabel : isShift ? 'Shift' : value)}
43
61
  {...props}
44
62
  >
45
63
  {mouseShortcut ? (
@@ -54,6 +72,24 @@ function ShortcutToken({ className, displayValue, value, ...props }: ShortcutTok
54
72
  />
55
73
  <span className="sr-only">{mouseShortcut.label}</span>
56
74
  </>
75
+ ) : isShift ? (
76
+ // Icon rather than the ⇧ text glyph — the font renders the glyph's
77
+ // crossbar too high to read as the shift key symbol.
78
+ <>
79
+ <Icon
80
+ aria-hidden="true"
81
+ className="shrink-0"
82
+ color="currentColor"
83
+ height={13}
84
+ icon="ph:arrow-fat-up"
85
+ width={13}
86
+ />
87
+ <span className="sr-only">Shift</span>
88
+ </>
89
+ ) : isCommand ? (
90
+ // The ⌘ glyph reads small next to letters at the same font size, so bump
91
+ // it up a touch on Mac. "Ctrl" stays at the token's normal size.
92
+ <span className={IS_MAC ? 'text-[13px] leading-none' : undefined}>{commandDisplay}</span>
57
93
  ) : (
58
94
  (displayValue ?? value)
59
95
  )}
@@ -63,6 +63,7 @@ export const useSidebarStore = create<SidebarStore>()(
63
63
  {
64
64
  name: 'sidebar-preferences',
65
65
  partialize: (state) => ({ width: state.width, isCollapsed: state.isCollapsed }),
66
+ skipHydration: true,
66
67
  },
67
68
  ),
68
69
  )
@@ -16,6 +16,7 @@ import useEditor from './../../../store/use-editor'
16
16
  import { type ExtraPanel, IconRail } from './icon-rail'
17
17
  import { SettingsPanel, type SettingsPanelProps } from './panels/settings-panel'
18
18
  import { SitePanel, type SitePanelProps } from './panels/site-panel'
19
+ import { useHostPanels } from './use-plugin-panels'
19
20
 
20
21
  interface AppSidebarProps {
21
22
  appMenuButton?: ReactNode
@@ -31,9 +32,12 @@ export function AppSidebar({
31
32
  sidebarTop,
32
33
  settingsPanelProps,
33
34
  sitePanelProps,
34
- extraPanels,
35
+ extraPanels: hostExtraPanels,
35
36
  commandPaletteEmptyAction,
36
37
  }: AppSidebarProps) {
38
+ // Host-provided panels merged with registered host panels — the icon rail and
39
+ // content area treat both identically.
40
+ const extraPanels = useHostPanels(hostExtraPanels)
37
41
  const activePanel = useEditor((s) => s.activeSidebarPanel)
38
42
  const setActivePanel = useEditor((s) => s.setActiveSidebarPanel)
39
43
  const hasActivePanel =
@@ -1,5 +1,6 @@
1
1
  'use client'
2
2
 
3
+ import { Plus } from 'lucide-react'
3
4
  import type { ComponentType, ReactNode } from 'react'
4
5
  import {
5
6
  Tooltip,
@@ -10,7 +11,13 @@ import { cn } from './../../../lib/utils'
10
11
 
11
12
  export type PanelId = string
12
13
 
13
- export type ExtraPanel = { id: string; icon: ReactNode; label: string; component: ComponentType }
14
+ export type ExtraPanel = {
15
+ id: string
16
+ icon: ReactNode
17
+ label: string
18
+ component: ComponentType
19
+ pluginId?: string
20
+ }
14
21
 
15
22
  interface IconRailProps {
16
23
  activePanel: PanelId
@@ -22,13 +29,13 @@ interface IconRailProps {
22
29
 
23
30
  const sitePanel: { id: PanelId; iconSrc: string; label: string } = {
24
31
  id: 'site',
25
- iconSrc: '/icons/level.png',
32
+ iconSrc: '/icons/level.webp',
26
33
  label: 'Site',
27
34
  }
28
35
 
29
36
  const settingsPanel: { id: PanelId; iconSrc: string; label: string } = {
30
37
  id: 'settings',
31
- iconSrc: '/icons/settings.png',
38
+ iconSrc: '/icons/settings.webp',
32
39
  label: 'Settings',
33
40
  }
34
41
 
@@ -41,6 +48,38 @@ export function IconRail({
41
48
  extraPanels,
42
49
  className,
43
50
  }: IconRailProps) {
51
+ const regularExtraPanels = extraPanels?.filter((panel) => !panel.pluginId && panel.id !== 'plugins')
52
+ const pluginPanels = extraPanels?.filter((panel) => panel.pluginId)
53
+ const pluginsPanel = extraPanels?.find((panel) => panel.id === 'plugins')
54
+
55
+ const renderExtraPanel = (panel: ExtraPanel) => {
56
+ const isActive = activePanel === panel.id
57
+ return (
58
+ <Tooltip key={panel.id}>
59
+ <TooltipTrigger asChild>
60
+ <button
61
+ className={cn(
62
+ 'flex h-9 w-9 items-center justify-center rounded-lg transition-all',
63
+ isActive ? 'bg-accent' : 'hover:bg-accent',
64
+ )}
65
+ onClick={() => onPanelChange(panel.id)}
66
+ type="button"
67
+ >
68
+ <span
69
+ className={cn(
70
+ 'flex h-6 w-6 items-center justify-center transition-all',
71
+ !isActive && 'opacity-50',
72
+ )}
73
+ >
74
+ {panel.id === 'plugins' ? <Plus className="h-5 w-5" /> : panel.icon}
75
+ </span>
76
+ </button>
77
+ </TooltipTrigger>
78
+ <TooltipContent side="right">{panel.label}</TooltipContent>
79
+ </Tooltip>
80
+ )
81
+ }
82
+
44
83
  return (
45
84
  <div
46
85
  className={cn(
@@ -83,34 +122,7 @@ export function IconRail({
83
122
  )
84
123
  })}
85
124
 
86
- {/* Extra panels (injected between site and settings) */}
87
- {extraPanels?.map((panel) => {
88
- const isActive = activePanel === panel.id
89
- return (
90
- <Tooltip key={panel.id}>
91
- <TooltipTrigger asChild>
92
- <button
93
- className={cn(
94
- 'flex h-9 w-9 items-center justify-center rounded-lg transition-all',
95
- isActive ? 'bg-accent' : 'hover:bg-accent',
96
- )}
97
- onClick={() => onPanelChange(panel.id)}
98
- type="button"
99
- >
100
- <span
101
- className={cn(
102
- 'flex h-6 w-6 items-center justify-center transition-all',
103
- !isActive && 'opacity-50',
104
- )}
105
- >
106
- {panel.icon}
107
- </span>
108
- </button>
109
- </TooltipTrigger>
110
- <TooltipContent side="right">{panel.label}</TooltipContent>
111
- </Tooltip>
112
- )
113
- })}
125
+ {regularExtraPanels?.map(renderExtraPanel)}
114
126
 
115
127
  {/* Settings panel */}
116
128
  {[settingsPanel].map((panel) => {
@@ -140,6 +152,13 @@ export function IconRail({
140
152
  </Tooltip>
141
153
  )
142
154
  })}
155
+
156
+ {(pluginPanels?.length || pluginsPanel) && (
157
+ <div className="mt-1 flex w-9 flex-col items-center gap-1 border-border/70 border-t pt-2">
158
+ {pluginPanels?.map(renderExtraPanel)}
159
+ {pluginsPanel && renderExtraPanel(pluginsPanel)}
160
+ </div>
161
+ )}
143
162
  </div>
144
163
  )
145
164
  }