@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.
- package/package.json +6 -6
- package/src/components/editor/custom-camera-controls.tsx +2 -1
- package/src/components/editor/editor-layout-v2.tsx +4 -3
- package/src/components/editor/first-person/build-collider-world.ts +5 -7
- package/src/components/editor/first-person/bvh-ecctrl.tsx +119 -54
- package/src/components/editor/first-person-controls.tsx +11 -11
- package/src/components/editor/floating-action-menu.tsx +0 -0
- package/src/components/editor/floorplan-panel.tsx +44 -37
- package/src/components/editor/index.tsx +68 -53
- package/src/components/editor/selection-manager.tsx +2 -2
- package/src/components/editor/snapshot-capture-overlay.tsx +465 -0
- package/src/components/editor/thumbnail-generator.tsx +18 -61
- package/src/components/editor/use-floorplan-background-placement.ts +3 -3
- package/src/components/editor/wall-measurement-label.tsx +0 -0
- package/src/components/editor-2d/renderers/floorplan-draft-layer.tsx +6 -1
- package/src/components/editor-2d/renderers/floorplan-measurements-layer.tsx +6 -1
- package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +5 -5
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +10 -12
- package/src/components/systems/roof/roof-edit-system.tsx +1 -1
- package/src/components/systems/stair/stair-edit-system.tsx +1 -1
- package/src/components/systems/zone/zone-label-editor-system.tsx +0 -0
- package/src/components/systems/zone/zone-system.tsx +0 -0
- package/src/components/tools/ceiling/move-ceiling-tool.tsx +9 -2
- package/src/components/tools/fence/curve-fence-tool.tsx +4 -5
- package/src/components/tools/fence/fence-tool.tsx +2 -2
- package/src/components/tools/fence/move-fence-endpoint-tool.tsx +11 -8
- package/src/components/tools/fence/move-fence-tool.tsx +13 -9
- package/src/components/tools/item/move-tool.tsx +3 -6
- package/src/components/tools/item/placement-math.ts +2 -4
- package/src/components/tools/item/placement-strategies.ts +11 -10
- package/src/components/tools/item/use-draft-node.ts +0 -1
- package/src/components/tools/item/use-placement-coordinator.tsx +9 -111
- package/src/components/tools/roof/move-roof-tool.tsx +7 -2
- package/src/components/tools/select/box-select-tool.tsx +12 -17
- package/src/components/tools/shared/segment-angle.ts +1 -1
- package/src/components/tools/tool-manager.tsx +12 -12
- package/src/components/tools/wall/curve-wall-tool.tsx +8 -6
- package/src/components/tools/wall/move-wall-endpoint-tool.tsx +11 -8
- package/src/components/tools/wall/move-wall-tool.tsx +6 -4
- package/src/components/tools/wall/wall-drafting.ts +0 -0
- package/src/components/tools/wall/wall-tool.tsx +3 -3
- package/src/components/tools/zone/zone-tool.tsx +20 -5
- package/src/components/ui/action-menu/camera-actions.tsx +0 -0
- package/src/components/ui/action-menu/control-modes.tsx +7 -1
- package/src/components/ui/action-menu/furnish-tools.tsx +6 -92
- package/src/components/ui/action-menu/index.tsx +35 -86
- package/src/components/ui/action-menu/view-toggles.tsx +19 -31
- package/src/components/ui/command-palette/editor-commands.tsx +6 -4
- package/src/components/ui/command-palette/index.tsx +4 -255
- package/src/components/ui/controls/material-picker.tsx +8 -5
- package/src/components/ui/floating-level-selector.tsx +1 -1
- package/src/components/ui/helpers/helper-manager.tsx +5 -0
- package/src/components/ui/item-catalog/catalog-items.tsx +1742 -315
- package/src/components/ui/item-catalog/item-catalog.tsx +88 -46
- package/src/components/ui/level-duplicate-dialog.tsx +3 -5
- package/src/components/ui/panels/ceiling-panel.tsx +2 -3
- package/src/components/ui/panels/column-panel.tsx +62 -18
- package/src/components/ui/panels/door-panel.tsx +272 -265
- package/src/components/ui/panels/fence-panel.tsx +0 -5
- package/src/components/ui/panels/paint-panel.tsx +66 -41
- package/src/components/ui/panels/panel-manager.tsx +3 -32
- package/src/components/ui/panels/reference-panel.tsx +28 -13
- package/src/components/ui/panels/roof-panel.tsx +52 -2
- package/src/components/ui/panels/roof-segment-panel.tsx +0 -0
- package/src/components/ui/panels/slab-panel.tsx +0 -0
- package/src/components/ui/panels/spawn-panel.tsx +10 -4
- package/src/components/ui/panels/stair-panel.tsx +66 -14
- package/src/components/ui/panels/wall-panel.tsx +97 -1
- package/src/components/ui/panels/window-panel.tsx +13 -5
- package/src/components/ui/primitives/number-input.tsx +1 -1
- package/src/components/ui/primitives/sidebar.tsx +0 -0
- package/src/components/ui/sidebar/app-sidebar.tsx +0 -0
- package/src/components/ui/sidebar/icon-rail.tsx +0 -0
- package/src/components/ui/sidebar/panels/items-panel/index.tsx +330 -0
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +0 -0
- package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +0 -0
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +4 -6
- package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +0 -0
- package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -7
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +3 -1
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +3 -1
- package/src/components/ui/sidebar/panels/zone-panel/index.tsx +1 -1
- package/src/components/ui/slider.tsx +1 -1
- package/src/components/viewer-overlay.tsx +0 -0
- package/src/components/viewer-zone-system.tsx +0 -0
- package/src/hooks/use-auto-save.ts +14 -0
- package/src/hooks/use-keyboard.ts +10 -0
- package/src/index.tsx +8 -1
- package/src/lib/level-duplication.test.ts +0 -2
- package/src/lib/level-duplication.ts +1 -1
- package/src/lib/material-paint.ts +1 -1
- package/src/lib/roof-duplication.ts +1 -1
- package/src/lib/scene-bounds.ts +1 -1
- package/src/lib/scene.ts +0 -0
- package/src/lib/sfx-bus.ts +2 -0
- package/src/lib/sfx-player.ts +5 -5
- package/src/lib/stair-duplication.ts +2 -2
- package/src/store/use-editor.tsx +27 -59
- package/tsconfig.json +2 -1
- package/src/components/feedback-dialog.tsx +0 -265
- package/src/components/pascal-radio.tsx +0 -280
- package/src/components/preview-button.tsx +0 -16
- package/src/components/ui/viewer-toolbar.tsx +0 -436
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
+
import { Icon } from '@iconify/react'
|
|
3
4
|
import {
|
|
4
5
|
type AnyNodeId,
|
|
5
6
|
type BuildingNode,
|
|
@@ -258,7 +259,7 @@ function GuidesControl() {
|
|
|
258
259
|
|
|
259
260
|
<PopoverContent
|
|
260
261
|
align="center"
|
|
261
|
-
className="w-72 rounded-xl border-border/45 bg-background/96 p-3 shadow-
|
|
262
|
+
className="w-72 rounded-xl border-border/45 bg-background/96 p-3 shadow-elevation-3 backdrop-blur-xl"
|
|
262
263
|
side="top"
|
|
263
264
|
sideOffset={14}
|
|
264
265
|
>
|
|
@@ -353,9 +354,9 @@ function GuidesControl() {
|
|
|
353
354
|
)
|
|
354
355
|
}
|
|
355
356
|
|
|
356
|
-
// ── Grid snap
|
|
357
|
+
// ── Grid snap toggle ────────────────────────────────────────────────────────
|
|
357
358
|
|
|
358
|
-
|
|
359
|
+
function GridSnapControl() {
|
|
359
360
|
const [isOpen, setIsOpen] = useState(false)
|
|
360
361
|
const gridSnapStep = useEditor((state) => state.gridSnapStep)
|
|
361
362
|
const setGridSnapStep = useEditor((state) => state.setGridSnapStep)
|
|
@@ -374,20 +375,7 @@ export function GridSnapControl() {
|
|
|
374
375
|
)}
|
|
375
376
|
type="button"
|
|
376
377
|
>
|
|
377
|
-
<
|
|
378
|
-
className="h-4 w-4"
|
|
379
|
-
fill="none"
|
|
380
|
-
stroke="currentColor"
|
|
381
|
-
strokeWidth={1.5}
|
|
382
|
-
viewBox="0 0 24 24"
|
|
383
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
384
|
-
>
|
|
385
|
-
<path
|
|
386
|
-
d="M3 3h7v7H3V3zm11 0h7v7h-7V3zm0 11h7v7h-7v-7zm-11 0h7v7H3v-7z"
|
|
387
|
-
strokeLinecap="round"
|
|
388
|
-
strokeLinejoin="round"
|
|
389
|
-
/>
|
|
390
|
-
</svg>
|
|
378
|
+
<Icon height={16} icon="lucide:grid-2x2" width={16} />
|
|
391
379
|
<span className="mt-1 font-medium text-[9px] leading-none">
|
|
392
380
|
{formatGridSnapStep(gridSnapStep)}
|
|
393
381
|
</span>
|
|
@@ -510,7 +498,7 @@ function ScansControl() {
|
|
|
510
498
|
|
|
511
499
|
<PopoverContent
|
|
512
500
|
align="center"
|
|
513
|
-
className="w-72 rounded-xl border-border/45 bg-background/96 p-3 shadow-
|
|
501
|
+
className="w-72 rounded-xl border-border/45 bg-background/96 p-3 shadow-elevation-3 backdrop-blur-xl"
|
|
514
502
|
side="top"
|
|
515
503
|
sideOffset={14}
|
|
516
504
|
>
|
|
@@ -605,6 +593,8 @@ function ScansControl() {
|
|
|
605
593
|
)
|
|
606
594
|
}
|
|
607
595
|
|
|
596
|
+
// ── Reference floor control ────────────────────────────────────────────────────────────────────
|
|
597
|
+
|
|
608
598
|
function ReferenceFloorControl() {
|
|
609
599
|
const showReferenceFloor = useEditor((state) => state.showReferenceFloor)
|
|
610
600
|
const toggleReferenceFloor = useEditor((state) => state.toggleReferenceFloor)
|
|
@@ -694,7 +684,11 @@ function ReferenceFloorControl() {
|
|
|
694
684
|
onClick={toggleReferenceFloor}
|
|
695
685
|
type="button"
|
|
696
686
|
>
|
|
697
|
-
{showReferenceFloor ?
|
|
687
|
+
{showReferenceFloor ? (
|
|
688
|
+
<Eye className="h-3.5 w-3.5" />
|
|
689
|
+
) : (
|
|
690
|
+
<EyeOff className="h-3.5 w-3.5" />
|
|
691
|
+
)}
|
|
698
692
|
</button>
|
|
699
693
|
</div>
|
|
700
694
|
|
|
@@ -728,9 +722,7 @@ function ReferenceFloorControl() {
|
|
|
728
722
|
)}
|
|
729
723
|
/>
|
|
730
724
|
<span className="min-w-0 flex-1 truncate">{levelName}</span>
|
|
731
|
-
<span className="text-[10px] text-muted-foreground">
|
|
732
|
-
{index + 1} below
|
|
733
|
-
</span>
|
|
725
|
+
<span className="text-[10px] text-muted-foreground">{index + 1} below</span>
|
|
734
726
|
</button>
|
|
735
727
|
)
|
|
736
728
|
})}
|
|
@@ -757,24 +749,20 @@ function ReferenceFloorControl() {
|
|
|
757
749
|
)
|
|
758
750
|
}
|
|
759
751
|
|
|
760
|
-
// ──
|
|
752
|
+
// ── Exports ─────────────────────────────────────────────────────────────────
|
|
761
753
|
|
|
762
|
-
export
|
|
754
|
+
export { GridSnapControl }
|
|
755
|
+
|
|
756
|
+
export function SecondaryToggles() {
|
|
763
757
|
return (
|
|
764
758
|
<div className="flex items-center gap-1">
|
|
765
|
-
{/* Scans (toggle + dropdown) */}
|
|
766
759
|
<ScansControl />
|
|
767
|
-
|
|
768
|
-
{/* Guides (toggle + dropdown) */}
|
|
769
760
|
<GuidesControl />
|
|
770
|
-
|
|
771
|
-
<ReferenceFloorControl />
|
|
772
761
|
</div>
|
|
773
762
|
)
|
|
774
763
|
}
|
|
775
764
|
|
|
776
|
-
|
|
777
|
-
export function SecondaryToggles() {
|
|
765
|
+
export function ViewToggles() {
|
|
778
766
|
return (
|
|
779
767
|
<div className="flex items-center gap-1">
|
|
780
768
|
<GridSnapControl />
|
|
@@ -35,8 +35,8 @@ import {
|
|
|
35
35
|
Video,
|
|
36
36
|
} from 'lucide-react'
|
|
37
37
|
import { useEffect } from 'react'
|
|
38
|
-
import { deleteLevelWithFallbackSelection } from '../../../lib/level-selection'
|
|
39
38
|
import { runRedo, runUndo } from '../../../lib/history'
|
|
39
|
+
import { deleteLevelWithFallbackSelection } from '../../../lib/level-selection'
|
|
40
40
|
import { useCommandRegistry } from '../../../store/use-command-registry'
|
|
41
41
|
import type { StructureTool } from '../../../store/use-editor'
|
|
42
42
|
import useEditor from '../../../store/use-editor'
|
|
@@ -294,12 +294,14 @@ export function EditorCommands() {
|
|
|
294
294
|
},
|
|
295
295
|
{
|
|
296
296
|
id: 'editor.viewer.camera-snapshot',
|
|
297
|
-
label: '
|
|
297
|
+
label: 'Take Snapshot',
|
|
298
298
|
group: 'Viewer Controls',
|
|
299
299
|
icon: <Camera className="h-4 w-4" />,
|
|
300
300
|
keywords: ['camera', 'snapshot', 'capture', 'save', 'view', 'bookmark'],
|
|
301
|
-
|
|
302
|
-
|
|
301
|
+
execute: () => {
|
|
302
|
+
setOpen(false)
|
|
303
|
+
useEditor.getState().setCaptureMode(true)
|
|
304
|
+
},
|
|
303
305
|
},
|
|
304
306
|
|
|
305
307
|
// ── View ─────────────────────────────────────────────────────────────
|
|
@@ -27,15 +27,13 @@ interface CommandPaletteStore {
|
|
|
27
27
|
setInputValue: (value: string) => void
|
|
28
28
|
navigateTo: (page: string) => void
|
|
29
29
|
goBack: () => void
|
|
30
|
-
cameraScope: { nodeId: string; label: string } | null
|
|
31
|
-
setCameraScope: (scope: { nodeId: string; label: string } | null) => void
|
|
32
30
|
}
|
|
33
31
|
|
|
34
32
|
export const useCommandPalette = create<CommandPaletteStore>((set, get) => ({
|
|
35
33
|
open: false,
|
|
36
34
|
setOpen: (open) => {
|
|
37
35
|
set({ open })
|
|
38
|
-
if (!open) set({ pages: [], inputValue: '',
|
|
36
|
+
if (!open) set({ pages: [], inputValue: '', mode: 'command' })
|
|
39
37
|
},
|
|
40
38
|
mode: 'command',
|
|
41
39
|
setMode: (mode) => set({ mode }),
|
|
@@ -44,12 +42,8 @@ export const useCommandPalette = create<CommandPaletteStore>((set, get) => ({
|
|
|
44
42
|
setInputValue: (value) => set({ inputValue: value }),
|
|
45
43
|
navigateTo: (page) => set((s) => ({ pages: [...s.pages, page], inputValue: '' })),
|
|
46
44
|
goBack: () => {
|
|
47
|
-
const { pages } = get()
|
|
48
|
-
if (pages[pages.length - 1] === 'camera-scope') set({ cameraScope: null })
|
|
49
45
|
set((s) => ({ pages: s.pages.slice(0, -1), inputValue: '' }))
|
|
50
46
|
},
|
|
51
|
-
cameraScope: null,
|
|
52
|
-
setCameraScope: (scope) => set({ cameraScope: scope }),
|
|
53
47
|
}))
|
|
54
48
|
|
|
55
49
|
// ---------------------------------------------------------------------------
|
|
@@ -157,8 +151,6 @@ const PAGE_LABEL: Record<string, string> = {
|
|
|
157
151
|
'level-mode': 'Level Mode',
|
|
158
152
|
'rename-level': 'Rename Level',
|
|
159
153
|
'goto-level': 'Go to Level',
|
|
160
|
-
'camera-view': 'Camera Snapshot',
|
|
161
|
-
'camera-scope': '',
|
|
162
154
|
}
|
|
163
155
|
|
|
164
156
|
// ---------------------------------------------------------------------------
|
|
@@ -196,19 +188,8 @@ function EmptyActionItem({ action }: { action: CommandPaletteEmptyAction }) {
|
|
|
196
188
|
// Main component
|
|
197
189
|
// ---------------------------------------------------------------------------
|
|
198
190
|
export function CommandPalette({ emptyAction }: { emptyAction?: CommandPaletteEmptyAction }) {
|
|
199
|
-
const {
|
|
200
|
-
|
|
201
|
-
setOpen,
|
|
202
|
-
mode,
|
|
203
|
-
setMode,
|
|
204
|
-
pages,
|
|
205
|
-
inputValue,
|
|
206
|
-
setInputValue,
|
|
207
|
-
navigateTo,
|
|
208
|
-
goBack,
|
|
209
|
-
cameraScope,
|
|
210
|
-
setCameraScope,
|
|
211
|
-
} = useCommandPalette()
|
|
191
|
+
const { open, setOpen, mode, setMode, pages, inputValue, setInputValue, navigateTo, goBack } =
|
|
192
|
+
useCommandPalette()
|
|
212
193
|
|
|
213
194
|
const [meta, setMeta] = useState('⌘')
|
|
214
195
|
const [isFullscreen, setIsFullscreen] = useState(false)
|
|
@@ -233,11 +214,6 @@ export function CommandPalette({ emptyAction }: { emptyAction?: CommandPaletteEm
|
|
|
233
214
|
),
|
|
234
215
|
)
|
|
235
216
|
|
|
236
|
-
const cameraScopeNode = useScene((s) =>
|
|
237
|
-
cameraScope ? s.nodes[cameraScope.nodeId as AnyNodeId] : null,
|
|
238
|
-
)
|
|
239
|
-
const hasScopeSnapshot = !!(cameraScopeNode as any)?.camera
|
|
240
|
-
|
|
241
217
|
// Platform detection
|
|
242
218
|
useEffect(() => {
|
|
243
219
|
setMeta(/Mac|iPhone|iPad|iPod/.test(navigator.platform) ? '⌘' : 'Ctrl')
|
|
@@ -279,7 +255,6 @@ export function CommandPalette({ emptyAction }: { emptyAction?: CommandPaletteEm
|
|
|
279
255
|
solo: 'Solo',
|
|
280
256
|
}
|
|
281
257
|
|
|
282
|
-
// Camera snapshot helpers (used by sub-pages registered via EditorCommands)
|
|
283
258
|
const confirmRename = () => {
|
|
284
259
|
if (!(activeLevelId && inputValue.trim())) return
|
|
285
260
|
run(() => {
|
|
@@ -287,29 +262,6 @@ export function CommandPalette({ emptyAction }: { emptyAction?: CommandPaletteEm
|
|
|
287
262
|
})
|
|
288
263
|
}
|
|
289
264
|
|
|
290
|
-
const takeSnapshot = () => {
|
|
291
|
-
if (!cameraScope) return
|
|
292
|
-
import('@pascal-app/core').then(({ emitter }) => {
|
|
293
|
-
run(() =>
|
|
294
|
-
emitter.emit('camera-controls:capture', { nodeId: cameraScope.nodeId as AnyNodeId }),
|
|
295
|
-
)
|
|
296
|
-
})
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
const viewSnapshot = () => {
|
|
300
|
-
if (!(cameraScope && hasScopeSnapshot)) return
|
|
301
|
-
import('@pascal-app/core').then(({ emitter }) => {
|
|
302
|
-
run(() => emitter.emit('camera-controls:view', { nodeId: cameraScope.nodeId as AnyNodeId }))
|
|
303
|
-
})
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
const clearSnapshot = () => {
|
|
307
|
-
if (!(cameraScope && hasScopeSnapshot)) return
|
|
308
|
-
run(() => {
|
|
309
|
-
useScene.getState().updateNode(cameraScope.nodeId as AnyNodeId, { camera: undefined } as any)
|
|
310
|
-
})
|
|
311
|
-
}
|
|
312
|
-
|
|
313
265
|
// ---------------------------------------------------------------------------
|
|
314
266
|
// Group registered actions by group (preserving insertion order)
|
|
315
267
|
// ---------------------------------------------------------------------------
|
|
@@ -363,9 +315,7 @@ export function CommandPalette({ emptyAction }: { emptyAction?: CommandPaletteEm
|
|
|
363
315
|
onClick={goBack}
|
|
364
316
|
type="button"
|
|
365
317
|
>
|
|
366
|
-
{page
|
|
367
|
-
? (cameraScope?.label ?? 'Snapshot')
|
|
368
|
-
: (PAGE_LABEL[page] ?? views.get(page)?.label ?? page)}
|
|
318
|
+
{PAGE_LABEL[page] ?? views.get(page)?.label ?? page}
|
|
369
319
|
</button>
|
|
370
320
|
)}
|
|
371
321
|
<Command.Input
|
|
@@ -500,207 +450,6 @@ export function CommandPalette({ emptyAction }: { emptyAction?: CommandPaletteEm
|
|
|
500
450
|
</Command.Item>
|
|
501
451
|
</Command.Group>
|
|
502
452
|
)}
|
|
503
|
-
|
|
504
|
-
{/* ── Camera Snapshot: scope picker ─────────────────────────── */}
|
|
505
|
-
{page === 'camera-view' && (
|
|
506
|
-
<Command.Group heading="Camera Snapshot — Select Scope">
|
|
507
|
-
<OptionItem
|
|
508
|
-
icon={
|
|
509
|
-
<svg
|
|
510
|
-
className="h-4 w-4"
|
|
511
|
-
fill="none"
|
|
512
|
-
stroke="currentColor"
|
|
513
|
-
strokeWidth={2}
|
|
514
|
-
viewBox="0 0 24 24"
|
|
515
|
-
>
|
|
516
|
-
<path d="M3 3h18v18H3z" strokeLinecap="round" strokeLinejoin="round" />
|
|
517
|
-
<path d="M3 9h18M9 21V9" strokeLinecap="round" strokeLinejoin="round" />
|
|
518
|
-
</svg>
|
|
519
|
-
}
|
|
520
|
-
label="Site"
|
|
521
|
-
onSelect={() => {
|
|
522
|
-
const { rootNodeIds } = useScene.getState()
|
|
523
|
-
const siteId = rootNodeIds[0]
|
|
524
|
-
if (siteId) {
|
|
525
|
-
setCameraScope({ nodeId: siteId, label: 'Site' })
|
|
526
|
-
navigateTo('camera-scope')
|
|
527
|
-
}
|
|
528
|
-
}}
|
|
529
|
-
/>
|
|
530
|
-
<OptionItem
|
|
531
|
-
icon={
|
|
532
|
-
<svg
|
|
533
|
-
className="h-4 w-4"
|
|
534
|
-
fill="none"
|
|
535
|
-
stroke="currentColor"
|
|
536
|
-
strokeWidth={2}
|
|
537
|
-
viewBox="0 0 24 24"
|
|
538
|
-
>
|
|
539
|
-
<path
|
|
540
|
-
d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"
|
|
541
|
-
strokeLinecap="round"
|
|
542
|
-
strokeLinejoin="round"
|
|
543
|
-
/>
|
|
544
|
-
<polyline
|
|
545
|
-
points="9 22 9 12 15 12 15 22"
|
|
546
|
-
strokeLinecap="round"
|
|
547
|
-
strokeLinejoin="round"
|
|
548
|
-
/>
|
|
549
|
-
</svg>
|
|
550
|
-
}
|
|
551
|
-
label="Building"
|
|
552
|
-
onSelect={() => {
|
|
553
|
-
const building = Object.values(useScene.getState().nodes).find(
|
|
554
|
-
(n) => n.type === 'building',
|
|
555
|
-
)
|
|
556
|
-
if (building) {
|
|
557
|
-
setCameraScope({ nodeId: building.id, label: 'Building' })
|
|
558
|
-
navigateTo('camera-scope')
|
|
559
|
-
}
|
|
560
|
-
}}
|
|
561
|
-
/>
|
|
562
|
-
<OptionItem
|
|
563
|
-
disabled={!activeLevelId}
|
|
564
|
-
icon={
|
|
565
|
-
<svg
|
|
566
|
-
className="h-4 w-4"
|
|
567
|
-
fill="none"
|
|
568
|
-
stroke="currentColor"
|
|
569
|
-
strokeWidth={2}
|
|
570
|
-
viewBox="0 0 24 24"
|
|
571
|
-
>
|
|
572
|
-
<path
|
|
573
|
-
d="M12 2L2 7l10 5 10-5-10-5z"
|
|
574
|
-
strokeLinecap="round"
|
|
575
|
-
strokeLinejoin="round"
|
|
576
|
-
/>
|
|
577
|
-
<path
|
|
578
|
-
d="M2 17l10 5 10-5M2 12l10 5 10-5"
|
|
579
|
-
strokeLinecap="round"
|
|
580
|
-
strokeLinejoin="round"
|
|
581
|
-
/>
|
|
582
|
-
</svg>
|
|
583
|
-
}
|
|
584
|
-
label="Level"
|
|
585
|
-
onSelect={() => {
|
|
586
|
-
if (activeLevelId) {
|
|
587
|
-
setCameraScope({ nodeId: activeLevelId, label: 'Level' })
|
|
588
|
-
navigateTo('camera-scope')
|
|
589
|
-
}
|
|
590
|
-
}}
|
|
591
|
-
/>
|
|
592
|
-
<OptionItem
|
|
593
|
-
disabled={!useViewer.getState().selection.selectedIds.length}
|
|
594
|
-
icon={
|
|
595
|
-
<svg
|
|
596
|
-
className="h-4 w-4"
|
|
597
|
-
fill="none"
|
|
598
|
-
stroke="currentColor"
|
|
599
|
-
strokeWidth={2}
|
|
600
|
-
viewBox="0 0 24 24"
|
|
601
|
-
>
|
|
602
|
-
<path d="M5 3l14 9-14 9V3z" strokeLinecap="round" strokeLinejoin="round" />
|
|
603
|
-
</svg>
|
|
604
|
-
}
|
|
605
|
-
label="Selection"
|
|
606
|
-
onSelect={() => {
|
|
607
|
-
const firstId = useViewer.getState().selection.selectedIds[0]
|
|
608
|
-
if (firstId) {
|
|
609
|
-
setCameraScope({ nodeId: firstId, label: 'Selection' })
|
|
610
|
-
navigateTo('camera-scope')
|
|
611
|
-
}
|
|
612
|
-
}}
|
|
613
|
-
/>
|
|
614
|
-
</Command.Group>
|
|
615
|
-
)}
|
|
616
|
-
|
|
617
|
-
{/* ── Camera Snapshot: actions for selected scope ───────────── */}
|
|
618
|
-
{page === 'camera-scope' && cameraScope && (
|
|
619
|
-
<Command.Group heading={`${cameraScope.label} Snapshot`}>
|
|
620
|
-
<OptionItem
|
|
621
|
-
icon={
|
|
622
|
-
<svg
|
|
623
|
-
className="h-4 w-4"
|
|
624
|
-
fill="none"
|
|
625
|
-
stroke="currentColor"
|
|
626
|
-
strokeWidth={2}
|
|
627
|
-
viewBox="0 0 24 24"
|
|
628
|
-
>
|
|
629
|
-
<path
|
|
630
|
-
d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"
|
|
631
|
-
strokeLinecap="round"
|
|
632
|
-
strokeLinejoin="round"
|
|
633
|
-
/>
|
|
634
|
-
<circle
|
|
635
|
-
cx="12"
|
|
636
|
-
cy="13"
|
|
637
|
-
r="4"
|
|
638
|
-
strokeLinecap="round"
|
|
639
|
-
strokeLinejoin="round"
|
|
640
|
-
/>
|
|
641
|
-
</svg>
|
|
642
|
-
}
|
|
643
|
-
label={hasScopeSnapshot ? 'Update Snapshot' : 'Take Snapshot'}
|
|
644
|
-
onSelect={takeSnapshot}
|
|
645
|
-
/>
|
|
646
|
-
{hasScopeSnapshot && (
|
|
647
|
-
<OptionItem
|
|
648
|
-
icon={
|
|
649
|
-
<svg
|
|
650
|
-
className="h-4 w-4"
|
|
651
|
-
fill="none"
|
|
652
|
-
stroke="currentColor"
|
|
653
|
-
strokeWidth={2}
|
|
654
|
-
viewBox="0 0 24 24"
|
|
655
|
-
>
|
|
656
|
-
<path
|
|
657
|
-
d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"
|
|
658
|
-
strokeLinecap="round"
|
|
659
|
-
strokeLinejoin="round"
|
|
660
|
-
/>
|
|
661
|
-
<circle
|
|
662
|
-
cx="12"
|
|
663
|
-
cy="12"
|
|
664
|
-
r="3"
|
|
665
|
-
strokeLinecap="round"
|
|
666
|
-
strokeLinejoin="round"
|
|
667
|
-
/>
|
|
668
|
-
</svg>
|
|
669
|
-
}
|
|
670
|
-
label="View Snapshot"
|
|
671
|
-
onSelect={viewSnapshot}
|
|
672
|
-
/>
|
|
673
|
-
)}
|
|
674
|
-
{hasScopeSnapshot && (
|
|
675
|
-
<OptionItem
|
|
676
|
-
icon={
|
|
677
|
-
<svg
|
|
678
|
-
className="h-4 w-4"
|
|
679
|
-
fill="none"
|
|
680
|
-
stroke="currentColor"
|
|
681
|
-
strokeWidth={2}
|
|
682
|
-
viewBox="0 0 24 24"
|
|
683
|
-
>
|
|
684
|
-
<polyline
|
|
685
|
-
points="3 6 5 6 21 6"
|
|
686
|
-
strokeLinecap="round"
|
|
687
|
-
strokeLinejoin="round"
|
|
688
|
-
/>
|
|
689
|
-
<path
|
|
690
|
-
d="M19 6l-1 14H6L5 6"
|
|
691
|
-
strokeLinecap="round"
|
|
692
|
-
strokeLinejoin="round"
|
|
693
|
-
/>
|
|
694
|
-
<path d="M10 11v6M14 11v6" strokeLinecap="round" strokeLinejoin="round" />
|
|
695
|
-
<path d="M9 6V4h6v2" strokeLinecap="round" strokeLinejoin="round" />
|
|
696
|
-
</svg>
|
|
697
|
-
}
|
|
698
|
-
label="Clear Snapshot"
|
|
699
|
-
onSelect={clearSnapshot}
|
|
700
|
-
/>
|
|
701
|
-
)}
|
|
702
|
-
</Command.Group>
|
|
703
|
-
)}
|
|
704
453
|
</Command.List>
|
|
705
454
|
|
|
706
455
|
{/* Footer hint */}
|
|
@@ -4,9 +4,11 @@ import {
|
|
|
4
4
|
getCatalogMaterialById,
|
|
5
5
|
getLibraryMaterialIdFromRef,
|
|
6
6
|
getMaterialsForCategory,
|
|
7
|
+
getMaterialsForTarget,
|
|
7
8
|
MATERIAL_CATEGORIES,
|
|
8
|
-
toLibraryMaterialRef,
|
|
9
9
|
type MaterialSchema,
|
|
10
|
+
type MaterialTarget,
|
|
11
|
+
toLibraryMaterialRef,
|
|
10
12
|
} from '@pascal-app/core'
|
|
11
13
|
import { useEffect, useRef, useState } from 'react'
|
|
12
14
|
import useEditor from '../../../store/use-editor'
|
|
@@ -17,6 +19,8 @@ type MaterialPickerProps = {
|
|
|
17
19
|
onChange?: (material: MaterialSchema) => void
|
|
18
20
|
onSelectMaterialPreset?: (materialPreset: string) => void
|
|
19
21
|
disabled?: boolean
|
|
22
|
+
nodeType?: MaterialTarget
|
|
23
|
+
hideSideControl?: boolean
|
|
20
24
|
}
|
|
21
25
|
|
|
22
26
|
export function MaterialPicker({
|
|
@@ -48,8 +52,7 @@ export function MaterialPicker({
|
|
|
48
52
|
return
|
|
49
53
|
}
|
|
50
54
|
|
|
51
|
-
const catalogId =
|
|
52
|
-
getLibraryMaterialIdFromRef(selectedMaterialPreset) ?? value?.id ?? undefined
|
|
55
|
+
const catalogId = getLibraryMaterialIdFromRef(selectedMaterialPreset) ?? value?.id ?? undefined
|
|
53
56
|
const selectedCatalogEntry = getCatalogMaterialById(catalogId)
|
|
54
57
|
if (selectedCatalogEntry?.category) {
|
|
55
58
|
setSelectedCategory(selectedCatalogEntry.category)
|
|
@@ -177,7 +180,7 @@ export function MaterialPicker({
|
|
|
177
180
|
title={item.label}
|
|
178
181
|
type="button"
|
|
179
182
|
>
|
|
180
|
-
<div className="pointer-events-none absolute inset-0 rounded-[inherit] ring-1 ring-
|
|
183
|
+
<div className="pointer-events-none absolute inset-0 rounded-[inherit] ring-1 ring-white/12 ring-inset" />
|
|
181
184
|
{item.previewThumbnailUrl ? (
|
|
182
185
|
<img
|
|
183
186
|
alt={item.label}
|
|
@@ -193,7 +196,7 @@ export function MaterialPicker({
|
|
|
193
196
|
))}
|
|
194
197
|
{selectedCategory === 'other' && onChange ? (
|
|
195
198
|
<button
|
|
196
|
-
className={`flex h-14 w-14 shrink-0 items-center justify-center rounded-lg border text-[10px]
|
|
199
|
+
className={`flex h-14 w-14 shrink-0 items-center justify-center rounded-lg border font-medium text-[10px] transition-all ${
|
|
197
200
|
showCustom
|
|
198
201
|
? 'border-blue-500 ring-2 ring-blue-500/30'
|
|
199
202
|
: 'border-gray-300 hover:border-gray-400'
|
|
@@ -162,7 +162,7 @@ function LevelRow({
|
|
|
162
162
|
{...dragHandleProps}
|
|
163
163
|
aria-label={`Reorder ${getLevelDisplayLabel(level)}`}
|
|
164
164
|
className={cn(
|
|
165
|
-
'ml-0.5 flex h-6 w-4 shrink-0 touch-none
|
|
165
|
+
'ml-0.5 flex h-6 w-4 shrink-0 cursor-grab touch-none items-center justify-center rounded-md text-muted-foreground/35 opacity-0 transition-colors hover:bg-white/5 hover:text-foreground focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary/50 group-hover/level:opacity-100',
|
|
166
166
|
isDragging && 'cursor-grabbing opacity-100',
|
|
167
167
|
)}
|
|
168
168
|
onClick={(e) => {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
+
import { useIsMobile } from '../../../hooks/use-mobile'
|
|
3
4
|
import useEditor from '../../../store/use-editor'
|
|
4
5
|
import { BuildingHelper } from './building-helper'
|
|
5
6
|
import { CeilingHelper } from './ceiling-helper'
|
|
@@ -12,6 +13,10 @@ export function HelperManager() {
|
|
|
12
13
|
const mode = useEditor((s) => s.mode)
|
|
13
14
|
const tool = useEditor((s) => s.tool)
|
|
14
15
|
const movingNode = useEditor((state) => state.movingNode)
|
|
16
|
+
const isMobile = useIsMobile()
|
|
17
|
+
|
|
18
|
+
// Helpers are keyboard-driven hints (Esc, R, etc.) — irrelevant on touch.
|
|
19
|
+
if (isMobile) return null
|
|
15
20
|
|
|
16
21
|
if (movingNode) {
|
|
17
22
|
if (movingNode.type === 'building') return <BuildingHelper showRotate />
|