@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.
- package/package.json +12 -13
- package/src/components/editor/alignment-3d-guide-layer.tsx +160 -0
- package/src/components/editor/custom-camera-controls.tsx +755 -25
- package/src/components/editor/editor-layout-mobile.tsx +3 -3
- package/src/components/editor/editor-layout-v2.tsx +62 -54
- package/src/components/editor/export-manager.tsx +32 -3
- package/src/components/editor/first-person/build-collider-world.test.ts +149 -0
- package/src/components/editor/first-person/build-collider-world.ts +203 -87
- package/src/components/editor/first-person/bvh-ecctrl.tsx +127 -62
- package/src/components/editor/first-person-controls.tsx +836 -31
- package/src/components/editor/floating-action-menu.tsx +287 -196
- package/src/components/editor/floating-building-action-menu.tsx +1 -1
- package/src/components/editor/floorplan-panel.tsx +5837 -12006
- package/src/components/editor/grid.tsx +51 -17
- package/src/components/editor/group-move-handle.tsx +316 -0
- package/src/components/editor/group-rotate-handle.tsx +382 -0
- package/src/components/editor/group-transform-shared.test.ts +205 -0
- package/src/components/editor/group-transform-shared.ts +256 -0
- package/src/components/editor/handles/handle-arrow.tsx +523 -0
- package/src/components/editor/handles/use-handle-drag.ts +213 -0
- package/src/components/editor/index.tsx +234 -190
- package/src/components/editor/measurement-pill.tsx +75 -0
- package/src/components/editor/node-arrow-handles.tsx +1453 -0
- package/src/components/editor/selection-manager.tsx +435 -158
- package/src/components/editor/site-edge-labels.tsx +44 -20
- package/src/components/editor/slab-hole-highlights.tsx +466 -0
- package/src/components/editor/snapshot-capture-overlay.tsx +539 -0
- package/src/components/editor/thumbnail-generator.tsx +27 -64
- package/src/components/editor/use-floorplan-background-placement.ts +122 -35
- package/src/components/editor/use-floorplan-hit-testing.ts +24 -0
- package/src/components/editor/use-floorplan-scene-data.ts +14 -2
- package/src/components/editor/wall-measurement-label.tsx +11 -6
- package/src/components/editor/wall-move-side-handles.tsx +855 -0
- package/src/components/editor/wall-opening-highlights.tsx +181 -0
- package/src/components/editor/wall-snap-beacon-layer.tsx +150 -0
- package/src/components/editor-2d/floorplan-action-menu-layer.tsx +6 -1
- package/src/components/editor-2d/floorplan-alignment-guide-layer.tsx +142 -0
- package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +5 -3
- package/src/components/editor-2d/floorplan-registry-action-menu.tsx +227 -0
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +616 -0
- package/src/components/editor-2d/floorplan-render-context.tsx +62 -0
- package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +75 -0
- package/src/components/editor-2d/floorplan-wall-move-ghost-layer.tsx +47 -0
- package/src/components/editor-2d/renderers/floorplan-draft-layer.tsx +6 -1
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +266 -0
- package/src/components/editor-2d/renderers/floorplan-measurements-layer.tsx +108 -61
- package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +59 -0
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +1963 -0
- package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +29 -14
- package/src/components/editor-2d/svg-paths.ts +10 -2
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +30 -9
- package/src/components/systems/ceiling/ceiling-system.tsx +12 -8
- package/src/components/systems/roof/roof-edit-system.tsx +86 -12
- package/src/components/systems/stair/stair-edit-system.tsx +2 -2
- 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/elevator/elevator-defaults.ts +8 -0
- package/src/components/tools/elevator/elevator-tool.tsx +349 -0
- package/src/components/tools/elevator/move-elevator-tool.tsx +252 -0
- package/src/components/tools/fence/fence-drafting.ts +64 -22
- package/src/components/tools/item/move-tool.tsx +40 -107
- package/src/components/tools/item/placement-math.test.ts +10 -0
- package/src/components/tools/item/placement-math.ts +33 -9
- package/src/components/tools/item/placement-strategies.ts +201 -25
- package/src/components/tools/item/placement-types.ts +10 -1
- package/src/components/tools/item/use-draft-node.ts +20 -2
- package/src/components/tools/item/use-placement-coordinator.tsx +700 -299
- package/src/components/tools/registry/move-registry-node-tool.tsx +602 -0
- package/src/components/tools/roof/roof-tool.tsx +108 -12
- package/src/components/tools/select/box-select-state.ts +77 -0
- package/src/components/tools/select/box-select-tool.tsx +324 -543
- package/src/components/tools/select/plane-box-select-tool.tsx +577 -0
- package/src/components/tools/select/screen-rectangle-selection.ts +84 -0
- package/src/components/tools/select/select-candidates.test.ts +109 -0
- package/src/components/tools/select/select-candidates.ts +85 -0
- package/src/components/tools/shared/affordance-dispatch.ts +30 -0
- package/src/components/tools/shared/drag-bounding-box.tsx +171 -0
- package/src/components/tools/shared/floor-stack-preview.ts +25 -0
- package/src/components/tools/shared/fresh-placement-visibility.ts +61 -0
- package/src/components/tools/shared/placement-box-geometry.ts +123 -0
- package/src/components/tools/shared/placement-box.tsx +123 -0
- package/src/components/tools/shared/polygon-editor.tsx +617 -167
- package/src/components/tools/shared/segment-angle.ts +57 -1
- package/src/components/tools/site/site-boundary-editor.tsx +464 -12
- package/src/components/tools/site/site-flag-model.ts +18 -0
- package/src/components/tools/stair/stair-defaults.ts +1 -1
- package/src/components/tools/stair/stair-tool.tsx +269 -35
- package/src/components/tools/tool-manager.tsx +172 -56
- package/src/components/tools/wall/wall-drafting.ts +98 -98
- package/src/components/tools/wall/wall-snap-geometry.test.ts +79 -0
- package/src/components/tools/wall/wall-snap-geometry.ts +270 -0
- package/src/components/tools/zone/zone-tool.tsx +36 -11
- package/src/components/ui/action-menu/action-button.tsx +21 -1
- package/src/components/ui/action-menu/camera-actions.tsx +0 -0
- package/src/components/ui/action-menu/control-modes.tsx +5 -76
- package/src/components/ui/action-menu/furnish-tools.tsx +6 -92
- package/src/components/ui/action-menu/index.tsx +33 -196
- package/src/components/ui/action-menu/structure-tools.tsx +13 -88
- package/src/components/ui/action-menu/view-toggles.tsx +262 -37
- package/src/components/ui/command-palette/editor-commands.tsx +24 -17
- package/src/components/ui/command-palette/index.tsx +6 -256
- package/src/components/ui/controls/material-paint-panel.tsx +83 -0
- package/src/components/ui/controls/material-picker.tsx +17 -35
- package/src/components/ui/controls/metric-control.tsx +33 -11
- package/src/components/ui/floating-level-selector.tsx +48 -11
- package/src/components/ui/helpers/helper-manager.tsx +21 -17
- package/src/components/ui/helpers/registered-tool-helper.tsx +25 -0
- package/src/components/ui/item-catalog/catalog-items.tsx +1743 -315
- package/src/components/ui/item-catalog/item-catalog.tsx +91 -46
- package/src/components/ui/level-duplicate-dialog.tsx +5 -6
- package/src/components/ui/panels/node-display.ts +1 -0
- package/src/components/ui/panels/paint-panel.tsx +66 -41
- package/src/components/ui/panels/panel-manager.tsx +33 -74
- package/src/components/ui/panels/panel-wrapper.tsx +217 -12
- package/src/components/ui/panels/parametric-inspector.tsx +469 -0
- package/src/components/ui/panels/reference-panel.tsx +28 -13
- package/src/components/ui/primitives/dropdown-menu.tsx +12 -8
- package/src/components/ui/primitives/number-input.tsx +1 -1
- package/src/components/ui/primitives/sidebar.tsx +0 -0
- package/src/components/ui/scene-loader.tsx +1 -3
- 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/function-tree-panel.tsx +263 -0
- package/src/components/ui/sidebar/panels/items-panel/index.tsx +404 -0
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +59 -9
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +6 -1
- package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +259 -0
- package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +3 -1
- package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +5 -2
- package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +83 -0
- package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +89 -0
- package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +75 -0
- package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +2 -2
- package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +83 -0
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +8 -12
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +2 -1
- package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +93 -0
- package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +41 -5
- package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +128 -0
- package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +5 -2
- package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +83 -0
- package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +68 -34
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +3 -1
- package/src/components/ui/sidebar/panels/zone-panel/index.tsx +52 -3
- package/src/components/ui/sidebar/tab-bar.tsx +61 -1
- package/src/components/ui/slider.tsx +1 -1
- package/src/components/viewer-overlay.tsx +169 -52
- package/src/components/viewer-zone-system.tsx +1 -1
- package/src/hooks/use-auto-save.ts +24 -3
- package/src/hooks/use-ceiling-events.ts +175 -0
- package/src/hooks/use-drag-action.ts +124 -0
- package/src/hooks/use-keyboard.ts +125 -62
- package/src/hooks/use-selection.ts +64 -0
- package/src/index.tsx +243 -4
- package/src/lib/constants.ts +6 -2
- package/src/lib/editor-api.ts +62 -0
- package/src/lib/elevator-support.ts +96 -0
- package/src/lib/floorplan/apply-alignment.ts +111 -0
- package/src/lib/floorplan/index.ts +7 -0
- package/src/lib/floorplan/selection-tool.ts +40 -0
- package/src/lib/floorplan/stairs.ts +12 -1
- package/src/lib/fresh-planar-placement.test.ts +102 -0
- package/src/lib/fresh-planar-placement.ts +63 -0
- package/src/lib/history.ts +2 -1
- package/src/lib/level-duplication.test.ts +0 -2
- package/src/lib/level-duplication.ts +55 -43
- package/src/lib/level-name.ts +11 -0
- package/src/lib/material-paint.ts +212 -20
- package/src/lib/placement-metadata.ts +29 -0
- package/src/lib/planar-cursor-placement.test.ts +43 -0
- package/src/lib/planar-cursor-placement.ts +42 -0
- package/src/lib/roof-duplication.ts +2 -2
- package/src/lib/scene-bounds.ts +1 -1
- package/src/lib/scene-clipboard.ts +267 -0
- package/src/lib/scene.ts +13 -4
- package/src/lib/sfx-bus.ts +25 -1
- package/src/lib/sfx-player.ts +75 -27
- package/src/lib/site-boundary.ts +1 -0
- package/src/lib/stair-duplication.ts +2 -2
- package/src/lib/stair-levels.test.ts +146 -0
- package/src/lib/stair-levels.ts +177 -0
- package/src/lib/world-grid-snap.ts +199 -0
- package/src/lib/zone-content.ts +130 -0
- package/src/store/use-alignment-guides.ts +21 -0
- package/src/store/use-editor.tsx +342 -91
- package/src/store/use-placement-preview.ts +31 -0
- package/src/store/use-wall-move-ghosts.ts +36 -0
- package/src/store/use-wall-snap-indicator.ts +32 -0
- package/tsconfig.json +2 -1
- package/src/components/editor/preset-thumbnail-generator.tsx +0 -125
- package/src/components/editor-2d/renderers/floorplan-roof-layer.tsx +0 -113
- 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/tools/building/move-building-tool.tsx +0 -157
- package/src/components/tools/ceiling/ceiling-boundary-editor.tsx +0 -43
- package/src/components/tools/ceiling/ceiling-hole-editor.tsx +0 -49
- package/src/components/tools/ceiling/ceiling-tool.tsx +0 -465
- package/src/components/tools/ceiling/move-ceiling-tool.tsx +0 -257
- package/src/components/tools/column/column-tool.tsx +0 -97
- package/src/components/tools/column/move-column-tool.tsx +0 -105
- package/src/components/tools/door/door-math.ts +0 -110
- package/src/components/tools/door/door-tool.tsx +0 -324
- package/src/components/tools/door/move-door-tool.tsx +0 -412
- package/src/components/tools/fence/curve-fence-tool.tsx +0 -179
- package/src/components/tools/fence/fence-tool.tsx +0 -346
- package/src/components/tools/fence/move-fence-endpoint-tool.tsx +0 -438
- package/src/components/tools/fence/move-fence-tool.tsx +0 -298
- package/src/components/tools/item/item-tool.tsx +0 -26
- package/src/components/tools/roof/move-roof-tool.tsx +0 -359
- package/src/components/tools/slab/move-slab-tool.tsx +0 -182
- package/src/components/tools/slab/slab-boundary-editor.tsx +0 -43
- package/src/components/tools/slab/slab-hole-editor.tsx +0 -49
- package/src/components/tools/slab/slab-tool.tsx +0 -322
- package/src/components/tools/spawn/move-spawn-tool.tsx +0 -101
- package/src/components/tools/spawn/spawn-tool.tsx +0 -130
- package/src/components/tools/wall/curve-wall-tool.tsx +0 -176
- package/src/components/tools/wall/move-wall-endpoint-tool.tsx +0 -423
- package/src/components/tools/wall/move-wall-tool.tsx +0 -356
- package/src/components/tools/wall/wall-tool.tsx +0 -332
- package/src/components/tools/window/move-window-tool.tsx +0 -447
- package/src/components/tools/window/window-math.ts +0 -117
- package/src/components/tools/window/window-tool.tsx +0 -332
- package/src/components/ui/helpers/ceiling-helper.tsx +0 -20
- package/src/components/ui/helpers/slab-helper.tsx +0 -20
- package/src/components/ui/helpers/wall-helper.tsx +0 -20
- package/src/components/ui/panels/ceiling-panel.tsx +0 -250
- package/src/components/ui/panels/column-panel.tsx +0 -715
- package/src/components/ui/panels/door-panel.tsx +0 -1292
- package/src/components/ui/panels/fence-panel.tsx +0 -227
- package/src/components/ui/panels/item-panel.tsx +0 -306
- package/src/components/ui/panels/presets/presets-popover.tsx +0 -511
- package/src/components/ui/panels/roof-panel.tsx +0 -230
- package/src/components/ui/panels/roof-segment-panel.tsx +0 -311
- package/src/components/ui/panels/slab-panel.tsx +0 -250
- package/src/components/ui/panels/spawn-panel.tsx +0 -155
- package/src/components/ui/panels/stair-panel.tsx +0 -525
- package/src/components/ui/panels/stair-segment-panel.tsx +0 -325
- package/src/components/ui/panels/wall-panel.tsx +0 -177
- package/src/components/ui/panels/window-panel.tsx +0 -960
- package/src/components/ui/viewer-toolbar.tsx +0 -436
- package/src/contexts/presets-context.tsx +0 -121
- package/src/hooks/use-contextual-tools.ts +0 -61
|
@@ -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,
|
|
@@ -12,6 +13,7 @@ import { useViewer } from '@pascal-app/viewer'
|
|
|
12
13
|
import { Check, ChevronDown, Eye, EyeOff, Layers2, Plus, Trash2 } from 'lucide-react'
|
|
13
14
|
import { useCallback, useRef, useState } from 'react'
|
|
14
15
|
import { useShallow } from 'zustand/react/shallow'
|
|
16
|
+
import { getLevelDisplayName } from '../../../lib/level-name'
|
|
15
17
|
import { createLocalGuideImage } from '../../../lib/local-guide-image'
|
|
16
18
|
import { cn } from '../../../lib/utils'
|
|
17
19
|
import useEditor, { type GridSnapStep } from '../../../store/use-editor'
|
|
@@ -83,10 +85,6 @@ function useLowerReferenceLevels(): LevelNode[] {
|
|
|
83
85
|
)
|
|
84
86
|
}
|
|
85
87
|
|
|
86
|
-
function getLevelDisplayName(level: LevelNode) {
|
|
87
|
-
return level.name || `Level ${level.level}`
|
|
88
|
-
}
|
|
89
|
-
|
|
90
88
|
// ── Shared upload button for dropdowns ──────────────────────────────────────
|
|
91
89
|
|
|
92
90
|
function UploadButton({ onError }: { onError: (message: string | null) => void }) {
|
|
@@ -258,7 +256,7 @@ function GuidesControl() {
|
|
|
258
256
|
|
|
259
257
|
<PopoverContent
|
|
260
258
|
align="center"
|
|
261
|
-
className="w-72 rounded-xl border-border/45 bg-background/96 p-3 shadow-
|
|
259
|
+
className="w-72 rounded-xl border-border/45 bg-background/96 p-3 shadow-elevation-3 backdrop-blur-xl"
|
|
262
260
|
side="top"
|
|
263
261
|
sideOffset={14}
|
|
264
262
|
>
|
|
@@ -353,9 +351,9 @@ function GuidesControl() {
|
|
|
353
351
|
)
|
|
354
352
|
}
|
|
355
353
|
|
|
356
|
-
// ── Grid snap
|
|
354
|
+
// ── Grid snap toggle ────────────────────────────────────────────────────────
|
|
357
355
|
|
|
358
|
-
|
|
356
|
+
function GridSnapControl() {
|
|
359
357
|
const [isOpen, setIsOpen] = useState(false)
|
|
360
358
|
const gridSnapStep = useEditor((state) => state.gridSnapStep)
|
|
361
359
|
const setGridSnapStep = useEditor((state) => state.setGridSnapStep)
|
|
@@ -374,20 +372,7 @@ export function GridSnapControl() {
|
|
|
374
372
|
)}
|
|
375
373
|
type="button"
|
|
376
374
|
>
|
|
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>
|
|
375
|
+
<Icon height={16} icon="lucide:grid-2x2" width={16} />
|
|
391
376
|
<span className="mt-1 font-medium text-[9px] leading-none">
|
|
392
377
|
{formatGridSnapStep(gridSnapStep)}
|
|
393
378
|
</span>
|
|
@@ -510,7 +495,7 @@ function ScansControl() {
|
|
|
510
495
|
|
|
511
496
|
<PopoverContent
|
|
512
497
|
align="center"
|
|
513
|
-
className="w-72 rounded-xl border-border/45 bg-background/96 p-3 shadow-
|
|
498
|
+
className="w-72 rounded-xl border-border/45 bg-background/96 p-3 shadow-elevation-3 backdrop-blur-xl"
|
|
514
499
|
side="top"
|
|
515
500
|
sideOffset={14}
|
|
516
501
|
>
|
|
@@ -605,6 +590,249 @@ function ScansControl() {
|
|
|
605
590
|
)
|
|
606
591
|
}
|
|
607
592
|
|
|
593
|
+
// ── References (merged scans + guides) ──────────────────────────────────────
|
|
594
|
+
// Bottom-bar control that folds the separate Scans and Guides toggles into one
|
|
595
|
+
// "References" split button + a popover holding both, each with its own
|
|
596
|
+
// visibility toggle, upload, and per-item opacity/delete.
|
|
597
|
+
|
|
598
|
+
function ReferenceListSection({
|
|
599
|
+
title,
|
|
600
|
+
iconSrc,
|
|
601
|
+
noun,
|
|
602
|
+
emptyText,
|
|
603
|
+
nodes,
|
|
604
|
+
show,
|
|
605
|
+
setShow,
|
|
606
|
+
onError,
|
|
607
|
+
}: {
|
|
608
|
+
title: string
|
|
609
|
+
iconSrc: string
|
|
610
|
+
noun: string
|
|
611
|
+
emptyText: string
|
|
612
|
+
nodes: (GuideNode | ScanNode)[]
|
|
613
|
+
show: boolean
|
|
614
|
+
setShow: (show: boolean) => void
|
|
615
|
+
onError: (message: string | null) => void
|
|
616
|
+
}) {
|
|
617
|
+
const setSelection = useViewer((state) => state.setSelection)
|
|
618
|
+
const updateNode = useScene((state) => state.updateNode)
|
|
619
|
+
const deleteNode = useScene((state) => state.deleteNode)
|
|
620
|
+
const selectedReferenceId = useEditor((state) => state.selectedReferenceId)
|
|
621
|
+
const setSelectedReferenceId = useEditor((state) => state.setSelectedReferenceId)
|
|
622
|
+
const hasItems = nodes.length > 0
|
|
623
|
+
|
|
624
|
+
const handleSelect = useCallback(
|
|
625
|
+
(id: AnyNodeId) => {
|
|
626
|
+
setShow(true)
|
|
627
|
+
setSelectedReferenceId(id)
|
|
628
|
+
setSelection({ selectedIds: [], zoneId: null })
|
|
629
|
+
},
|
|
630
|
+
[setShow, setSelectedReferenceId, setSelection],
|
|
631
|
+
)
|
|
632
|
+
|
|
633
|
+
return (
|
|
634
|
+
<div className="space-y-2">
|
|
635
|
+
<div className="flex items-center gap-2">
|
|
636
|
+
<span className="flex h-7 w-7 shrink-0 items-center justify-center rounded-lg bg-background/80">
|
|
637
|
+
<img alt="" className="h-4 w-4 object-contain" src={iconSrc} />
|
|
638
|
+
</span>
|
|
639
|
+
<div className="min-w-0 flex-1">
|
|
640
|
+
<p className="font-medium text-foreground text-sm">{title}</p>
|
|
641
|
+
{hasItems && (
|
|
642
|
+
<p className="text-muted-foreground text-xs">
|
|
643
|
+
{nodes.length} {noun}
|
|
644
|
+
{nodes.length !== 1 ? 's' : ''} on this level
|
|
645
|
+
</p>
|
|
646
|
+
)}
|
|
647
|
+
</div>
|
|
648
|
+
<button
|
|
649
|
+
aria-label={show ? `Hide ${title.toLowerCase()}` : `Show ${title.toLowerCase()}`}
|
|
650
|
+
className="flex h-6 w-6 shrink-0 items-center justify-center rounded-full border border-border/40 text-muted-foreground transition-colors hover:bg-white/10 hover:text-foreground"
|
|
651
|
+
onClick={() => setShow(!show)}
|
|
652
|
+
type="button"
|
|
653
|
+
>
|
|
654
|
+
{show ? <Eye className="h-3.5 w-3.5" /> : <EyeOff className="h-3.5 w-3.5" />}
|
|
655
|
+
</button>
|
|
656
|
+
<UploadButton onError={onError} />
|
|
657
|
+
</div>
|
|
658
|
+
|
|
659
|
+
{hasItems ? (
|
|
660
|
+
<div className="max-h-40 space-y-2 overflow-y-auto pr-1">
|
|
661
|
+
{nodes.map((node, index) => (
|
|
662
|
+
<div
|
|
663
|
+
className={cn(
|
|
664
|
+
'group/item space-y-2 rounded-xl border bg-background/75 p-2.5 transition-colors',
|
|
665
|
+
selectedReferenceId === node.id
|
|
666
|
+
? 'border-foreground/35 bg-white/10'
|
|
667
|
+
: 'border-border/45',
|
|
668
|
+
)}
|
|
669
|
+
key={node.id}
|
|
670
|
+
>
|
|
671
|
+
<div className="flex min-w-0 items-center gap-2">
|
|
672
|
+
<button
|
|
673
|
+
className="flex min-w-0 flex-1 items-center gap-2 text-left"
|
|
674
|
+
onClick={() => handleSelect(node.id)}
|
|
675
|
+
type="button"
|
|
676
|
+
>
|
|
677
|
+
<img
|
|
678
|
+
alt=""
|
|
679
|
+
className="h-3.5 w-3.5 shrink-0 object-contain opacity-70"
|
|
680
|
+
src={iconSrc}
|
|
681
|
+
/>
|
|
682
|
+
<p className="truncate font-medium text-foreground text-sm">
|
|
683
|
+
{node.name || `${noun.charAt(0).toUpperCase()}${noun.slice(1)} ${index + 1}`}
|
|
684
|
+
</p>
|
|
685
|
+
{selectedReferenceId === node.id && (
|
|
686
|
+
<Check className="ml-auto h-3.5 w-3.5 shrink-0 text-foreground/80" />
|
|
687
|
+
)}
|
|
688
|
+
</button>
|
|
689
|
+
<button
|
|
690
|
+
aria-label={`Delete ${noun}`}
|
|
691
|
+
className="flex h-5 w-5 shrink-0 items-center justify-center rounded-md text-muted-foreground/50 opacity-0 transition-all hover:bg-destructive/10 hover:text-destructive group-hover/item:opacity-100"
|
|
692
|
+
onClick={(event) => {
|
|
693
|
+
event.stopPropagation()
|
|
694
|
+
deleteNode(node.id)
|
|
695
|
+
if (selectedReferenceId === node.id) {
|
|
696
|
+
setSelectedReferenceId(null)
|
|
697
|
+
}
|
|
698
|
+
}}
|
|
699
|
+
type="button"
|
|
700
|
+
>
|
|
701
|
+
<Trash2 className="h-3 w-3" />
|
|
702
|
+
</button>
|
|
703
|
+
</div>
|
|
704
|
+
<SliderControl
|
|
705
|
+
label="Opacity"
|
|
706
|
+
max={100}
|
|
707
|
+
min={0}
|
|
708
|
+
onChange={(value) =>
|
|
709
|
+
updateNode(node.id, { opacity: Math.round(Math.min(100, Math.max(0, value))) })
|
|
710
|
+
}
|
|
711
|
+
precision={0}
|
|
712
|
+
step={1}
|
|
713
|
+
unit="%"
|
|
714
|
+
value={node.opacity}
|
|
715
|
+
/>
|
|
716
|
+
</div>
|
|
717
|
+
))}
|
|
718
|
+
</div>
|
|
719
|
+
) : (
|
|
720
|
+
<div className="rounded-xl border border-border/45 border-dashed bg-background/60 px-3 py-3 text-muted-foreground text-sm">
|
|
721
|
+
{emptyText}
|
|
722
|
+
</div>
|
|
723
|
+
)}
|
|
724
|
+
</div>
|
|
725
|
+
)
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
function ReferencesControl() {
|
|
729
|
+
const showScans = useViewer((state) => state.showScans)
|
|
730
|
+
const setShowScans = useViewer((state) => state.setShowScans)
|
|
731
|
+
const showGuides = useViewer((state) => state.showGuides)
|
|
732
|
+
const setShowGuides = useViewer((state) => state.setShowGuides)
|
|
733
|
+
const [isOpen, setIsOpen] = useState(false)
|
|
734
|
+
const [uploadError, setUploadError] = useState<string | null>(null)
|
|
735
|
+
|
|
736
|
+
const scans = useLevelScans()
|
|
737
|
+
const guides = useLevelGuides()
|
|
738
|
+
const total = scans.length + guides.length
|
|
739
|
+
const anyVisible = showScans || showGuides
|
|
740
|
+
|
|
741
|
+
const toggleAll = useCallback(() => {
|
|
742
|
+
const next = !anyVisible
|
|
743
|
+
setShowScans(next)
|
|
744
|
+
setShowGuides(next)
|
|
745
|
+
}, [anyVisible, setShowScans, setShowGuides])
|
|
746
|
+
|
|
747
|
+
return (
|
|
748
|
+
<Popover onOpenChange={setIsOpen} open={isOpen}>
|
|
749
|
+
<div className="flex items-center">
|
|
750
|
+
<ActionButton
|
|
751
|
+
className={cn(
|
|
752
|
+
'rounded-r-none p-0',
|
|
753
|
+
anyVisible
|
|
754
|
+
? 'bg-white/15'
|
|
755
|
+
: 'opacity-60 grayscale hover:bg-white/5 hover:opacity-100 hover:grayscale-0',
|
|
756
|
+
)}
|
|
757
|
+
label={`References: ${anyVisible ? 'Visible' : 'Hidden'}`}
|
|
758
|
+
onClick={toggleAll}
|
|
759
|
+
size="icon"
|
|
760
|
+
variant="ghost"
|
|
761
|
+
>
|
|
762
|
+
<div className="relative">
|
|
763
|
+
<img
|
|
764
|
+
alt="References"
|
|
765
|
+
className="h-[28px] w-[28px] object-contain"
|
|
766
|
+
src="/icons/floorplan.png"
|
|
767
|
+
/>
|
|
768
|
+
<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
|
+
{total}
|
|
770
|
+
</span>
|
|
771
|
+
</div>
|
|
772
|
+
</ActionButton>
|
|
773
|
+
|
|
774
|
+
<PopoverTrigger asChild>
|
|
775
|
+
<button
|
|
776
|
+
aria-expanded={isOpen}
|
|
777
|
+
aria-label="Reference settings"
|
|
778
|
+
className={cn(
|
|
779
|
+
'flex h-11 w-6 items-center justify-center rounded-r-lg transition-colors',
|
|
780
|
+
anyVisible
|
|
781
|
+
? isOpen
|
|
782
|
+
? 'bg-white/10'
|
|
783
|
+
: 'bg-white/5 hover:bg-white/8'
|
|
784
|
+
: isOpen
|
|
785
|
+
? 'bg-white/8'
|
|
786
|
+
: 'opacity-60 hover:bg-white/5 hover:opacity-100',
|
|
787
|
+
)}
|
|
788
|
+
type="button"
|
|
789
|
+
>
|
|
790
|
+
<ChevronDown className={cn('h-3 w-3 transition-transform', isOpen && 'rotate-180')} />
|
|
791
|
+
</button>
|
|
792
|
+
</PopoverTrigger>
|
|
793
|
+
</div>
|
|
794
|
+
|
|
795
|
+
<PopoverContent
|
|
796
|
+
align="center"
|
|
797
|
+
className="w-72 rounded-xl border-border/45 bg-background/96 p-3 shadow-elevation-3 backdrop-blur-xl"
|
|
798
|
+
side="top"
|
|
799
|
+
sideOffset={14}
|
|
800
|
+
>
|
|
801
|
+
<div className="space-y-3">
|
|
802
|
+
{uploadError && (
|
|
803
|
+
<div className="rounded-lg border border-destructive/30 bg-destructive/10 px-2.5 py-2 text-destructive text-xs">
|
|
804
|
+
{uploadError}
|
|
805
|
+
</div>
|
|
806
|
+
)}
|
|
807
|
+
<ReferenceListSection
|
|
808
|
+
emptyText="No scans on this level yet."
|
|
809
|
+
iconSrc="/icons/mesh.png"
|
|
810
|
+
nodes={scans}
|
|
811
|
+
noun="scan"
|
|
812
|
+
onError={setUploadError}
|
|
813
|
+
setShow={setShowScans}
|
|
814
|
+
show={showScans}
|
|
815
|
+
title="Scans"
|
|
816
|
+
/>
|
|
817
|
+
<div className="h-px bg-border/45" />
|
|
818
|
+
<ReferenceListSection
|
|
819
|
+
emptyText="No guide images on this level yet."
|
|
820
|
+
iconSrc="/icons/floorplan.png"
|
|
821
|
+
nodes={guides}
|
|
822
|
+
noun="guide image"
|
|
823
|
+
onError={setUploadError}
|
|
824
|
+
setShow={setShowGuides}
|
|
825
|
+
show={showGuides}
|
|
826
|
+
title="Guide images"
|
|
827
|
+
/>
|
|
828
|
+
</div>
|
|
829
|
+
</PopoverContent>
|
|
830
|
+
</Popover>
|
|
831
|
+
)
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
// ── Reference floor control ────────────────────────────────────────────────────────────────────
|
|
835
|
+
|
|
608
836
|
function ReferenceFloorControl() {
|
|
609
837
|
const showReferenceFloor = useEditor((state) => state.showReferenceFloor)
|
|
610
838
|
const toggleReferenceFloor = useEditor((state) => state.toggleReferenceFloor)
|
|
@@ -694,7 +922,11 @@ function ReferenceFloorControl() {
|
|
|
694
922
|
onClick={toggleReferenceFloor}
|
|
695
923
|
type="button"
|
|
696
924
|
>
|
|
697
|
-
{showReferenceFloor ?
|
|
925
|
+
{showReferenceFloor ? (
|
|
926
|
+
<Eye className="h-3.5 w-3.5" />
|
|
927
|
+
) : (
|
|
928
|
+
<EyeOff className="h-3.5 w-3.5" />
|
|
929
|
+
)}
|
|
698
930
|
</button>
|
|
699
931
|
</div>
|
|
700
932
|
|
|
@@ -728,9 +960,7 @@ function ReferenceFloorControl() {
|
|
|
728
960
|
)}
|
|
729
961
|
/>
|
|
730
962
|
<span className="min-w-0 flex-1 truncate">{levelName}</span>
|
|
731
|
-
<span className="text-[10px] text-muted-foreground">
|
|
732
|
-
{index + 1} below
|
|
733
|
-
</span>
|
|
963
|
+
<span className="text-[10px] text-muted-foreground">{index + 1} below</span>
|
|
734
964
|
</button>
|
|
735
965
|
)
|
|
736
966
|
})}
|
|
@@ -757,24 +987,19 @@ function ReferenceFloorControl() {
|
|
|
757
987
|
)
|
|
758
988
|
}
|
|
759
989
|
|
|
760
|
-
// ──
|
|
990
|
+
// ── Exports ─────────────────────────────────────────────────────────────────
|
|
761
991
|
|
|
762
|
-
export
|
|
992
|
+
export { GridSnapControl }
|
|
993
|
+
|
|
994
|
+
export function SecondaryToggles() {
|
|
763
995
|
return (
|
|
764
996
|
<div className="flex items-center gap-1">
|
|
765
|
-
|
|
766
|
-
<ScansControl />
|
|
767
|
-
|
|
768
|
-
{/* Guides (toggle + dropdown) */}
|
|
769
|
-
<GuidesControl />
|
|
770
|
-
|
|
771
|
-
<ReferenceFloorControl />
|
|
997
|
+
<ReferencesControl />
|
|
772
998
|
</div>
|
|
773
999
|
)
|
|
774
1000
|
}
|
|
775
1001
|
|
|
776
|
-
|
|
777
|
-
export function SecondaryToggles() {
|
|
1002
|
+
export function ViewToggles() {
|
|
778
1003
|
return (
|
|
779
1004
|
<div className="flex items-center gap-1">
|
|
780
1005
|
<GridSnapControl />
|
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
PencilLine,
|
|
28
28
|
Plus,
|
|
29
29
|
Redo2,
|
|
30
|
+
Sparkles,
|
|
30
31
|
Square,
|
|
31
32
|
SquareStack,
|
|
32
33
|
Sun,
|
|
@@ -35,8 +36,8 @@ import {
|
|
|
35
36
|
Video,
|
|
36
37
|
} from 'lucide-react'
|
|
37
38
|
import { useEffect } from 'react'
|
|
38
|
-
import { deleteLevelWithFallbackSelection } from '../../../lib/level-selection'
|
|
39
39
|
import { runRedo, runUndo } from '../../../lib/history'
|
|
40
|
+
import { deleteLevelWithFallbackSelection } from '../../../lib/level-selection'
|
|
40
41
|
import { useCommandRegistry } from '../../../store/use-command-registry'
|
|
41
42
|
import type { StructureTool } from '../../../store/use-editor'
|
|
42
43
|
import useEditor from '../../../store/use-editor'
|
|
@@ -190,8 +191,11 @@ export function EditorCommands() {
|
|
|
190
191
|
const { nodes } = useScene.getState()
|
|
191
192
|
const building = Object.values(nodes).find((n) => n.type === 'building')
|
|
192
193
|
if (!building) return
|
|
194
|
+
const levelCount = building.children.filter(
|
|
195
|
+
(childId) => nodes[childId as keyof typeof nodes]?.type === 'level',
|
|
196
|
+
).length
|
|
193
197
|
const newLevel = LevelNode.parse({
|
|
194
|
-
level:
|
|
198
|
+
level: levelCount,
|
|
195
199
|
children: [],
|
|
196
200
|
parentId: building.id,
|
|
197
201
|
})
|
|
@@ -278,28 +282,31 @@ export function EditorCommands() {
|
|
|
278
282
|
}),
|
|
279
283
|
},
|
|
280
284
|
{
|
|
281
|
-
id: 'editor.viewer.
|
|
282
|
-
label:
|
|
283
|
-
const theme = useViewer.getState().theme
|
|
284
|
-
return theme === 'dark' ? 'Switch to Light Theme' : 'Switch to Dark Theme'
|
|
285
|
-
},
|
|
285
|
+
id: 'editor.viewer.shading-solid',
|
|
286
|
+
label: 'Switch to Solid',
|
|
286
287
|
group: 'Viewer Controls',
|
|
287
|
-
icon: <
|
|
288
|
-
keywords: ['
|
|
289
|
-
execute: () =>
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
288
|
+
icon: <Box className="h-4 w-4" />,
|
|
289
|
+
keywords: ['solid', 'shading', 'render', 'mode', 'performance'],
|
|
290
|
+
execute: () => run(() => useViewer.getState().setShading('solid')),
|
|
291
|
+
},
|
|
292
|
+
{
|
|
293
|
+
id: 'editor.viewer.shading-rendered',
|
|
294
|
+
label: 'Switch to Rendered',
|
|
295
|
+
group: 'Viewer Controls',
|
|
296
|
+
icon: <Sparkles className="h-4 w-4" />,
|
|
297
|
+
keywords: ['rendered', 'shading', 'render', 'mode', 'quality'],
|
|
298
|
+
execute: () => run(() => useViewer.getState().setShading('rendered')),
|
|
294
299
|
},
|
|
295
300
|
{
|
|
296
301
|
id: 'editor.viewer.camera-snapshot',
|
|
297
|
-
label: '
|
|
302
|
+
label: 'Take Snapshot',
|
|
298
303
|
group: 'Viewer Controls',
|
|
299
304
|
icon: <Camera className="h-4 w-4" />,
|
|
300
305
|
keywords: ['camera', 'snapshot', 'capture', 'save', 'view', 'bookmark'],
|
|
301
|
-
|
|
302
|
-
|
|
306
|
+
execute: () => {
|
|
307
|
+
setOpen(false)
|
|
308
|
+
useEditor.getState().setCaptureMode(true)
|
|
309
|
+
},
|
|
303
310
|
},
|
|
304
311
|
|
|
305
312
|
// ── View ─────────────────────────────────────────────────────────────
|