@pascal-app/editor 0.7.0 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (244) hide show
  1. package/package.json +12 -13
  2. package/src/components/editor/alignment-3d-guide-layer.tsx +160 -0
  3. package/src/components/editor/custom-camera-controls.tsx +755 -25
  4. package/src/components/editor/editor-layout-mobile.tsx +3 -3
  5. package/src/components/editor/editor-layout-v2.tsx +62 -54
  6. package/src/components/editor/export-manager.tsx +32 -3
  7. package/src/components/editor/first-person/build-collider-world.test.ts +149 -0
  8. package/src/components/editor/first-person/build-collider-world.ts +203 -87
  9. package/src/components/editor/first-person/bvh-ecctrl.tsx +127 -62
  10. package/src/components/editor/first-person-controls.tsx +836 -31
  11. package/src/components/editor/floating-action-menu.tsx +287 -196
  12. package/src/components/editor/floating-building-action-menu.tsx +1 -1
  13. package/src/components/editor/floorplan-panel.tsx +5837 -12006
  14. package/src/components/editor/grid.tsx +51 -17
  15. package/src/components/editor/group-move-handle.tsx +316 -0
  16. package/src/components/editor/group-rotate-handle.tsx +382 -0
  17. package/src/components/editor/group-transform-shared.test.ts +205 -0
  18. package/src/components/editor/group-transform-shared.ts +256 -0
  19. package/src/components/editor/handles/handle-arrow.tsx +523 -0
  20. package/src/components/editor/handles/use-handle-drag.ts +213 -0
  21. package/src/components/editor/index.tsx +234 -190
  22. package/src/components/editor/measurement-pill.tsx +75 -0
  23. package/src/components/editor/node-arrow-handles.tsx +1453 -0
  24. package/src/components/editor/selection-manager.tsx +435 -158
  25. package/src/components/editor/site-edge-labels.tsx +44 -20
  26. package/src/components/editor/slab-hole-highlights.tsx +466 -0
  27. package/src/components/editor/snapshot-capture-overlay.tsx +539 -0
  28. package/src/components/editor/thumbnail-generator.tsx +27 -64
  29. package/src/components/editor/use-floorplan-background-placement.ts +122 -35
  30. package/src/components/editor/use-floorplan-hit-testing.ts +24 -0
  31. package/src/components/editor/use-floorplan-scene-data.ts +14 -2
  32. package/src/components/editor/wall-measurement-label.tsx +11 -6
  33. package/src/components/editor/wall-move-side-handles.tsx +855 -0
  34. package/src/components/editor/wall-opening-highlights.tsx +181 -0
  35. package/src/components/editor/wall-snap-beacon-layer.tsx +150 -0
  36. package/src/components/editor-2d/floorplan-action-menu-layer.tsx +6 -1
  37. package/src/components/editor-2d/floorplan-alignment-guide-layer.tsx +142 -0
  38. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +5 -3
  39. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +227 -0
  40. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +616 -0
  41. package/src/components/editor-2d/floorplan-render-context.tsx +62 -0
  42. package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +75 -0
  43. package/src/components/editor-2d/floorplan-wall-move-ghost-layer.tsx +47 -0
  44. package/src/components/editor-2d/renderers/floorplan-draft-layer.tsx +6 -1
  45. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +266 -0
  46. package/src/components/editor-2d/renderers/floorplan-measurements-layer.tsx +108 -61
  47. package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +59 -0
  48. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +1963 -0
  49. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +29 -14
  50. package/src/components/editor-2d/svg-paths.ts +10 -2
  51. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +30 -9
  52. package/src/components/systems/ceiling/ceiling-system.tsx +12 -8
  53. package/src/components/systems/roof/roof-edit-system.tsx +86 -12
  54. package/src/components/systems/stair/stair-edit-system.tsx +2 -2
  55. package/src/components/systems/zone/zone-label-editor-system.tsx +0 -0
  56. package/src/components/systems/zone/zone-system.tsx +0 -0
  57. package/src/components/tools/elevator/elevator-defaults.ts +8 -0
  58. package/src/components/tools/elevator/elevator-tool.tsx +349 -0
  59. package/src/components/tools/elevator/move-elevator-tool.tsx +252 -0
  60. package/src/components/tools/fence/fence-drafting.ts +64 -22
  61. package/src/components/tools/item/move-tool.tsx +40 -107
  62. package/src/components/tools/item/placement-math.test.ts +10 -0
  63. package/src/components/tools/item/placement-math.ts +33 -9
  64. package/src/components/tools/item/placement-strategies.ts +201 -25
  65. package/src/components/tools/item/placement-types.ts +10 -1
  66. package/src/components/tools/item/use-draft-node.ts +20 -2
  67. package/src/components/tools/item/use-placement-coordinator.tsx +700 -299
  68. package/src/components/tools/registry/move-registry-node-tool.tsx +602 -0
  69. package/src/components/tools/roof/roof-tool.tsx +108 -12
  70. package/src/components/tools/select/box-select-state.ts +77 -0
  71. package/src/components/tools/select/box-select-tool.tsx +324 -543
  72. package/src/components/tools/select/plane-box-select-tool.tsx +577 -0
  73. package/src/components/tools/select/screen-rectangle-selection.ts +84 -0
  74. package/src/components/tools/select/select-candidates.test.ts +109 -0
  75. package/src/components/tools/select/select-candidates.ts +85 -0
  76. package/src/components/tools/shared/affordance-dispatch.ts +30 -0
  77. package/src/components/tools/shared/drag-bounding-box.tsx +171 -0
  78. package/src/components/tools/shared/floor-stack-preview.ts +25 -0
  79. package/src/components/tools/shared/fresh-placement-visibility.ts +61 -0
  80. package/src/components/tools/shared/placement-box-geometry.ts +123 -0
  81. package/src/components/tools/shared/placement-box.tsx +123 -0
  82. package/src/components/tools/shared/polygon-editor.tsx +617 -167
  83. package/src/components/tools/shared/segment-angle.ts +57 -1
  84. package/src/components/tools/site/site-boundary-editor.tsx +464 -12
  85. package/src/components/tools/site/site-flag-model.ts +18 -0
  86. package/src/components/tools/stair/stair-defaults.ts +1 -1
  87. package/src/components/tools/stair/stair-tool.tsx +269 -35
  88. package/src/components/tools/tool-manager.tsx +172 -56
  89. package/src/components/tools/wall/wall-drafting.ts +98 -98
  90. package/src/components/tools/wall/wall-snap-geometry.test.ts +79 -0
  91. package/src/components/tools/wall/wall-snap-geometry.ts +270 -0
  92. package/src/components/tools/zone/zone-tool.tsx +36 -11
  93. package/src/components/ui/action-menu/action-button.tsx +21 -1
  94. package/src/components/ui/action-menu/camera-actions.tsx +0 -0
  95. package/src/components/ui/action-menu/control-modes.tsx +5 -76
  96. package/src/components/ui/action-menu/furnish-tools.tsx +6 -92
  97. package/src/components/ui/action-menu/index.tsx +33 -196
  98. package/src/components/ui/action-menu/structure-tools.tsx +13 -88
  99. package/src/components/ui/action-menu/view-toggles.tsx +262 -37
  100. package/src/components/ui/command-palette/editor-commands.tsx +24 -17
  101. package/src/components/ui/command-palette/index.tsx +6 -256
  102. package/src/components/ui/controls/material-paint-panel.tsx +83 -0
  103. package/src/components/ui/controls/material-picker.tsx +17 -35
  104. package/src/components/ui/controls/metric-control.tsx +33 -11
  105. package/src/components/ui/floating-level-selector.tsx +48 -11
  106. package/src/components/ui/helpers/helper-manager.tsx +21 -17
  107. package/src/components/ui/helpers/registered-tool-helper.tsx +25 -0
  108. package/src/components/ui/item-catalog/catalog-items.tsx +1743 -315
  109. package/src/components/ui/item-catalog/item-catalog.tsx +91 -46
  110. package/src/components/ui/level-duplicate-dialog.tsx +5 -6
  111. package/src/components/ui/panels/node-display.ts +1 -0
  112. package/src/components/ui/panels/paint-panel.tsx +66 -41
  113. package/src/components/ui/panels/panel-manager.tsx +33 -74
  114. package/src/components/ui/panels/panel-wrapper.tsx +217 -12
  115. package/src/components/ui/panels/parametric-inspector.tsx +469 -0
  116. package/src/components/ui/panels/reference-panel.tsx +28 -13
  117. package/src/components/ui/primitives/dropdown-menu.tsx +12 -8
  118. package/src/components/ui/primitives/number-input.tsx +1 -1
  119. package/src/components/ui/primitives/sidebar.tsx +0 -0
  120. package/src/components/ui/scene-loader.tsx +1 -3
  121. package/src/components/ui/sidebar/app-sidebar.tsx +0 -0
  122. package/src/components/ui/sidebar/icon-rail.tsx +0 -0
  123. package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +263 -0
  124. package/src/components/ui/sidebar/panels/items-panel/index.tsx +404 -0
  125. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +59 -9
  126. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +6 -1
  127. package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +259 -0
  128. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +3 -1
  129. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +5 -2
  130. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +83 -0
  131. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +89 -0
  132. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +75 -0
  133. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +2 -2
  134. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +83 -0
  135. package/src/components/ui/sidebar/panels/site-panel/index.tsx +8 -12
  136. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +2 -1
  137. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +93 -0
  138. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +41 -5
  139. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +128 -0
  140. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +5 -2
  141. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +83 -0
  142. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -1
  143. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +68 -34
  144. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +3 -1
  145. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +52 -3
  146. package/src/components/ui/sidebar/tab-bar.tsx +61 -1
  147. package/src/components/ui/slider.tsx +1 -1
  148. package/src/components/viewer-overlay.tsx +169 -52
  149. package/src/components/viewer-zone-system.tsx +1 -1
  150. package/src/hooks/use-auto-save.ts +24 -3
  151. package/src/hooks/use-ceiling-events.ts +175 -0
  152. package/src/hooks/use-drag-action.ts +124 -0
  153. package/src/hooks/use-keyboard.ts +125 -62
  154. package/src/hooks/use-selection.ts +64 -0
  155. package/src/index.tsx +243 -4
  156. package/src/lib/constants.ts +6 -2
  157. package/src/lib/editor-api.ts +62 -0
  158. package/src/lib/elevator-support.ts +96 -0
  159. package/src/lib/floorplan/apply-alignment.ts +111 -0
  160. package/src/lib/floorplan/index.ts +7 -0
  161. package/src/lib/floorplan/selection-tool.ts +40 -0
  162. package/src/lib/floorplan/stairs.ts +12 -1
  163. package/src/lib/fresh-planar-placement.test.ts +102 -0
  164. package/src/lib/fresh-planar-placement.ts +63 -0
  165. package/src/lib/history.ts +2 -1
  166. package/src/lib/level-duplication.test.ts +0 -2
  167. package/src/lib/level-duplication.ts +55 -43
  168. package/src/lib/level-name.ts +11 -0
  169. package/src/lib/material-paint.ts +212 -20
  170. package/src/lib/placement-metadata.ts +29 -0
  171. package/src/lib/planar-cursor-placement.test.ts +43 -0
  172. package/src/lib/planar-cursor-placement.ts +42 -0
  173. package/src/lib/roof-duplication.ts +2 -2
  174. package/src/lib/scene-bounds.ts +1 -1
  175. package/src/lib/scene-clipboard.ts +267 -0
  176. package/src/lib/scene.ts +13 -4
  177. package/src/lib/sfx-bus.ts +25 -1
  178. package/src/lib/sfx-player.ts +75 -27
  179. package/src/lib/site-boundary.ts +1 -0
  180. package/src/lib/stair-duplication.ts +2 -2
  181. package/src/lib/stair-levels.test.ts +146 -0
  182. package/src/lib/stair-levels.ts +177 -0
  183. package/src/lib/world-grid-snap.ts +199 -0
  184. package/src/lib/zone-content.ts +130 -0
  185. package/src/store/use-alignment-guides.ts +21 -0
  186. package/src/store/use-editor.tsx +342 -91
  187. package/src/store/use-placement-preview.ts +31 -0
  188. package/src/store/use-wall-move-ghosts.ts +36 -0
  189. package/src/store/use-wall-snap-indicator.ts +32 -0
  190. package/tsconfig.json +2 -1
  191. package/src/components/editor/preset-thumbnail-generator.tsx +0 -125
  192. package/src/components/editor-2d/renderers/floorplan-roof-layer.tsx +0 -113
  193. package/src/components/feedback-dialog.tsx +0 -265
  194. package/src/components/pascal-radio.tsx +0 -280
  195. package/src/components/preview-button.tsx +0 -16
  196. package/src/components/tools/building/move-building-tool.tsx +0 -157
  197. package/src/components/tools/ceiling/ceiling-boundary-editor.tsx +0 -43
  198. package/src/components/tools/ceiling/ceiling-hole-editor.tsx +0 -49
  199. package/src/components/tools/ceiling/ceiling-tool.tsx +0 -465
  200. package/src/components/tools/ceiling/move-ceiling-tool.tsx +0 -257
  201. package/src/components/tools/column/column-tool.tsx +0 -97
  202. package/src/components/tools/column/move-column-tool.tsx +0 -105
  203. package/src/components/tools/door/door-math.ts +0 -110
  204. package/src/components/tools/door/door-tool.tsx +0 -324
  205. package/src/components/tools/door/move-door-tool.tsx +0 -412
  206. package/src/components/tools/fence/curve-fence-tool.tsx +0 -179
  207. package/src/components/tools/fence/fence-tool.tsx +0 -346
  208. package/src/components/tools/fence/move-fence-endpoint-tool.tsx +0 -438
  209. package/src/components/tools/fence/move-fence-tool.tsx +0 -298
  210. package/src/components/tools/item/item-tool.tsx +0 -26
  211. package/src/components/tools/roof/move-roof-tool.tsx +0 -359
  212. package/src/components/tools/slab/move-slab-tool.tsx +0 -182
  213. package/src/components/tools/slab/slab-boundary-editor.tsx +0 -43
  214. package/src/components/tools/slab/slab-hole-editor.tsx +0 -49
  215. package/src/components/tools/slab/slab-tool.tsx +0 -322
  216. package/src/components/tools/spawn/move-spawn-tool.tsx +0 -101
  217. package/src/components/tools/spawn/spawn-tool.tsx +0 -130
  218. package/src/components/tools/wall/curve-wall-tool.tsx +0 -176
  219. package/src/components/tools/wall/move-wall-endpoint-tool.tsx +0 -423
  220. package/src/components/tools/wall/move-wall-tool.tsx +0 -356
  221. package/src/components/tools/wall/wall-tool.tsx +0 -332
  222. package/src/components/tools/window/move-window-tool.tsx +0 -447
  223. package/src/components/tools/window/window-math.ts +0 -117
  224. package/src/components/tools/window/window-tool.tsx +0 -332
  225. package/src/components/ui/helpers/ceiling-helper.tsx +0 -20
  226. package/src/components/ui/helpers/slab-helper.tsx +0 -20
  227. package/src/components/ui/helpers/wall-helper.tsx +0 -20
  228. package/src/components/ui/panels/ceiling-panel.tsx +0 -250
  229. package/src/components/ui/panels/column-panel.tsx +0 -715
  230. package/src/components/ui/panels/door-panel.tsx +0 -1292
  231. package/src/components/ui/panels/fence-panel.tsx +0 -227
  232. package/src/components/ui/panels/item-panel.tsx +0 -306
  233. package/src/components/ui/panels/presets/presets-popover.tsx +0 -511
  234. package/src/components/ui/panels/roof-panel.tsx +0 -230
  235. package/src/components/ui/panels/roof-segment-panel.tsx +0 -311
  236. package/src/components/ui/panels/slab-panel.tsx +0 -250
  237. package/src/components/ui/panels/spawn-panel.tsx +0 -155
  238. package/src/components/ui/panels/stair-panel.tsx +0 -525
  239. package/src/components/ui/panels/stair-segment-panel.tsx +0 -325
  240. package/src/components/ui/panels/wall-panel.tsx +0 -177
  241. package/src/components/ui/panels/window-panel.tsx +0 -960
  242. package/src/components/ui/viewer-toolbar.tsx +0 -436
  243. package/src/contexts/presets-context.tsx +0 -121
  244. package/src/hooks/use-contextual-tools.ts +0 -61
@@ -1,436 +0,0 @@
1
- 'use client'
2
-
3
- import { Icon as IconifyIcon } from '@iconify/react'
4
- import { useViewer } from '@pascal-app/viewer'
5
- import {
6
- Check,
7
- ChevronsLeft,
8
- ChevronsRight,
9
- Columns2,
10
- Eye,
11
- EyeOff,
12
- Footprints,
13
- Grid2X2,
14
- Moon,
15
- Sun,
16
- } from 'lucide-react'
17
- import { useCallback } from 'react'
18
- import { cn } from '../../lib/utils'
19
- import useEditor from '../../store/use-editor'
20
- import type { GridSnapStep, ViewMode } from '../../store/use-editor'
21
- import {
22
- DropdownMenu,
23
- DropdownMenuContent,
24
- DropdownMenuItem,
25
- DropdownMenuTrigger,
26
- } from './primitives/dropdown-menu'
27
- import { useSidebarStore } from './primitives/sidebar'
28
- import { Tooltip, TooltipContent, TooltipTrigger } from './primitives/tooltip'
29
-
30
- // ── Shared styles ───────────────────────────────────────────────────────────
31
-
32
- /** Container for a group of buttons — no padding, overflow-hidden clips children flush. */
33
- const TOOLBAR_CONTAINER =
34
- 'inline-flex h-8 items-stretch overflow-hidden rounded-xl border border-border bg-background/90 shadow-2xl backdrop-blur-md'
35
-
36
- /** Ghost button inside a container — flush edges, no individual border/radius. */
37
- const TOOLBAR_BTN =
38
- 'flex items-center justify-center w-8 text-muted-foreground/80 transition-colors hover:bg-white/8 hover:text-foreground/90'
39
-
40
- // ── View mode segmented control ─────────────────────────────────────────────
41
-
42
- const VIEW_MODES: { id: ViewMode; label: string; icon: React.ReactNode }[] = [
43
- {
44
- id: '3d',
45
- label: '3D',
46
- icon: <img alt="" className="h-3.5 w-3.5 object-contain" src="/icons/building.png" />,
47
- },
48
- {
49
- id: '2d',
50
- label: '2D',
51
- icon: <img alt="" className="h-3.5 w-3.5 object-contain" src="/icons/blueprint.png" />,
52
- },
53
- {
54
- id: 'split',
55
- label: 'Split',
56
- icon: <Columns2 className="h-3 w-3" />,
57
- },
58
- ]
59
-
60
- function ViewModeControl() {
61
- const viewMode = useEditor((s) => s.viewMode)
62
- const setViewMode = useEditor((s) => s.setViewMode)
63
-
64
- return (
65
- <div className={TOOLBAR_CONTAINER}>
66
- {VIEW_MODES.map((mode) => {
67
- const isActive = viewMode === mode.id
68
- return (
69
- <button
70
- className={cn(
71
- 'flex items-center justify-center gap-1.5 px-2.5 font-medium text-xs transition-colors',
72
- isActive
73
- ? 'bg-white/10 text-foreground'
74
- : 'text-muted-foreground/70 hover:bg-white/8 hover:text-muted-foreground',
75
- )}
76
- key={mode.id}
77
- onClick={() => setViewMode(mode.id)}
78
- type="button"
79
- >
80
- {mode.icon}
81
- <span>{mode.label}</span>
82
- </button>
83
- )
84
- })}
85
- </div>
86
- )
87
- }
88
-
89
- // ── Collapse sidebar button ─────────────────────────────────────────────────
90
-
91
- function CollapseSidebarButton() {
92
- const isCollapsed = useSidebarStore((s) => s.isCollapsed)
93
- const setIsCollapsed = useSidebarStore((s) => s.setIsCollapsed)
94
-
95
- const toggle = useCallback(() => {
96
- setIsCollapsed(!isCollapsed)
97
- }, [isCollapsed, setIsCollapsed])
98
-
99
- return (
100
- <div className={TOOLBAR_CONTAINER}>
101
- <button
102
- className={TOOLBAR_BTN}
103
- onClick={toggle}
104
- title={isCollapsed ? 'Expand sidebar' : 'Collapse sidebar'}
105
- type="button"
106
- >
107
- {isCollapsed ? <ChevronsRight className="h-4 w-4" /> : <ChevronsLeft className="h-4 w-4" />}
108
- </button>
109
- </div>
110
- )
111
- }
112
-
113
- // ── Right toolbar buttons ───────────────────────────────────────────────────
114
-
115
- function WalkthroughButton() {
116
- const isFirstPersonMode = useEditor((s) => s.isFirstPersonMode)
117
- const setFirstPersonMode = useEditor((s) => s.setFirstPersonMode)
118
-
119
- const toggle = () => {
120
- setFirstPersonMode(!isFirstPersonMode)
121
- }
122
-
123
- return (
124
- <Tooltip>
125
- <TooltipTrigger asChild>
126
- <button
127
- className={cn(
128
- TOOLBAR_BTN,
129
- isFirstPersonMode && 'bg-emerald-500/15 text-emerald-400 hover:bg-emerald-500/20',
130
- )}
131
- onClick={toggle}
132
- type="button"
133
- >
134
- <Footprints className="h-4 w-4" />
135
- </button>
136
- </TooltipTrigger>
137
- <TooltipContent side="bottom">Walkthrough</TooltipContent>
138
- </Tooltip>
139
- )
140
- }
141
-
142
- function UnitToggle() {
143
- const unit = useViewer((s) => s.unit)
144
- const setUnit = useViewer((s) => s.setUnit)
145
-
146
- return (
147
- <Tooltip>
148
- <TooltipTrigger asChild>
149
- <button
150
- className={TOOLBAR_BTN}
151
- onClick={() => setUnit(unit === 'metric' ? 'imperial' : 'metric')}
152
- type="button"
153
- >
154
- <span className="font-semibold text-[10px]">{unit === 'metric' ? 'm' : 'ft'}</span>
155
- </button>
156
- </TooltipTrigger>
157
- <TooltipContent side="bottom">
158
- {unit === 'metric' ? 'Metric (m)' : 'Imperial (ft)'}
159
- </TooltipContent>
160
- </Tooltip>
161
- )
162
- }
163
-
164
- function ThemeToggle() {
165
- const theme = useViewer((s) => s.theme)
166
- const setTheme = useViewer((s) => s.setTheme)
167
-
168
- return (
169
- <Tooltip>
170
- <TooltipTrigger asChild>
171
- <button
172
- className={cn(TOOLBAR_BTN, theme === 'dark' ? 'text-indigo-400/60' : 'text-amber-400/60')}
173
- onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
174
- type="button"
175
- >
176
- {theme === 'dark' ? <Moon className="h-3.5 w-3.5" /> : <Sun className="h-3.5 w-3.5" />}
177
- </button>
178
- </TooltipTrigger>
179
- <TooltipContent side="bottom">{theme === 'dark' ? 'Dark' : 'Light'}</TooltipContent>
180
- </Tooltip>
181
- )
182
- }
183
-
184
- // ── Level mode toggle ───────────────────────────────────────────────────────
185
-
186
- const levelModeOrder = ['stacked', 'exploded', 'solo'] as const
187
- const levelModeLabels: Record<string, string> = {
188
- manual: 'Stack',
189
- stacked: 'Stack',
190
- exploded: 'Exploded',
191
- solo: 'Solo',
192
- }
193
-
194
- const gridSnapOrder: GridSnapStep[] = [0.5, 0.25, 0.1, 0.05]
195
- const gridSnapLabels: Record<GridSnapStep, string> = {
196
- 0.5: '0.50',
197
- 0.25: '0.25',
198
- 0.1: '0.10',
199
- 0.05: '0.05',
200
- }
201
-
202
- function formatGridSnapStep(step: GridSnapStep): string {
203
- return gridSnapLabels[step]
204
- }
205
-
206
- function LevelModeToggle() {
207
- const levelMode = useViewer((s) => s.levelMode)
208
- const setLevelMode = useViewer((s) => s.setLevelMode)
209
-
210
- const cycle = () => {
211
- if (levelMode === 'manual') {
212
- setLevelMode('stacked')
213
- return
214
- }
215
- const idx = levelModeOrder.indexOf(levelMode as (typeof levelModeOrder)[number])
216
- const next = levelModeOrder[(idx + 1) % levelModeOrder.length]
217
- if (next) setLevelMode(next)
218
- }
219
-
220
- const isDefault = levelMode === 'stacked' || levelMode === 'manual'
221
-
222
- return (
223
- <Tooltip>
224
- <TooltipTrigger asChild>
225
- <button
226
- className={cn(
227
- TOOLBAR_BTN,
228
- 'w-auto gap-1.5 px-2.5',
229
- !isDefault && 'bg-white/10 text-foreground/90',
230
- )}
231
- onClick={cycle}
232
- type="button"
233
- >
234
- {levelMode === 'solo' ? (
235
- <IconifyIcon height={14} icon="lucide:diamond" width={14} />
236
- ) : levelMode === 'exploded' ? (
237
- <IconifyIcon height={14} icon="charm:stack-pop" width={14} />
238
- ) : (
239
- <IconifyIcon height={14} icon="charm:stack-push" width={14} />
240
- )}
241
- <span className="font-medium text-xs">{levelModeLabels[levelMode] ?? 'Stack'}</span>
242
- </button>
243
- </TooltipTrigger>
244
- <TooltipContent side="bottom">
245
- Levels: {levelMode === 'manual' ? 'Manual' : levelModeLabels[levelMode]}
246
- </TooltipContent>
247
- </Tooltip>
248
- )
249
- }
250
-
251
- function GridSnapToggle() {
252
- const gridSnapStep = useEditor((s) => s.gridSnapStep)
253
- const setGridSnapStep = useEditor((s) => s.setGridSnapStep)
254
-
255
- return (
256
- <DropdownMenu>
257
- <Tooltip>
258
- <TooltipTrigger asChild>
259
- <DropdownMenuTrigger asChild>
260
- <button className={cn(TOOLBAR_BTN, 'w-auto gap-1.5 px-2.5')} type="button">
261
- <IconifyIcon height={14} icon="lucide:grid-2x2" width={14} />
262
- <span className="font-medium text-xs">{formatGridSnapStep(gridSnapStep)}</span>
263
- </button>
264
- </DropdownMenuTrigger>
265
- </TooltipTrigger>
266
- <TooltipContent side="bottom">Grid snap: {formatGridSnapStep(gridSnapStep)}</TooltipContent>
267
- </Tooltip>
268
- <DropdownMenuContent align="center" side="bottom">
269
- {gridSnapOrder.map((step) => {
270
- const isActive = step === gridSnapStep
271
- return (
272
- <DropdownMenuItem key={step} onSelect={() => setGridSnapStep(step)}>
273
- <span className="flex min-w-12 items-center justify-between gap-3">
274
- <span>{formatGridSnapStep(step)}</span>
275
- {isActive ? <Check className="h-3.5 w-3.5" /> : <span className="h-3.5 w-3.5" />}
276
- </span>
277
- </DropdownMenuItem>
278
- )
279
- })}
280
- </DropdownMenuContent>
281
- </DropdownMenu>
282
- )
283
- }
284
-
285
- function GridVisibilityToggle() {
286
- const showGrid = useViewer((s) => s.showGrid)
287
- const setShowGrid = useViewer((s) => s.setShowGrid)
288
-
289
- return (
290
- <Tooltip>
291
- <TooltipTrigger asChild>
292
- <button
293
- aria-label={`Grid: ${showGrid ? 'Visible' : 'Hidden'}`}
294
- aria-pressed={showGrid}
295
- className={cn(
296
- TOOLBAR_BTN,
297
- 'w-auto gap-1.5 px-2.5',
298
- showGrid
299
- ? 'bg-white/10 text-foreground/90'
300
- : 'opacity-60 grayscale hover:opacity-100 hover:grayscale-0',
301
- )}
302
- onClick={() => setShowGrid(!showGrid)}
303
- type="button"
304
- >
305
- <Grid2X2 className="h-3.5 w-3.5" />
306
- {showGrid ? <Eye className="h-3.5 w-3.5" /> : <EyeOff className="h-3.5 w-3.5" />}
307
- </button>
308
- </TooltipTrigger>
309
- <TooltipContent side="bottom">Grid: {showGrid ? 'Visible' : 'Hidden'}</TooltipContent>
310
- </Tooltip>
311
- )
312
- }
313
-
314
- // ── Wall mode toggle ────────────────────────────────────────────────────────
315
-
316
- const wallModeOrder = ['cutaway', 'up', 'down'] as const
317
- const wallModeConfig: Record<string, { icon: string; label: string }> = {
318
- up: { icon: '/icons/room.png', label: 'Full height' },
319
- cutaway: { icon: '/icons/wallcut.png', label: 'Cutaway' },
320
- down: { icon: '/icons/walllow.png', label: 'Low' },
321
- }
322
-
323
- function WallModeToggle() {
324
- const wallMode = useViewer((s) => s.wallMode)
325
- const setWallMode = useViewer((s) => s.setWallMode)
326
-
327
- const cycle = () => {
328
- const idx = wallModeOrder.indexOf(wallMode as (typeof wallModeOrder)[number])
329
- const next = wallModeOrder[(idx + 1) % wallModeOrder.length]
330
- if (next) setWallMode(next)
331
- }
332
-
333
- const config = wallModeConfig[wallMode] ?? wallModeConfig.cutaway!
334
-
335
- return (
336
- <Tooltip>
337
- <TooltipTrigger asChild>
338
- <button
339
- className={cn(
340
- TOOLBAR_BTN,
341
- 'w-auto gap-1.5 px-2.5',
342
- wallMode !== 'cutaway'
343
- ? 'bg-white/10'
344
- : 'opacity-60 grayscale hover:opacity-100 hover:grayscale-0',
345
- )}
346
- onClick={cycle}
347
- type="button"
348
- >
349
- <img alt={config.label} className="h-4 w-4 object-contain" src={config.icon} />
350
- <span className="font-medium text-xs">{config.label}</span>
351
- </button>
352
- </TooltipTrigger>
353
- <TooltipContent side="bottom">Walls: {config.label}</TooltipContent>
354
- </Tooltip>
355
- )
356
- }
357
-
358
- // ── Camera mode toggle ──────────────────────────────────────────────────────
359
-
360
- function CameraModeToggle() {
361
- const cameraMode = useViewer((s) => s.cameraMode)
362
- const setCameraMode = useViewer((s) => s.setCameraMode)
363
-
364
- return (
365
- <Tooltip>
366
- <TooltipTrigger asChild>
367
- <button
368
- className={cn(
369
- TOOLBAR_BTN,
370
- cameraMode === 'orthographic' && 'bg-white/10 text-foreground/90',
371
- )}
372
- onClick={() =>
373
- setCameraMode(cameraMode === 'perspective' ? 'orthographic' : 'perspective')
374
- }
375
- type="button"
376
- >
377
- {cameraMode === 'perspective' ? (
378
- <IconifyIcon height={16} icon="icon-park-outline:perspective" width={16} />
379
- ) : (
380
- <IconifyIcon height={16} icon="vaadin:grid" width={16} />
381
- )}
382
- </button>
383
- </TooltipTrigger>
384
- <TooltipContent side="bottom">
385
- {cameraMode === 'perspective' ? 'Perspective' : 'Orthographic'}
386
- </TooltipContent>
387
- </Tooltip>
388
- )
389
- }
390
-
391
- function PreviewButton() {
392
- return (
393
- <Tooltip>
394
- <TooltipTrigger asChild>
395
- <button
396
- className="flex items-center gap-1.5 px-2.5 font-medium text-muted-foreground/80 text-xs transition-colors hover:bg-white/8 hover:text-foreground/90"
397
- onClick={() => useEditor.getState().setPreviewMode(true)}
398
- type="button"
399
- >
400
- <Eye className="h-3.5 w-3.5 shrink-0" />
401
- <span>Preview</span>
402
- </button>
403
- </TooltipTrigger>
404
- <TooltipContent side="bottom">Preview mode</TooltipContent>
405
- </Tooltip>
406
- )
407
- }
408
-
409
- // ── Composed toolbar sections ───────────────────────────────────────────────
410
-
411
- export function ViewerToolbarLeft() {
412
- return (
413
- <>
414
- <CollapseSidebarButton />
415
- <ViewModeControl />
416
- </>
417
- )
418
- }
419
-
420
- export function ViewerToolbarRight() {
421
- return (
422
- <div className={TOOLBAR_CONTAINER}>
423
- <LevelModeToggle />
424
- <WallModeToggle />
425
- <GridSnapToggle />
426
- <GridVisibilityToggle />
427
- <div className="my-1.5 w-px bg-border/50" />
428
- <UnitToggle />
429
- <ThemeToggle />
430
- <CameraModeToggle />
431
- <div className="my-1.5 w-px bg-border/50" />
432
- <WalkthroughButton />
433
- <PreviewButton />
434
- </div>
435
- )
436
- }
@@ -1,121 +0,0 @@
1
- 'use client'
2
-
3
- import { createContext, useContext } from 'react'
4
- import type { PresetData, PresetType } from '../components/ui/panels/presets/presets-popover'
5
-
6
- export type { PresetData, PresetType }
7
-
8
- export type PresetsTab = 'community' | 'mine'
9
-
10
- export interface PresetsAdapter {
11
- /** Tabs to show. Default: both. Standalone passes ['mine']. */
12
- tabs?: PresetsTab[]
13
- isAuthenticated?: boolean
14
- fetchPresets: (type: PresetType, tab: PresetsTab) => Promise<PresetData[]>
15
- savePreset: (
16
- type: PresetType,
17
- name: string,
18
- data: Record<string, unknown>,
19
- ) => Promise<string | null>
20
- overwritePreset: (type: PresetType, id: string, data: Record<string, unknown>) => Promise<void>
21
- renamePreset: (id: string, name: string) => Promise<void>
22
- deletePreset: (id: string) => Promise<void>
23
- togglePresetCommunity?: (id: string, current: boolean) => Promise<void>
24
- uploadPresetThumbnail?: (presetId: string, blob: Blob) => Promise<string | null>
25
- }
26
-
27
- const PRESETS_KEY = (type: string) => `pascal-presets-${type}`
28
-
29
- export const localStoragePresetsAdapter: PresetsAdapter = {
30
- tabs: ['mine'],
31
- isAuthenticated: true,
32
-
33
- fetchPresets: async (type, tab) => {
34
- if (tab === 'community') return []
35
- try {
36
- const raw = localStorage.getItem(PRESETS_KEY(type))
37
- return raw ? (JSON.parse(raw) as PresetData[]) : []
38
- } catch {
39
- return []
40
- }
41
- },
42
-
43
- savePreset: async (type, name, data) => {
44
- try {
45
- const id = Math.random().toString(36).slice(2, 10)
46
- const raw = localStorage.getItem(PRESETS_KEY(type))
47
- const presets: PresetData[] = raw ? JSON.parse(raw) : []
48
- presets.push({
49
- id,
50
- type,
51
- name,
52
- data,
53
- thumbnail_url: null,
54
- user_id: null,
55
- is_community: false,
56
- created_at: new Date().toISOString(),
57
- })
58
- localStorage.setItem(PRESETS_KEY(type), JSON.stringify(presets))
59
- return id
60
- } catch {
61
- return null
62
- }
63
- },
64
-
65
- overwritePreset: async (type, id, data) => {
66
- try {
67
- const raw = localStorage.getItem(PRESETS_KEY(type))
68
- if (!raw) return
69
- const presets: PresetData[] = JSON.parse(raw)
70
- localStorage.setItem(
71
- PRESETS_KEY(type),
72
- JSON.stringify(presets.map((p) => (p.id === id ? { ...p, data } : p))),
73
- )
74
- } catch {}
75
- },
76
-
77
- renamePreset: async (id, name) => {
78
- for (const type of ['door', 'window']) {
79
- try {
80
- const raw = localStorage.getItem(PRESETS_KEY(type))
81
- if (!raw) continue
82
- const presets: PresetData[] = JSON.parse(raw)
83
- localStorage.setItem(
84
- PRESETS_KEY(type),
85
- JSON.stringify(presets.map((p) => (p.id === id ? { ...p, name } : p))),
86
- )
87
- } catch {}
88
- }
89
- },
90
-
91
- deletePreset: async (id) => {
92
- for (const type of ['door', 'window']) {
93
- try {
94
- const raw = localStorage.getItem(PRESETS_KEY(type))
95
- if (!raw) continue
96
- const presets: PresetData[] = JSON.parse(raw)
97
- localStorage.setItem(PRESETS_KEY(type), JSON.stringify(presets.filter((p) => p.id !== id)))
98
- } catch {}
99
- }
100
- },
101
- }
102
-
103
- const PresetsContext = createContext<PresetsAdapter>(localStoragePresetsAdapter)
104
-
105
- export function PresetsProvider({
106
- adapter,
107
- children,
108
- }: {
109
- adapter?: PresetsAdapter
110
- children: React.ReactNode
111
- }) {
112
- return (
113
- <PresetsContext.Provider value={adapter ?? localStoragePresetsAdapter}>
114
- {children}
115
- </PresetsContext.Provider>
116
- )
117
- }
118
-
119
- export function usePresetsAdapter(): PresetsAdapter {
120
- return useContext(PresetsContext)
121
- }
@@ -1,61 +0,0 @@
1
- import { type AnyNodeId, useScene } from '@pascal-app/core'
2
- import { useViewer } from '@pascal-app/viewer'
3
- import { useMemo } from 'react'
4
- import { useShallow } from 'zustand/react/shallow'
5
- import useEditor, { type StructureTool } from '../store/use-editor'
6
-
7
- export function useContextualTools() {
8
- const selection = useViewer((s) => s.selection)
9
- // Only resubscribe when the *types* of selected nodes change, not when any
10
- // node in the scene mutates.
11
- const selectedTypes = useScene(
12
- useShallow((s) =>
13
- selection.selectedIds.map((id) => s.nodes[id as AnyNodeId]?.type).filter(Boolean),
14
- ),
15
- )
16
- const structureLayer = useEditor((s) => s.structureLayer)
17
-
18
- return useMemo(() => {
19
- // If we are in the zones layer, only zone tool is relevant
20
- if (structureLayer === 'zones') {
21
- return ['zone'] as StructureTool[]
22
- }
23
-
24
- // Default tools when nothing is selected
25
- const defaultTools: StructureTool[] = [
26
- 'wall',
27
- 'fence',
28
- 'slab',
29
- 'ceiling',
30
- 'roof',
31
- 'door',
32
- 'window',
33
- ]
34
-
35
- if (selectedTypes.length === 0) {
36
- return defaultTools
37
- }
38
-
39
- // If a wall is selected, prioritize wall-hosted elements
40
- if (selectedTypes.includes('wall')) {
41
- return ['window', 'door', 'wall', 'fence'] as StructureTool[]
42
- }
43
-
44
- // If a slab is selected, prioritize slab editing
45
- if (selectedTypes.includes('slab')) {
46
- return ['slab', 'wall'] as StructureTool[]
47
- }
48
-
49
- // If a ceiling is selected, prioritize ceiling editing
50
- if (selectedTypes.includes('ceiling')) {
51
- return ['ceiling'] as StructureTool[]
52
- }
53
-
54
- // If a roof is selected, prioritize roof editing
55
- if (selectedTypes.includes('roof')) {
56
- return ['roof'] as StructureTool[]
57
- }
58
-
59
- return defaultTools
60
- }, [selectedTypes, structureLayer])
61
- }