@pascal-app/editor 0.7.0 → 0.8.0

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 (103) hide show
  1. package/package.json +6 -6
  2. package/src/components/editor/custom-camera-controls.tsx +2 -1
  3. package/src/components/editor/editor-layout-v2.tsx +4 -3
  4. package/src/components/editor/first-person/build-collider-world.ts +5 -7
  5. package/src/components/editor/first-person/bvh-ecctrl.tsx +119 -54
  6. package/src/components/editor/first-person-controls.tsx +11 -11
  7. package/src/components/editor/floating-action-menu.tsx +0 -0
  8. package/src/components/editor/floorplan-panel.tsx +44 -37
  9. package/src/components/editor/index.tsx +68 -53
  10. package/src/components/editor/selection-manager.tsx +2 -2
  11. package/src/components/editor/snapshot-capture-overlay.tsx +465 -0
  12. package/src/components/editor/thumbnail-generator.tsx +18 -61
  13. package/src/components/editor/use-floorplan-background-placement.ts +3 -3
  14. package/src/components/editor/wall-measurement-label.tsx +0 -0
  15. package/src/components/editor-2d/renderers/floorplan-draft-layer.tsx +6 -1
  16. package/src/components/editor-2d/renderers/floorplan-measurements-layer.tsx +6 -1
  17. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +5 -5
  18. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +10 -12
  19. package/src/components/systems/roof/roof-edit-system.tsx +1 -1
  20. package/src/components/systems/stair/stair-edit-system.tsx +1 -1
  21. package/src/components/systems/zone/zone-label-editor-system.tsx +0 -0
  22. package/src/components/systems/zone/zone-system.tsx +0 -0
  23. package/src/components/tools/ceiling/move-ceiling-tool.tsx +9 -2
  24. package/src/components/tools/fence/curve-fence-tool.tsx +4 -5
  25. package/src/components/tools/fence/fence-tool.tsx +2 -2
  26. package/src/components/tools/fence/move-fence-endpoint-tool.tsx +11 -8
  27. package/src/components/tools/fence/move-fence-tool.tsx +13 -9
  28. package/src/components/tools/item/move-tool.tsx +3 -6
  29. package/src/components/tools/item/placement-math.ts +2 -4
  30. package/src/components/tools/item/placement-strategies.ts +11 -10
  31. package/src/components/tools/item/use-draft-node.ts +0 -1
  32. package/src/components/tools/item/use-placement-coordinator.tsx +9 -111
  33. package/src/components/tools/roof/move-roof-tool.tsx +7 -2
  34. package/src/components/tools/select/box-select-tool.tsx +12 -17
  35. package/src/components/tools/shared/segment-angle.ts +1 -1
  36. package/src/components/tools/tool-manager.tsx +12 -12
  37. package/src/components/tools/wall/curve-wall-tool.tsx +8 -6
  38. package/src/components/tools/wall/move-wall-endpoint-tool.tsx +11 -8
  39. package/src/components/tools/wall/move-wall-tool.tsx +6 -4
  40. package/src/components/tools/wall/wall-drafting.ts +0 -0
  41. package/src/components/tools/wall/wall-tool.tsx +3 -3
  42. package/src/components/tools/zone/zone-tool.tsx +20 -5
  43. package/src/components/ui/action-menu/camera-actions.tsx +0 -0
  44. package/src/components/ui/action-menu/control-modes.tsx +7 -1
  45. package/src/components/ui/action-menu/furnish-tools.tsx +6 -92
  46. package/src/components/ui/action-menu/index.tsx +35 -86
  47. package/src/components/ui/action-menu/view-toggles.tsx +19 -31
  48. package/src/components/ui/command-palette/editor-commands.tsx +6 -4
  49. package/src/components/ui/command-palette/index.tsx +4 -255
  50. package/src/components/ui/controls/material-picker.tsx +8 -5
  51. package/src/components/ui/floating-level-selector.tsx +1 -1
  52. package/src/components/ui/helpers/helper-manager.tsx +5 -0
  53. package/src/components/ui/item-catalog/catalog-items.tsx +1742 -315
  54. package/src/components/ui/item-catalog/item-catalog.tsx +88 -46
  55. package/src/components/ui/level-duplicate-dialog.tsx +3 -5
  56. package/src/components/ui/panels/ceiling-panel.tsx +2 -3
  57. package/src/components/ui/panels/column-panel.tsx +62 -18
  58. package/src/components/ui/panels/door-panel.tsx +272 -265
  59. package/src/components/ui/panels/fence-panel.tsx +0 -5
  60. package/src/components/ui/panels/paint-panel.tsx +66 -41
  61. package/src/components/ui/panels/panel-manager.tsx +3 -32
  62. package/src/components/ui/panels/reference-panel.tsx +28 -13
  63. package/src/components/ui/panels/roof-panel.tsx +52 -2
  64. package/src/components/ui/panels/roof-segment-panel.tsx +0 -0
  65. package/src/components/ui/panels/slab-panel.tsx +0 -0
  66. package/src/components/ui/panels/spawn-panel.tsx +10 -4
  67. package/src/components/ui/panels/stair-panel.tsx +66 -14
  68. package/src/components/ui/panels/wall-panel.tsx +97 -1
  69. package/src/components/ui/panels/window-panel.tsx +13 -5
  70. package/src/components/ui/primitives/number-input.tsx +1 -1
  71. package/src/components/ui/primitives/sidebar.tsx +0 -0
  72. package/src/components/ui/sidebar/app-sidebar.tsx +0 -0
  73. package/src/components/ui/sidebar/icon-rail.tsx +0 -0
  74. package/src/components/ui/sidebar/panels/items-panel/index.tsx +330 -0
  75. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +0 -0
  76. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +0 -0
  77. package/src/components/ui/sidebar/panels/site-panel/index.tsx +4 -6
  78. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +0 -0
  79. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -7
  80. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +3 -1
  81. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +3 -1
  82. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +1 -1
  83. package/src/components/ui/slider.tsx +1 -1
  84. package/src/components/viewer-overlay.tsx +0 -0
  85. package/src/components/viewer-zone-system.tsx +0 -0
  86. package/src/hooks/use-auto-save.ts +14 -0
  87. package/src/hooks/use-keyboard.ts +10 -0
  88. package/src/index.tsx +8 -1
  89. package/src/lib/level-duplication.test.ts +0 -2
  90. package/src/lib/level-duplication.ts +1 -1
  91. package/src/lib/material-paint.ts +1 -1
  92. package/src/lib/roof-duplication.ts +1 -1
  93. package/src/lib/scene-bounds.ts +1 -1
  94. package/src/lib/scene.ts +0 -0
  95. package/src/lib/sfx-bus.ts +2 -0
  96. package/src/lib/sfx-player.ts +5 -5
  97. package/src/lib/stair-duplication.ts +2 -2
  98. package/src/store/use-editor.tsx +27 -59
  99. package/tsconfig.json +2 -1
  100. package/src/components/feedback-dialog.tsx +0 -265
  101. package/src/components/pascal-radio.tsx +0 -280
  102. package/src/components/preview-button.tsx +0 -16
  103. package/src/components/ui/viewer-toolbar.tsx +0 -436
@@ -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
- }