@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
@@ -0,0 +1,188 @@
1
+ 'use client'
2
+
3
+ import { useViewer } from '@pascal-app/viewer'
4
+ import {
5
+ Box,
6
+ Check,
7
+ ChevronDown,
8
+ Eye,
9
+ EyeOff,
10
+ Ruler,
11
+ ScanSearch,
12
+ Square,
13
+ Triangle,
14
+ Waypoints,
15
+ } from 'lucide-react'
16
+ import { useState } from 'react'
17
+ import type { CreatableMeasurementKind } from '../../../lib/measurement-kind'
18
+ import { cn } from '../../../lib/utils'
19
+ import useEditor from '../../../store/use-editor'
20
+ import { Popover, PopoverContent, PopoverTrigger } from '../primitives/popover'
21
+ import { ActionButton } from './action-button'
22
+
23
+ const measurementOptions = [
24
+ { kind: 'distance', label: 'Distance', icon: Ruler },
25
+ { kind: 'angle', label: 'Angle', icon: Triangle },
26
+ { kind: 'area', label: 'Area', icon: Square },
27
+ { kind: 'perimeter', label: 'Perimeter', icon: Waypoints },
28
+ { kind: 'volume', label: 'Volume', icon: Box },
29
+ ] as const satisfies readonly {
30
+ kind: CreatableMeasurementKind
31
+ label: string
32
+ icon: typeof Ruler
33
+ }[]
34
+
35
+ const measurementMenuOptions = [
36
+ { kind: 'smart', label: 'Smart', icon: ScanSearch },
37
+ ...measurementOptions,
38
+ ] as const
39
+
40
+ export function MeasurementControl() {
41
+ const [isOpen, setIsOpen] = useState(false)
42
+ const mode = useEditor((state) => state.mode)
43
+ const tool = useEditor((state) => state.tool)
44
+ const selectedKind = useEditor((state) => state.lastMeasurementKind)
45
+ const activeToolKind = useEditor((state) => state.toolDefaults.measurement?.kind)
46
+ const setMode = useEditor((state) => state.setMode)
47
+ const setPhase = useEditor((state) => state.setPhase)
48
+ const setLastMeasurementKind = useEditor((state) => state.setLastMeasurementKind)
49
+ const setStructureLayer = useEditor((state) => state.setStructureLayer)
50
+ const setTool = useEditor((state) => state.setTool)
51
+ const setToolDefaults = useEditor((state) => state.setToolDefaults)
52
+ const showMeasurements = useViewer((state) => state.showMeasurements)
53
+ const setShowMeasurements = useViewer((state) => state.setShowMeasurements)
54
+
55
+ const selectedOption =
56
+ measurementOptions.find((option) => option.kind === selectedKind) ?? measurementOptions[0]
57
+ const isActive = mode === 'build' && tool === 'measurement'
58
+ const isSmartActive = isActive && activeToolKind === 'smart'
59
+ const SelectedIcon = isSmartActive ? ScanSearch : selectedOption.icon
60
+ const selectedLabel = isSmartActive ? 'Smart' : selectedOption.label
61
+
62
+ const activateMeasurement = (kind: CreatableMeasurementKind) => {
63
+ setPhase('structure')
64
+ setStructureLayer('elements')
65
+ setLastMeasurementKind(kind)
66
+ setToolDefaults('measurement', { kind })
67
+ setMode('build')
68
+ setTool('measurement')
69
+ }
70
+
71
+ const handlePrimaryClick = () => {
72
+ if (isActive) {
73
+ setMode('select')
74
+ return
75
+ }
76
+ activateMeasurement(selectedKind)
77
+ }
78
+
79
+ const activateSmartMeasurement = () => {
80
+ setPhase('structure')
81
+ setStructureLayer('elements')
82
+ setToolDefaults('measurement', { kind: 'smart' })
83
+ setMode('build')
84
+ setTool('measurement')
85
+ }
86
+
87
+ return (
88
+ <Popover onOpenChange={setIsOpen} open={isOpen}>
89
+ <div className="flex items-center">
90
+ <ActionButton
91
+ aria-label={`Measure: ${selectedLabel}`}
92
+ aria-pressed={isActive}
93
+ className={cn(
94
+ 'rounded-r-none p-0 text-muted-foreground',
95
+ isActive
96
+ ? 'bg-cyan-500/20 text-cyan-400 hover:bg-cyan-500/20'
97
+ : 'hover:bg-cyan-500/15 hover:text-cyan-400',
98
+ )}
99
+ label={`Measure: ${selectedLabel}`}
100
+ onClick={handlePrimaryClick}
101
+ shortcut="M"
102
+ size="icon"
103
+ variant="ghost"
104
+ >
105
+ <SelectedIcon aria-hidden="true" className="h-5 w-5" />
106
+ </ActionButton>
107
+
108
+ <PopoverTrigger asChild>
109
+ <button
110
+ aria-expanded={isOpen}
111
+ aria-haspopup="menu"
112
+ aria-label="Measurement options"
113
+ className={cn(
114
+ 'flex h-11 w-6 items-center justify-center rounded-r-lg text-muted-foreground transition-colors',
115
+ isOpen
116
+ ? 'bg-cyan-500/15 text-cyan-400'
117
+ : 'hover:bg-cyan-500/10 hover:text-cyan-400',
118
+ )}
119
+ type="button"
120
+ >
121
+ <ChevronDown
122
+ aria-hidden="true"
123
+ className={cn('h-3 w-3 transition-transform', isOpen && 'rotate-180')}
124
+ />
125
+ </button>
126
+ </PopoverTrigger>
127
+ </div>
128
+
129
+ <PopoverContent
130
+ align="center"
131
+ className="w-56 rounded-lg border-border/45 bg-background/96 p-2 shadow-elevation-3 backdrop-blur-xl"
132
+ side="top"
133
+ sideOffset={14}
134
+ >
135
+ <div aria-label="Measurement type" className="space-y-1" role="menu">
136
+ {measurementMenuOptions.map((option) => {
137
+ const OptionIcon = option.icon
138
+ const isSmart = option.kind === 'smart'
139
+ const isSelected = isSmart
140
+ ? isSmartActive
141
+ : !isSmartActive && option.kind === selectedKind
142
+ return (
143
+ <button
144
+ aria-checked={isSelected}
145
+ className={cn(
146
+ 'flex h-9 w-full items-center gap-2 rounded-md px-2.5 text-left text-sm transition-colors',
147
+ isSelected
148
+ ? 'bg-white/10 text-foreground'
149
+ : 'text-muted-foreground hover:bg-white/8 hover:text-foreground',
150
+ )}
151
+ key={option.kind}
152
+ onClick={() => {
153
+ if (isSmart) activateSmartMeasurement()
154
+ else activateMeasurement(option.kind)
155
+ setIsOpen(false)
156
+ }}
157
+ role="menuitemradio"
158
+ type="button"
159
+ >
160
+ <OptionIcon aria-hidden="true" className="h-4 w-4" />
161
+ <span>{option.label}</span>
162
+ {isSelected ? <Check aria-hidden="true" className="ml-auto h-4 w-4" /> : null}
163
+ </button>
164
+ )
165
+ })}
166
+
167
+ <div className="my-1.5 h-px bg-border/60" />
168
+
169
+ <button
170
+ aria-checked={showMeasurements}
171
+ className="flex h-9 w-full items-center gap-2 rounded-md px-2.5 text-left text-muted-foreground text-sm transition-colors hover:bg-white/8 hover:text-foreground"
172
+ onClick={() => setShowMeasurements(!showMeasurements)}
173
+ role="menuitemcheckbox"
174
+ type="button"
175
+ >
176
+ {showMeasurements ? (
177
+ <Eye aria-hidden="true" className="h-4 w-4" />
178
+ ) : (
179
+ <EyeOff aria-hidden="true" className="h-4 w-4" />
180
+ )}
181
+ <span>Show measurements</span>
182
+ <span className="ml-auto text-xs">{showMeasurements ? 'On' : 'Off'}</span>
183
+ </button>
184
+ </div>
185
+ </PopoverContent>
186
+ </Popover>
187
+ )
188
+ }
@@ -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
  ]
@@ -1,6 +1,5 @@
1
1
  'use client'
2
2
 
3
- import { Icon } from '@iconify/react'
4
3
  import {
5
4
  type AnyNodeId,
6
5
  type BuildingNode,
@@ -10,26 +9,22 @@ import {
10
9
  useScene,
11
10
  } from '@pascal-app/core'
12
11
  import { useViewer } from '@pascal-app/viewer'
13
- import { Check, ChevronDown, Eye, EyeOff, Layers2, Plus, Trash2 } from 'lucide-react'
12
+ import { Check, ChevronDown, Eye, EyeOff, Layers2, Plus, Trash2, Waypoints } from 'lucide-react'
14
13
  import { useCallback, useRef, useState } from 'react'
15
14
  import { useShallow } from 'zustand/react/shallow'
16
- import { getLevelDisplayName } from '../../../lib/level-name'
15
+ import { getLevelDisplayName } from '@pascal-app/core'
17
16
  import { createLocalGuideImage } from '../../../lib/local-guide-image'
18
17
  import { cn } from '../../../lib/utils'
19
- import useEditor, { type GridSnapStep } from '../../../store/use-editor'
18
+ import useEditor from '../../../store/use-editor'
20
19
  import { useUploadStore } from '../../../store/use-upload'
21
20
  import { SliderControl } from '../controls/slider-control'
22
21
  import { Popover, PopoverContent, PopoverTrigger } from '../primitives/popover'
23
- import { Tooltip, TooltipContent, TooltipTrigger } from '../primitives/tooltip'
24
22
  import { ActionButton } from './action-button'
25
23
 
26
24
  const MAX_FILE_SIZE = 200 * 1024 * 1024 // 200MB
27
25
  const ACCEPTED_FILE_TYPES = '.glb,.gltf,image/jpeg,image/png,image/webp,image/gif'
28
- const GRID_SNAP_STEPS: GridSnapStep[] = [0.5, 0.25, 0.1, 0.05]
29
-
30
- function formatGridSnapStep(step: GridSnapStep) {
31
- return step.toFixed(2)
32
- }
26
+ const REFERENCES_EMPTY_TEXT =
27
+ 'Upload GLB meshes as scan references or blueprint images as guide references.'
33
28
 
34
29
  // ── Helper: get guide images for the current level ──────────────────────────
35
30
 
@@ -224,7 +219,7 @@ function GuidesControl() {
224
219
  <img
225
220
  alt="Guides"
226
221
  className="h-[28px] w-[28px] object-contain"
227
- src="/icons/floorplan.png"
222
+ src="/icons/floorplan.webp"
228
223
  />
229
224
  <span className="absolute -right-1.5 -bottom-1 min-w-[14px] rounded-full bg-white/20 px-[3px] text-center font-medium text-[9px] text-white/70 leading-[14px]">
230
225
  {guides.length}
@@ -263,7 +258,7 @@ function GuidesControl() {
263
258
  <div className="space-y-3">
264
259
  <div className="flex items-center gap-2">
265
260
  <span className="flex h-7 w-7 shrink-0 items-center justify-center rounded-lg bg-background/80">
266
- <img alt="" className="h-4 w-4 object-contain" src="/icons/floorplan.png" />
261
+ <img alt="" className="h-4 w-4 object-contain" src="/icons/floorplan.webp" />
267
262
  </span>
268
263
  <div className="min-w-0 flex-1">
269
264
  <p className="font-medium text-foreground text-sm">Guide images</p>
@@ -303,7 +298,7 @@ function GuidesControl() {
303
298
  <img
304
299
  alt=""
305
300
  className="h-3.5 w-3.5 shrink-0 object-contain opacity-70"
306
- src="/icons/floorplan.png"
301
+ src="/icons/floorplan.webp"
307
302
  />
308
303
  <p className="truncate font-medium text-foreground text-sm">
309
304
  {guide.name || `Guide image ${index + 1}`}
@@ -342,7 +337,7 @@ function GuidesControl() {
342
337
  </div>
343
338
  ) : (
344
339
  <div className="rounded-xl border border-border/45 border-dashed bg-background/60 px-3 py-4 text-muted-foreground text-sm">
345
- No guide images on this level yet.
340
+ {REFERENCES_EMPTY_TEXT}
346
341
  </div>
347
342
  )}
348
343
  </div>
@@ -351,70 +346,6 @@ function GuidesControl() {
351
346
  )
352
347
  }
353
348
 
354
- // ── Grid snap toggle ────────────────────────────────────────────────────────
355
-
356
- function GridSnapControl() {
357
- const [isOpen, setIsOpen] = useState(false)
358
- const gridSnapStep = useEditor((state) => state.gridSnapStep)
359
- const setGridSnapStep = useEditor((state) => state.setGridSnapStep)
360
-
361
- return (
362
- <Popover onOpenChange={setIsOpen} open={isOpen}>
363
- <Tooltip>
364
- <TooltipTrigger asChild>
365
- <PopoverTrigger asChild>
366
- <button
367
- aria-expanded={isOpen}
368
- aria-label={`Grid snap: ${formatGridSnapStep(gridSnapStep)}`}
369
- className={cn(
370
- 'flex h-11 w-11 flex-col items-center justify-center rounded-lg text-muted-foreground transition-all hover:bg-white/5 hover:text-foreground',
371
- isOpen && 'bg-white/10 text-foreground',
372
- )}
373
- type="button"
374
- >
375
- <Icon height={16} icon="lucide:grid-2x2" width={16} />
376
- <span className="mt-1 font-medium text-[9px] leading-none">
377
- {formatGridSnapStep(gridSnapStep)}
378
- </span>
379
- </button>
380
- </PopoverTrigger>
381
- </TooltipTrigger>
382
- <TooltipContent side="top">Grid snap: {formatGridSnapStep(gridSnapStep)}</TooltipContent>
383
- </Tooltip>
384
-
385
- <PopoverContent
386
- align="center"
387
- className="w-36 rounded-xl border-border/45 bg-background/96 p-2 shadow-elevation-3 backdrop-blur-xl"
388
- side="top"
389
- sideOffset={14}
390
- >
391
- <div className="space-y-1">
392
- {GRID_SNAP_STEPS.map((step) => {
393
- const isActive = step === gridSnapStep
394
- return (
395
- <button
396
- className={cn(
397
- 'flex w-full items-center justify-between rounded-lg px-2.5 py-2 text-left text-sm transition-colors hover:bg-white/8',
398
- isActive && 'bg-white/10 text-foreground',
399
- )}
400
- key={step}
401
- onClick={() => {
402
- setGridSnapStep(step)
403
- setIsOpen(false)
404
- }}
405
- type="button"
406
- >
407
- <span>{formatGridSnapStep(step)}</span>
408
- {isActive ? <Check className="h-3.5 w-3.5" /> : <span className="h-3.5 w-3.5" />}
409
- </button>
410
- )
411
- })}
412
- </div>
413
- </PopoverContent>
414
- </Popover>
415
- )
416
- }
417
-
418
349
  // ── Scans toggle + dropdown ─────────────────────────────────────────────────
419
350
 
420
351
  function ScansControl() {
@@ -464,7 +395,7 @@ function ScansControl() {
464
395
  variant="ghost"
465
396
  >
466
397
  <div className="relative">
467
- <img alt="Scans" className="h-[28px] w-[28px] object-contain" src="/icons/mesh.png" />
398
+ <img alt="Scans" className="h-[28px] w-[28px] object-contain" src="/icons/mesh.webp" />
468
399
  <span className="absolute -right-1.5 -bottom-1 min-w-[14px] rounded-full bg-white/20 px-[3px] text-center font-medium text-[9px] text-white/70 leading-[14px]">
469
400
  {scans.length}
470
401
  </span>
@@ -502,7 +433,7 @@ function ScansControl() {
502
433
  <div className="space-y-3">
503
434
  <div className="flex items-center gap-2">
504
435
  <span className="flex h-7 w-7 shrink-0 items-center justify-center rounded-lg bg-background/80">
505
- <img alt="" className="h-4 w-4 object-contain" src="/icons/mesh.png" />
436
+ <img alt="" className="h-4 w-4 object-contain" src="/icons/mesh.webp" />
506
437
  </span>
507
438
  <div className="min-w-0 flex-1">
508
439
  <p className="font-medium text-foreground text-sm">Scans</p>
@@ -542,7 +473,7 @@ function ScansControl() {
542
473
  <img
543
474
  alt=""
544
475
  className="h-3.5 w-3.5 shrink-0 object-contain opacity-70"
545
- src="/icons/mesh.png"
476
+ src="/icons/mesh.webp"
546
477
  />
547
478
  <p className="truncate font-medium text-foreground text-sm">
548
479
  {scan.name || `Scan ${index + 1}`}
@@ -581,7 +512,7 @@ function ScansControl() {
581
512
  </div>
582
513
  ) : (
583
514
  <div className="rounded-xl border border-border/45 border-dashed bg-background/60 px-3 py-4 text-muted-foreground text-sm">
584
- No scans on this level yet.
515
+ {REFERENCES_EMPTY_TEXT}
585
516
  </div>
586
517
  )}
587
518
  </div>
@@ -763,7 +694,7 @@ function ReferencesControl() {
763
694
  <img
764
695
  alt="References"
765
696
  className="h-[28px] w-[28px] object-contain"
766
- src="/icons/floorplan.png"
697
+ src="/icons/floorplan.webp"
767
698
  />
768
699
  <span className="absolute -right-1.5 -bottom-1 min-w-[14px] rounded-full bg-white/20 px-[3px] text-center font-medium text-[9px] text-white/70 leading-[14px]">
769
700
  {total}
@@ -805,8 +736,8 @@ function ReferencesControl() {
805
736
  </div>
806
737
  )}
807
738
  <ReferenceListSection
808
- emptyText="No scans on this level yet."
809
- iconSrc="/icons/mesh.png"
739
+ emptyText={REFERENCES_EMPTY_TEXT}
740
+ iconSrc="/icons/mesh.webp"
810
741
  nodes={scans}
811
742
  noun="scan"
812
743
  onError={setUploadError}
@@ -816,8 +747,8 @@ function ReferencesControl() {
816
747
  />
817
748
  <div className="h-px bg-border/45" />
818
749
  <ReferenceListSection
819
- emptyText="No guide images on this level yet."
820
- iconSrc="/icons/floorplan.png"
750
+ emptyText={REFERENCES_EMPTY_TEXT}
751
+ iconSrc="/icons/floorplan.webp"
821
752
  nodes={guides}
822
753
  noun="guide image"
823
754
  onError={setUploadError}
@@ -987,9 +918,30 @@ function ReferenceFloorControl() {
987
918
  )
988
919
  }
989
920
 
990
- // ── Exports ─────────────────────────────────────────────────────────────────
921
+ // ── Riser diagram control ────────────────────────────────────────────────────
922
+
923
+ function RiserControl() {
924
+ const isRiserOpen = useEditor((state) => state.isRiserOpen)
925
+ const toggleRiserOpen = useEditor((state) => state.toggleRiserOpen)
926
+
927
+ return (
928
+ <ActionButton
929
+ className={cn(
930
+ isRiserOpen
931
+ ? 'bg-white/15'
932
+ : 'opacity-60 grayscale hover:bg-white/5 hover:opacity-100 hover:grayscale-0',
933
+ )}
934
+ label="Riser diagram"
935
+ onClick={toggleRiserOpen}
936
+ size="icon"
937
+ variant="ghost"
938
+ >
939
+ <Waypoints className="h-4 w-4" />
940
+ </ActionButton>
941
+ )
942
+ }
991
943
 
992
- export { GridSnapControl }
944
+ // ── Exports ─────────────────────────────────────────────────────────────────
993
945
 
994
946
  export function SecondaryToggles() {
995
947
  return (
@@ -1002,10 +954,10 @@ export function SecondaryToggles() {
1002
954
  export function ViewToggles() {
1003
955
  return (
1004
956
  <div className="flex items-center gap-1">
1005
- <GridSnapControl />
1006
957
  <ScansControl />
1007
958
  <GuidesControl />
1008
959
  <ReferenceFloorControl />
960
+ <RiserControl />
1009
961
  </div>
1010
962
  )
1011
963
  }
@@ -245,10 +245,10 @@ export function EditorCommands() {
245
245
  label: 'Wall Mode',
246
246
  group: 'Viewer Controls',
247
247
  icon: <Layers className="h-4 w-4" />,
248
- keywords: ['wall', 'cutaway', 'up', 'down', 'view'],
248
+ keywords: ['wall', 'cutaway', 'up', 'down', 'translucent', 'view'],
249
249
  badge: () => {
250
250
  const mode = useViewer.getState().wallMode
251
- return { cutaway: 'Cutaway', up: 'Up', down: 'Down' }[mode]
251
+ return { cutaway: 'Cutaway', up: 'Up', down: 'Down', translucent: 'Translucent' }[mode]
252
252
  },
253
253
  navigate: true,
254
254
  execute: () => navigateTo('wall-mode'),
@@ -10,7 +10,7 @@ import { useEffect, useState } from 'react'
10
10
  import { create } from 'zustand'
11
11
  import { useShallow } from 'zustand/shallow'
12
12
  import { Dialog, DialogContent, DialogTitle } from './../../../components/ui/primitives/dialog'
13
- import { getLevelDisplayName } from '../../../lib/level-name'
13
+ import { getLevelDisplayName } from '@pascal-app/core'
14
14
  import { useCommandRegistry } from '../../../store/use-command-registry'
15
15
  import { usePaletteViewRegistry } from '../../../store/use-palette-view-registry'
16
16
 
@@ -244,10 +244,11 @@ export function CommandPalette({ emptyAction }: { emptyAction?: CommandPaletteEm
244
244
  setOpen(false)
245
245
  }
246
246
 
247
- const wallModeLabel: Record<'cutaway' | 'up' | 'down', string> = {
247
+ const wallModeLabel: Record<'cutaway' | 'up' | 'down' | 'translucent', string> = {
248
248
  cutaway: 'Cutaway',
249
249
  up: 'Up',
250
250
  down: 'Down',
251
+ translucent: 'Translucent',
251
252
  }
252
253
  const levelModeLabel: Record<'manual' | 'stacked' | 'exploded' | 'solo', string> = {
253
254
  manual: 'Manual',
@@ -373,7 +374,7 @@ export function CommandPalette({ emptyAction }: { emptyAction?: CommandPaletteEm
373
374
  {/* ── Wall Mode sub-page ────────────────────────────────────── */}
374
375
  {page === 'wall-mode' && (
375
376
  <Command.Group heading="Wall Mode">
376
- {(['cutaway', 'up', 'down'] as const).map((mode) => (
377
+ {(['cutaway', 'up', 'down', 'translucent'] as const).map((mode) => (
377
378
  <OptionItem
378
379
  isActive={wallMode === mode}
379
380
  key={mode}