@pascal-app/editor 0.8.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 +8 -9
- package/src/components/editor/alignment-3d-guide-layer.tsx +160 -0
- package/src/components/editor/custom-camera-controls.tsx +754 -25
- package/src/components/editor/editor-layout-mobile.tsx +3 -3
- package/src/components/editor/editor-layout-v2.tsx +58 -51
- 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 +201 -83
- package/src/components/editor/first-person/bvh-ecctrl.tsx +8 -8
- package/src/components/editor/first-person-controls.tsx +825 -20
- 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 +5829 -12005
- 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 +178 -149
- 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 +433 -156
- 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 +136 -62
- package/src/components/editor/thumbnail-generator.tsx +9 -3
- package/src/components/editor/use-floorplan-background-placement.ts +119 -32
- 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-geometry-renderer.tsx +266 -0
- package/src/components/editor-2d/renderers/floorplan-measurements-layer.tsx +108 -66
- 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 +24 -9
- package/src/components/editor-2d/svg-paths.ts +10 -2
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +26 -3
- 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 +1 -1
- 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 +41 -105
- package/src/components/tools/item/placement-math.test.ts +10 -0
- package/src/components/tools/item/placement-math.ts +31 -5
- package/src/components/tools/item/placement-strategies.ts +190 -15
- package/src/components/tools/item/placement-types.ts +10 -1
- package/src/components/tools/item/use-draft-node.ts +20 -1
- package/src/components/tools/item/use-placement-coordinator.tsx +694 -191
- 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 -538
- 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 +56 -0
- 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 +16 -6
- package/src/components/ui/action-menu/action-button.tsx +21 -1
- package/src/components/ui/action-menu/control-modes.tsx +5 -82
- package/src/components/ui/action-menu/index.tsx +1 -113
- package/src/components/ui/action-menu/structure-tools.tsx +13 -88
- package/src/components/ui/action-menu/view-toggles.tsx +243 -6
- package/src/components/ui/command-palette/editor-commands.tsx +18 -13
- package/src/components/ui/command-palette/index.tsx +2 -1
- package/src/components/ui/controls/material-paint-panel.tsx +83 -0
- package/src/components/ui/controls/material-picker.tsx +11 -32
- package/src/components/ui/controls/metric-control.tsx +33 -11
- package/src/components/ui/floating-level-selector.tsx +47 -10
- package/src/components/ui/helpers/helper-manager.tsx +16 -17
- package/src/components/ui/helpers/registered-tool-helper.tsx +25 -0
- package/src/components/ui/item-catalog/catalog-items.tsx +1 -0
- package/src/components/ui/item-catalog/item-catalog.tsx +3 -0
- package/src/components/ui/level-duplicate-dialog.tsx +2 -1
- package/src/components/ui/panels/node-display.ts +1 -0
- package/src/components/ui/panels/panel-manager.tsx +35 -47
- package/src/components/ui/panels/panel-wrapper.tsx +217 -12
- package/src/components/ui/panels/parametric-inspector.tsx +469 -0
- package/src/components/ui/primitives/dropdown-menu.tsx +12 -8
- package/src/components/ui/scene-loader.tsx +1 -3
- package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +263 -0
- package/src/components/ui/sidebar/panels/items-panel/index.tsx +82 -8
- 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 +4 -6
- 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 +7 -1
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +68 -36
- package/src/components/ui/sidebar/panels/zone-panel/index.tsx +51 -2
- package/src/components/ui/sidebar/tab-bar.tsx +61 -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 +10 -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 +115 -62
- package/src/hooks/use-selection.ts +64 -0
- package/src/index.tsx +237 -5
- 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.ts +54 -42
- package/src/lib/level-name.ts +11 -0
- package/src/lib/material-paint.ts +211 -19
- 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 +1 -1
- package/src/lib/scene-clipboard.ts +267 -0
- package/src/lib/scene.ts +13 -4
- package/src/lib/sfx-bus.ts +23 -1
- package/src/lib/sfx-player.ts +70 -22
- package/src/lib/site-boundary.ts +1 -0
- 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 +320 -37
- 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/src/components/editor/preset-thumbnail-generator.tsx +0 -125
- package/src/components/editor-2d/renderers/floorplan-roof-layer.tsx +0 -113
- 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 -264
- 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 -178
- package/src/components/tools/fence/fence-tool.tsx +0 -346
- package/src/components/tools/fence/move-fence-endpoint-tool.tsx +0 -441
- package/src/components/tools/fence/move-fence-tool.tsx +0 -302
- package/src/components/tools/item/item-tool.tsx +0 -26
- package/src/components/tools/roof/move-roof-tool.tsx +0 -364
- 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 -178
- package/src/components/tools/wall/move-wall-endpoint-tool.tsx +0 -426
- package/src/components/tools/wall/move-wall-tool.tsx +0 -358
- 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 -249
- package/src/components/ui/panels/column-panel.tsx +0 -759
- package/src/components/ui/panels/door-panel.tsx +0 -1299
- package/src/components/ui/panels/fence-panel.tsx +0 -222
- 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 -280
- 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 -161
- package/src/components/ui/panels/stair-panel.tsx +0 -577
- package/src/components/ui/panels/stair-segment-panel.tsx +0 -325
- package/src/components/ui/panels/wall-panel.tsx +0 -273
- package/src/components/ui/panels/window-panel.tsx +0 -968
- package/src/contexts/presets-context.tsx +0 -121
- package/src/hooks/use-contextual-tools.ts +0 -61
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import type { BuildStats, SchemaIssue, ValidateBuildJsonResult } from '@pascal-app/core'
|
|
2
|
+
import {
|
|
3
|
+
AlertTriangle,
|
|
4
|
+
AppWindow,
|
|
5
|
+
Box,
|
|
6
|
+
Building2,
|
|
7
|
+
CheckCircle2,
|
|
8
|
+
DoorOpen,
|
|
9
|
+
Layers,
|
|
10
|
+
MapPin,
|
|
11
|
+
Scan,
|
|
12
|
+
Square,
|
|
13
|
+
XCircle,
|
|
14
|
+
} from 'lucide-react'
|
|
15
|
+
import { useState } from 'react'
|
|
16
|
+
import { Button } from '../../../../../components/ui/primitives/button'
|
|
17
|
+
import {
|
|
18
|
+
Dialog,
|
|
19
|
+
DialogContent,
|
|
20
|
+
DialogDescription,
|
|
21
|
+
DialogFooter,
|
|
22
|
+
DialogHeader,
|
|
23
|
+
DialogTitle,
|
|
24
|
+
} from '../../../../../components/ui/primitives/dialog'
|
|
25
|
+
|
|
26
|
+
export type PendingImport = {
|
|
27
|
+
fileName: string
|
|
28
|
+
fileSizeBytes: number
|
|
29
|
+
result: ValidateBuildJsonResult
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
type Props = {
|
|
33
|
+
pending: PendingImport | null
|
|
34
|
+
onCancel: () => void
|
|
35
|
+
onConfirm: (parsed: NonNullable<ValidateBuildJsonResult['parsed']>) => void
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
type StatRow = {
|
|
39
|
+
icon: typeof Building2
|
|
40
|
+
label: string
|
|
41
|
+
count: number
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function statsRows(stats: BuildStats): StatRow[] {
|
|
45
|
+
return (
|
|
46
|
+
[
|
|
47
|
+
{ icon: MapPin, label: 'Sites', count: stats.byType.site ?? 0 },
|
|
48
|
+
{ icon: Building2, label: 'Buildings', count: stats.byType.building ?? 0 },
|
|
49
|
+
{ icon: Layers, label: 'Levels', count: stats.byType.level ?? 0 },
|
|
50
|
+
{ icon: Square, label: 'Walls', count: stats.byType.wall ?? 0 },
|
|
51
|
+
{ icon: DoorOpen, label: 'Doors', count: stats.byType.door ?? 0 },
|
|
52
|
+
{ icon: AppWindow, label: 'Windows', count: stats.byType.window ?? 0 },
|
|
53
|
+
{ icon: Box, label: 'Items', count: stats.byType.item ?? 0 },
|
|
54
|
+
{ icon: Square, label: 'Slabs', count: stats.byType.slab ?? 0 },
|
|
55
|
+
{ icon: Square, label: 'Ceilings', count: stats.byType.ceiling ?? 0 },
|
|
56
|
+
{ icon: Square, label: 'Zones', count: stats.byType.zone ?? 0 },
|
|
57
|
+
{ icon: Scan, label: 'Scans', count: stats.byType.scan ?? 0 },
|
|
58
|
+
] satisfies StatRow[]
|
|
59
|
+
).filter((row) => row.count > 0)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function groupSchemaIssuesByType(
|
|
63
|
+
issues: SchemaIssue[],
|
|
64
|
+
): { type: string; issues: SchemaIssue[] }[] {
|
|
65
|
+
const groups = new Map<string, SchemaIssue[]>()
|
|
66
|
+
for (const issue of issues) {
|
|
67
|
+
const existing = groups.get(issue.nodeType)
|
|
68
|
+
if (existing) {
|
|
69
|
+
existing.push(issue)
|
|
70
|
+
} else {
|
|
71
|
+
groups.set(issue.nodeType, [issue])
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return Array.from(groups, ([type, list]) => ({ type, issues: list }))
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function formatFileSize(bytes: number): string {
|
|
78
|
+
if (bytes < 1024) return `${bytes} B`
|
|
79
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
|
|
80
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function formatFloorArea(m2: number): string {
|
|
84
|
+
if (m2 === 0) return '—'
|
|
85
|
+
if (m2 < 10) return `${m2.toFixed(2)} m²`
|
|
86
|
+
return `${m2.toFixed(1)} m²`
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function LoadBuildDialog({ pending, onCancel, onConfirm }: Props) {
|
|
90
|
+
const [showAllWarnings, setShowAllWarnings] = useState(false)
|
|
91
|
+
const [showSchemaIssues, setShowSchemaIssues] = useState(false)
|
|
92
|
+
|
|
93
|
+
if (!pending) return null
|
|
94
|
+
|
|
95
|
+
const { fileName, fileSizeBytes, result } = pending
|
|
96
|
+
const { ok, parsed, stats, errors, warnings, schemaIssues, schemaIssueCount } = result
|
|
97
|
+
const rows = statsRows(stats)
|
|
98
|
+
const visibleWarnings = showAllWarnings ? warnings : warnings.slice(0, 3)
|
|
99
|
+
const hiddenWarningCount = warnings.length - visibleWarnings.length
|
|
100
|
+
const schemaIssuesByType = groupSchemaIssuesByType(schemaIssues)
|
|
101
|
+
|
|
102
|
+
return (
|
|
103
|
+
<Dialog
|
|
104
|
+
open
|
|
105
|
+
onOpenChange={(open) => {
|
|
106
|
+
if (!open) onCancel()
|
|
107
|
+
}}
|
|
108
|
+
>
|
|
109
|
+
<DialogContent className="sm:max-w-lg">
|
|
110
|
+
<DialogHeader>
|
|
111
|
+
<DialogTitle className="flex items-center gap-2">
|
|
112
|
+
{ok ? (
|
|
113
|
+
<CheckCircle2 className="size-5 text-emerald-600" />
|
|
114
|
+
) : (
|
|
115
|
+
<XCircle className="size-5 text-red-600" />
|
|
116
|
+
)}
|
|
117
|
+
{ok ? 'Ready to import' : 'Cannot import this file'}
|
|
118
|
+
</DialogTitle>
|
|
119
|
+
<DialogDescription>
|
|
120
|
+
{fileName} · {formatFileSize(fileSizeBytes)} · {stats.total} node
|
|
121
|
+
{stats.total === 1 ? '' : 's'}
|
|
122
|
+
</DialogDescription>
|
|
123
|
+
</DialogHeader>
|
|
124
|
+
|
|
125
|
+
<div className="max-h-[60vh] space-y-4 overflow-y-auto py-2">
|
|
126
|
+
{errors.length > 0 && (
|
|
127
|
+
<div className="space-y-2 rounded-md border border-red-200 bg-red-50 p-3">
|
|
128
|
+
<div className="flex items-center gap-2 font-medium text-red-800 text-sm">
|
|
129
|
+
<XCircle className="size-4" />
|
|
130
|
+
{errors.length} error{errors.length === 1 ? '' : 's'}
|
|
131
|
+
</div>
|
|
132
|
+
<ul className="space-y-1 text-red-700 text-xs">
|
|
133
|
+
{errors.map((e) => (
|
|
134
|
+
<li key={`${e.code}-${e.nodeId ?? ''}`}>· {e.message}</li>
|
|
135
|
+
))}
|
|
136
|
+
</ul>
|
|
137
|
+
</div>
|
|
138
|
+
)}
|
|
139
|
+
|
|
140
|
+
{stats.total > 0 && (
|
|
141
|
+
<div className="rounded-md border bg-card">
|
|
142
|
+
<div className="border-b px-3 py-2 font-medium text-muted-foreground text-xs uppercase">
|
|
143
|
+
Structure
|
|
144
|
+
</div>
|
|
145
|
+
{rows.length > 0 ? (
|
|
146
|
+
<div>
|
|
147
|
+
{rows.map((row, i) => {
|
|
148
|
+
const Icon = row.icon
|
|
149
|
+
return (
|
|
150
|
+
<div
|
|
151
|
+
className={`flex items-center justify-between px-3 py-2 ${
|
|
152
|
+
i === rows.length - 1 ? '' : 'border-b'
|
|
153
|
+
}`}
|
|
154
|
+
key={row.label}
|
|
155
|
+
>
|
|
156
|
+
<div className="flex items-center gap-2">
|
|
157
|
+
<Icon className="size-4 text-muted-foreground" />
|
|
158
|
+
<span className="text-sm">{row.label}</span>
|
|
159
|
+
</div>
|
|
160
|
+
<span className="font-medium text-sm">{row.count}</span>
|
|
161
|
+
</div>
|
|
162
|
+
)
|
|
163
|
+
})}
|
|
164
|
+
{stats.floorAreaM2 > 0 && (
|
|
165
|
+
<div className="flex items-center justify-between border-t px-3 py-2">
|
|
166
|
+
<span className="text-muted-foreground text-sm">Floor area</span>
|
|
167
|
+
<span className="font-medium text-sm">
|
|
168
|
+
{formatFloorArea(stats.floorAreaM2)}
|
|
169
|
+
</span>
|
|
170
|
+
</div>
|
|
171
|
+
)}
|
|
172
|
+
</div>
|
|
173
|
+
) : (
|
|
174
|
+
<div className="px-3 py-4 text-center text-muted-foreground text-xs">
|
|
175
|
+
The file contains no recognised nodes.
|
|
176
|
+
</div>
|
|
177
|
+
)}
|
|
178
|
+
</div>
|
|
179
|
+
)}
|
|
180
|
+
|
|
181
|
+
{warnings.length > 0 && (
|
|
182
|
+
<div className="space-y-2 rounded-md border border-amber-200 bg-amber-50 p-3">
|
|
183
|
+
<div className="flex items-center gap-2 font-medium text-amber-800 text-sm">
|
|
184
|
+
<AlertTriangle className="size-4" />
|
|
185
|
+
{warnings.length} warning{warnings.length === 1 ? '' : 's'}
|
|
186
|
+
</div>
|
|
187
|
+
<ul className="space-y-1 text-amber-700 text-xs">
|
|
188
|
+
{visibleWarnings.map((w, i) => (
|
|
189
|
+
<li key={`${w.code}-${w.nodeId ?? ''}-${i}`}>· {w.message}</li>
|
|
190
|
+
))}
|
|
191
|
+
</ul>
|
|
192
|
+
{hiddenWarningCount > 0 && (
|
|
193
|
+
<button
|
|
194
|
+
className="text-amber-800 text-xs underline hover:no-underline"
|
|
195
|
+
onClick={() => setShowAllWarnings(true)}
|
|
196
|
+
type="button"
|
|
197
|
+
>
|
|
198
|
+
Show {hiddenWarningCount} more
|
|
199
|
+
</button>
|
|
200
|
+
)}
|
|
201
|
+
</div>
|
|
202
|
+
)}
|
|
203
|
+
|
|
204
|
+
{schemaIssues.length > 0 && (
|
|
205
|
+
<div className="space-y-2 rounded-md border bg-card p-3">
|
|
206
|
+
<button
|
|
207
|
+
className="flex w-full items-center justify-between text-left"
|
|
208
|
+
onClick={() => setShowSchemaIssues((v) => !v)}
|
|
209
|
+
type="button"
|
|
210
|
+
>
|
|
211
|
+
<span className="font-medium text-muted-foreground text-xs uppercase">
|
|
212
|
+
Schema details ({schemaIssueCount} node
|
|
213
|
+
{schemaIssueCount === 1 ? '' : 's'})
|
|
214
|
+
</span>
|
|
215
|
+
<span className="text-muted-foreground text-xs">
|
|
216
|
+
{showSchemaIssues ? 'Hide' : 'Show'}
|
|
217
|
+
</span>
|
|
218
|
+
</button>
|
|
219
|
+
{showSchemaIssues && (
|
|
220
|
+
<div className="space-y-3 pt-1">
|
|
221
|
+
{schemaIssuesByType.map(({ type, issues }) => (
|
|
222
|
+
<div className="space-y-1" key={type}>
|
|
223
|
+
<div className="font-medium text-xs">
|
|
224
|
+
{type} · {issues.length}
|
|
225
|
+
</div>
|
|
226
|
+
<ul className="space-y-0.5 text-muted-foreground text-xs">
|
|
227
|
+
{issues.map((issue) => (
|
|
228
|
+
<li className="font-mono" key={issue.nodeId}>
|
|
229
|
+
<span className="text-foreground">{issue.nodeId}</span>
|
|
230
|
+
{issue.path && <span> · {issue.path}</span>}
|
|
231
|
+
<span> — {issue.message}</span>
|
|
232
|
+
</li>
|
|
233
|
+
))}
|
|
234
|
+
</ul>
|
|
235
|
+
</div>
|
|
236
|
+
))}
|
|
237
|
+
</div>
|
|
238
|
+
)}
|
|
239
|
+
</div>
|
|
240
|
+
)}
|
|
241
|
+
</div>
|
|
242
|
+
|
|
243
|
+
<DialogFooter>
|
|
244
|
+
<Button onClick={onCancel} variant="outline">
|
|
245
|
+
Cancel
|
|
246
|
+
</Button>
|
|
247
|
+
<Button
|
|
248
|
+
disabled={!ok || !parsed}
|
|
249
|
+
onClick={() => {
|
|
250
|
+
if (parsed) onConfirm(parsed)
|
|
251
|
+
}}
|
|
252
|
+
>
|
|
253
|
+
Replace current scene
|
|
254
|
+
</Button>
|
|
255
|
+
</DialogFooter>
|
|
256
|
+
</DialogContent>
|
|
257
|
+
</Dialog>
|
|
258
|
+
)
|
|
259
|
+
}
|
|
@@ -39,8 +39,10 @@ export const BuildingTreeNode = memo(function BuildingTreeNode({
|
|
|
39
39
|
|
|
40
40
|
const handleAddLevel = (e: React.MouseEvent) => {
|
|
41
41
|
e.stopPropagation()
|
|
42
|
+
const nodes = useScene.getState().nodes
|
|
43
|
+
const levelCount = children.filter((childId) => nodes[childId]?.type === 'level').length
|
|
42
44
|
const newLevel = LevelNode.parse({
|
|
43
|
-
level:
|
|
45
|
+
level: levelCount,
|
|
44
46
|
children: [],
|
|
45
47
|
parentId: nodeId,
|
|
46
48
|
})
|
|
@@ -130,8 +130,11 @@ function calculatePolygonArea(polygon: Array<[number, number]>): number {
|
|
|
130
130
|
|
|
131
131
|
for (let i = 0; i < n; i++) {
|
|
132
132
|
const j = (i + 1) % n
|
|
133
|
-
|
|
134
|
-
|
|
133
|
+
const pi = polygon[i]
|
|
134
|
+
const pj = polygon[j]
|
|
135
|
+
if (!(pi && pj)) continue
|
|
136
|
+
area += pi[0] * pj[1]
|
|
137
|
+
area -= pj[0] * pi[1]
|
|
135
138
|
}
|
|
136
139
|
|
|
137
140
|
return Math.abs(area) / 2
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { type AnyNodeId, type ChimneyNode, useScene } from '@pascal-app/core'
|
|
2
|
+
import { useViewer } from '@pascal-app/viewer'
|
|
3
|
+
import Image from 'next/image'
|
|
4
|
+
import { memo, useCallback, useState } from 'react'
|
|
5
|
+
import useEditor from './../../../../../store/use-editor'
|
|
6
|
+
import { InlineRenameInput } from './inline-rename-input'
|
|
7
|
+
import { focusTreeNode, handleTreeSelection, TreeNodeWrapper } from './tree-node'
|
|
8
|
+
import { TreeNodeActions } from './tree-node-actions'
|
|
9
|
+
|
|
10
|
+
interface ChimneyTreeNodeProps {
|
|
11
|
+
nodeId: AnyNodeId
|
|
12
|
+
depth: number
|
|
13
|
+
isLast?: boolean
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const ChimneyTreeNode = memo(function ChimneyTreeNode({
|
|
17
|
+
nodeId,
|
|
18
|
+
depth,
|
|
19
|
+
isLast,
|
|
20
|
+
}: ChimneyTreeNodeProps) {
|
|
21
|
+
const [isEditing, setIsEditing] = useState(false)
|
|
22
|
+
const isVisible = useScene((s) => s.nodes[nodeId]?.visible !== false)
|
|
23
|
+
const node = useScene((s) => s.nodes[nodeId] as ChimneyNode | undefined)
|
|
24
|
+
const isSelected = useViewer((state) => state.selection.selectedIds.includes(nodeId))
|
|
25
|
+
const isHovered = useViewer((state) => state.hoveredId === nodeId)
|
|
26
|
+
const setSelection = useViewer((state) => state.setSelection)
|
|
27
|
+
const setHoveredId = useViewer((state) => state.setHoveredId)
|
|
28
|
+
|
|
29
|
+
const handleClick = useCallback(
|
|
30
|
+
(e: React.MouseEvent) => {
|
|
31
|
+
e.stopPropagation()
|
|
32
|
+
const handled = handleTreeSelection(
|
|
33
|
+
e,
|
|
34
|
+
nodeId,
|
|
35
|
+
useViewer.getState().selection.selectedIds,
|
|
36
|
+
setSelection,
|
|
37
|
+
)
|
|
38
|
+
if (!handled && useEditor.getState().phase === 'furnish') {
|
|
39
|
+
useEditor.getState().setPhase('structure')
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
[nodeId, setSelection],
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
const defaultName = node?.name || 'Chimney'
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<TreeNodeWrapper
|
|
49
|
+
actions={<TreeNodeActions nodeId={nodeId} />}
|
|
50
|
+
depth={depth}
|
|
51
|
+
expanded={false}
|
|
52
|
+
hasChildren={false}
|
|
53
|
+
icon={
|
|
54
|
+
<Image
|
|
55
|
+
alt=""
|
|
56
|
+
className="object-contain opacity-60"
|
|
57
|
+
height={14}
|
|
58
|
+
src="/icons/roof.png"
|
|
59
|
+
width={14}
|
|
60
|
+
/>
|
|
61
|
+
}
|
|
62
|
+
isHovered={isHovered}
|
|
63
|
+
isLast={isLast}
|
|
64
|
+
isSelected={isSelected}
|
|
65
|
+
isVisible={isVisible}
|
|
66
|
+
label={
|
|
67
|
+
<InlineRenameInput
|
|
68
|
+
defaultName={defaultName}
|
|
69
|
+
isEditing={isEditing}
|
|
70
|
+
nodeId={nodeId}
|
|
71
|
+
onStartEditing={() => setIsEditing(true)}
|
|
72
|
+
onStopEditing={() => setIsEditing(false)}
|
|
73
|
+
/>
|
|
74
|
+
}
|
|
75
|
+
nodeId={nodeId}
|
|
76
|
+
onClick={handleClick}
|
|
77
|
+
onDoubleClick={() => focusTreeNode(nodeId)}
|
|
78
|
+
onMouseEnter={() => setHoveredId(nodeId)}
|
|
79
|
+
onMouseLeave={() => setHoveredId(null)}
|
|
80
|
+
onToggle={() => {}}
|
|
81
|
+
/>
|
|
82
|
+
)
|
|
83
|
+
})
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { type AnyNodeId, type DormerNode, useScene } from '@pascal-app/core'
|
|
2
|
+
import { useViewer } from '@pascal-app/viewer'
|
|
3
|
+
import Image from 'next/image'
|
|
4
|
+
import { memo, useCallback, useState } from 'react'
|
|
5
|
+
import useEditor from './../../../../../store/use-editor'
|
|
6
|
+
import { InlineRenameInput } from './inline-rename-input'
|
|
7
|
+
import { focusTreeNode, handleTreeSelection, TreeNodeWrapper } from './tree-node'
|
|
8
|
+
import { TreeNodeActions } from './tree-node-actions'
|
|
9
|
+
|
|
10
|
+
interface DormerTreeNodeProps {
|
|
11
|
+
nodeId: AnyNodeId
|
|
12
|
+
depth: number
|
|
13
|
+
isLast?: boolean
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Sidebar tree-node entry for a dormer. Mirrors `ChimneyTreeNode`
|
|
18
|
+
* exactly — dormers are leaf entries (no children) parented under
|
|
19
|
+
* their host roof segment. The `roof.png` icon matches the rest of
|
|
20
|
+
* the roof-accessory kinds.
|
|
21
|
+
*/
|
|
22
|
+
export const DormerTreeNode = memo(function DormerTreeNode({
|
|
23
|
+
nodeId,
|
|
24
|
+
depth,
|
|
25
|
+
isLast,
|
|
26
|
+
}: DormerTreeNodeProps) {
|
|
27
|
+
const [isEditing, setIsEditing] = useState(false)
|
|
28
|
+
const isVisible = useScene((s) => s.nodes[nodeId]?.visible !== false)
|
|
29
|
+
const node = useScene((s) => s.nodes[nodeId] as DormerNode | undefined)
|
|
30
|
+
const isSelected = useViewer((state) => state.selection.selectedIds.includes(nodeId))
|
|
31
|
+
const isHovered = useViewer((state) => state.hoveredId === nodeId)
|
|
32
|
+
const setSelection = useViewer((state) => state.setSelection)
|
|
33
|
+
const setHoveredId = useViewer((state) => state.setHoveredId)
|
|
34
|
+
|
|
35
|
+
const handleClick = useCallback(
|
|
36
|
+
(e: React.MouseEvent) => {
|
|
37
|
+
e.stopPropagation()
|
|
38
|
+
const handled = handleTreeSelection(
|
|
39
|
+
e,
|
|
40
|
+
nodeId,
|
|
41
|
+
useViewer.getState().selection.selectedIds,
|
|
42
|
+
setSelection,
|
|
43
|
+
)
|
|
44
|
+
if (!handled && useEditor.getState().phase === 'furnish') {
|
|
45
|
+
useEditor.getState().setPhase('structure')
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
[nodeId, setSelection],
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
const defaultName = node?.name || 'Dormer'
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<TreeNodeWrapper
|
|
55
|
+
actions={<TreeNodeActions nodeId={nodeId} />}
|
|
56
|
+
depth={depth}
|
|
57
|
+
expanded={false}
|
|
58
|
+
hasChildren={false}
|
|
59
|
+
icon={
|
|
60
|
+
<Image
|
|
61
|
+
alt=""
|
|
62
|
+
className="object-contain opacity-60"
|
|
63
|
+
height={14}
|
|
64
|
+
src="/icons/roof.png"
|
|
65
|
+
width={14}
|
|
66
|
+
/>
|
|
67
|
+
}
|
|
68
|
+
isHovered={isHovered}
|
|
69
|
+
isLast={isLast}
|
|
70
|
+
isSelected={isSelected}
|
|
71
|
+
isVisible={isVisible}
|
|
72
|
+
label={
|
|
73
|
+
<InlineRenameInput
|
|
74
|
+
defaultName={defaultName}
|
|
75
|
+
isEditing={isEditing}
|
|
76
|
+
nodeId={nodeId}
|
|
77
|
+
onStartEditing={() => setIsEditing(true)}
|
|
78
|
+
onStopEditing={() => setIsEditing(false)}
|
|
79
|
+
/>
|
|
80
|
+
}
|
|
81
|
+
nodeId={nodeId}
|
|
82
|
+
onClick={handleClick}
|
|
83
|
+
onDoubleClick={() => focusTreeNode(nodeId)}
|
|
84
|
+
onMouseEnter={() => setHoveredId(nodeId)}
|
|
85
|
+
onMouseLeave={() => setHoveredId(null)}
|
|
86
|
+
onToggle={() => {}}
|
|
87
|
+
/>
|
|
88
|
+
)
|
|
89
|
+
})
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { type AnyNodeId, type ElevatorNode, useScene } from '@pascal-app/core'
|
|
2
|
+
import { useViewer } from '@pascal-app/viewer'
|
|
3
|
+
import Image from 'next/image'
|
|
4
|
+
import { memo, useCallback, useState } from 'react'
|
|
5
|
+
import useEditor from './../../../../../store/use-editor'
|
|
6
|
+
import { InlineRenameInput } from './inline-rename-input'
|
|
7
|
+
import { focusTreeNode, handleTreeSelection, TreeNodeWrapper } from './tree-node'
|
|
8
|
+
import { TreeNodeActions } from './tree-node-actions'
|
|
9
|
+
|
|
10
|
+
interface ElevatorTreeNodeProps {
|
|
11
|
+
nodeId: AnyNodeId
|
|
12
|
+
depth: number
|
|
13
|
+
isLast?: boolean
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const ElevatorTreeNode = memo(function ElevatorTreeNode({
|
|
17
|
+
nodeId,
|
|
18
|
+
depth,
|
|
19
|
+
isLast,
|
|
20
|
+
}: ElevatorTreeNodeProps) {
|
|
21
|
+
const [isEditing, setIsEditing] = useState(false)
|
|
22
|
+
const isVisible = useScene((s) => s.nodes[nodeId]?.visible !== false)
|
|
23
|
+
const node = useScene((s) => s.nodes[nodeId] as ElevatorNode | undefined)
|
|
24
|
+
const isSelected = useViewer((state) => state.selection.selectedIds.includes(nodeId))
|
|
25
|
+
const isHovered = useViewer((state) => state.hoveredId === nodeId)
|
|
26
|
+
const setSelection = useViewer((state) => state.setSelection)
|
|
27
|
+
const setHoveredId = useViewer((state) => state.setHoveredId)
|
|
28
|
+
|
|
29
|
+
const handleClick = useCallback(
|
|
30
|
+
(event: React.MouseEvent) => {
|
|
31
|
+
event.stopPropagation()
|
|
32
|
+
const handled = handleTreeSelection(
|
|
33
|
+
event,
|
|
34
|
+
nodeId,
|
|
35
|
+
useViewer.getState().selection.selectedIds,
|
|
36
|
+
setSelection,
|
|
37
|
+
)
|
|
38
|
+
if (!handled && useEditor.getState().phase === 'furnish') {
|
|
39
|
+
useEditor.getState().setPhase('structure')
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
[nodeId, setSelection],
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<TreeNodeWrapper
|
|
47
|
+
actions={<TreeNodeActions nodeId={nodeId} />}
|
|
48
|
+
depth={depth}
|
|
49
|
+
expanded={false}
|
|
50
|
+
hasChildren={false}
|
|
51
|
+
icon={
|
|
52
|
+
<Image alt="" className="object-contain" height={14} src="/icons/elevator.png" width={14} />
|
|
53
|
+
}
|
|
54
|
+
isHovered={isHovered}
|
|
55
|
+
isLast={isLast}
|
|
56
|
+
isSelected={isSelected}
|
|
57
|
+
isVisible={isVisible}
|
|
58
|
+
label={
|
|
59
|
+
<InlineRenameInput
|
|
60
|
+
defaultName={node?.name || 'Elevator'}
|
|
61
|
+
isEditing={isEditing}
|
|
62
|
+
nodeId={nodeId}
|
|
63
|
+
onStartEditing={() => setIsEditing(true)}
|
|
64
|
+
onStopEditing={() => setIsEditing(false)}
|
|
65
|
+
/>
|
|
66
|
+
}
|
|
67
|
+
nodeId={nodeId}
|
|
68
|
+
onClick={handleClick}
|
|
69
|
+
onDoubleClick={() => focusTreeNode(nodeId)}
|
|
70
|
+
onMouseEnter={() => setHoveredId(nodeId)}
|
|
71
|
+
onMouseLeave={() => setHoveredId(null)}
|
|
72
|
+
onToggle={() => {}}
|
|
73
|
+
/>
|
|
74
|
+
)
|
|
75
|
+
})
|
|
@@ -38,7 +38,7 @@ export const FenceTreeNode = memo(function FenceTreeNode({
|
|
|
38
38
|
|
|
39
39
|
return (
|
|
40
40
|
<TreeNodeWrapper
|
|
41
|
-
actions={<TreeNodeActions
|
|
41
|
+
actions={<TreeNodeActions nodeId={node.id} />}
|
|
42
42
|
depth={depth}
|
|
43
43
|
expanded={false}
|
|
44
44
|
hasChildren={false}
|
|
@@ -53,7 +53,7 @@ export const FenceTreeNode = memo(function FenceTreeNode({
|
|
|
53
53
|
<InlineRenameInput
|
|
54
54
|
defaultName="Fence"
|
|
55
55
|
isEditing={isEditing}
|
|
56
|
-
|
|
56
|
+
nodeId={node.id}
|
|
57
57
|
onStartEditing={() => setIsEditing(true)}
|
|
58
58
|
onStopEditing={() => setIsEditing(false)}
|
|
59
59
|
/>
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { type AnyNodeId, type GutterNode, useScene } from '@pascal-app/core'
|
|
2
|
+
import { useViewer } from '@pascal-app/viewer'
|
|
3
|
+
import Image from 'next/image'
|
|
4
|
+
import { memo, useCallback, useState } from 'react'
|
|
5
|
+
import useEditor from './../../../../../store/use-editor'
|
|
6
|
+
import { InlineRenameInput } from './inline-rename-input'
|
|
7
|
+
import { focusTreeNode, handleTreeSelection, TreeNodeWrapper } from './tree-node'
|
|
8
|
+
import { TreeNodeActions } from './tree-node-actions'
|
|
9
|
+
|
|
10
|
+
interface GutterTreeNodeProps {
|
|
11
|
+
nodeId: AnyNodeId
|
|
12
|
+
depth: number
|
|
13
|
+
isLast?: boolean
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const GutterTreeNode = memo(function GutterTreeNode({
|
|
17
|
+
nodeId,
|
|
18
|
+
depth,
|
|
19
|
+
isLast,
|
|
20
|
+
}: GutterTreeNodeProps) {
|
|
21
|
+
const [isEditing, setIsEditing] = useState(false)
|
|
22
|
+
const isVisible = useScene((s) => s.nodes[nodeId]?.visible !== false)
|
|
23
|
+
const node = useScene((s) => s.nodes[nodeId] as GutterNode | undefined)
|
|
24
|
+
const isSelected = useViewer((state) => state.selection.selectedIds.includes(nodeId))
|
|
25
|
+
const isHovered = useViewer((state) => state.hoveredId === nodeId)
|
|
26
|
+
const setSelection = useViewer((state) => state.setSelection)
|
|
27
|
+
const setHoveredId = useViewer((state) => state.setHoveredId)
|
|
28
|
+
|
|
29
|
+
const handleClick = useCallback(
|
|
30
|
+
(e: React.MouseEvent) => {
|
|
31
|
+
e.stopPropagation()
|
|
32
|
+
const handled = handleTreeSelection(
|
|
33
|
+
e,
|
|
34
|
+
nodeId,
|
|
35
|
+
useViewer.getState().selection.selectedIds,
|
|
36
|
+
setSelection,
|
|
37
|
+
)
|
|
38
|
+
if (!handled && useEditor.getState().phase === 'furnish') {
|
|
39
|
+
useEditor.getState().setPhase('structure')
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
[nodeId, setSelection],
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
const defaultName = node?.name || 'Gutter'
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<TreeNodeWrapper
|
|
49
|
+
actions={<TreeNodeActions nodeId={nodeId} />}
|
|
50
|
+
depth={depth}
|
|
51
|
+
expanded={false}
|
|
52
|
+
hasChildren={false}
|
|
53
|
+
icon={
|
|
54
|
+
<Image
|
|
55
|
+
alt=""
|
|
56
|
+
className="object-contain opacity-60"
|
|
57
|
+
height={14}
|
|
58
|
+
src="/icons/roof.png"
|
|
59
|
+
width={14}
|
|
60
|
+
/>
|
|
61
|
+
}
|
|
62
|
+
isHovered={isHovered}
|
|
63
|
+
isLast={isLast}
|
|
64
|
+
isSelected={isSelected}
|
|
65
|
+
isVisible={isVisible}
|
|
66
|
+
label={
|
|
67
|
+
<InlineRenameInput
|
|
68
|
+
defaultName={defaultName}
|
|
69
|
+
isEditing={isEditing}
|
|
70
|
+
nodeId={nodeId}
|
|
71
|
+
onStartEditing={() => setIsEditing(true)}
|
|
72
|
+
onStopEditing={() => setIsEditing(false)}
|
|
73
|
+
/>
|
|
74
|
+
}
|
|
75
|
+
nodeId={nodeId}
|
|
76
|
+
onClick={handleClick}
|
|
77
|
+
onDoubleClick={() => focusTreeNode(nodeId)}
|
|
78
|
+
onMouseEnter={() => setHoveredId(nodeId)}
|
|
79
|
+
onMouseLeave={() => setHoveredId(null)}
|
|
80
|
+
onToggle={() => {}}
|
|
81
|
+
/>
|
|
82
|
+
)
|
|
83
|
+
})
|
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
buildLevelDuplicateCreateOps,
|
|
37
37
|
type LevelDuplicatePreset,
|
|
38
38
|
} from './../../../../../lib/level-duplication'
|
|
39
|
+
import { getDefaultLevelName } from './../../../../../lib/level-name'
|
|
39
40
|
import { deleteLevelWithFallbackSelection } from './../../../../../lib/level-selection'
|
|
40
41
|
import { createLocalGuideImage } from './../../../../../lib/local-guide-image'
|
|
41
42
|
import { cn } from './../../../../../lib/utils'
|
|
@@ -723,7 +724,7 @@ const LevelItem = memo(function LevelItem({
|
|
|
723
724
|
src="/icons/level.png"
|
|
724
725
|
/>
|
|
725
726
|
<InlineRenameInput
|
|
726
|
-
defaultName={
|
|
727
|
+
defaultName={getDefaultLevelName(level.level)}
|
|
727
728
|
isEditing={isEditing}
|
|
728
729
|
nodeId={level.id}
|
|
729
730
|
onStartEditing={() => setIsEditing(true)}
|
|
@@ -1541,10 +1542,7 @@ export function SitePanel({ projectId, onUploadAsset, onDeleteAsset }: SitePanel
|
|
|
1541
1542
|
useShallow((s) => {
|
|
1542
1543
|
if (!siteNode) return []
|
|
1543
1544
|
return siteNode.children
|
|
1544
|
-
.map((
|
|
1545
|
-
const id = typeof child === 'string' ? child : child.id
|
|
1546
|
-
return s.nodes[id] as BuildingNode | undefined
|
|
1547
|
-
})
|
|
1545
|
+
.map((childId) => s.nodes[childId as AnyNodeId] as BuildingNode | undefined)
|
|
1548
1546
|
.filter((node): node is BuildingNode => node?.type === 'building')
|
|
1549
1547
|
}),
|
|
1550
1548
|
)
|
|
@@ -1571,7 +1569,7 @@ export function SitePanel({ projectId, onUploadAsset, onDeleteAsset }: SitePanel
|
|
|
1571
1569
|
'h-5 w-5 object-contain transition-all',
|
|
1572
1570
|
phase !== 'site' && 'opacity-60 grayscale',
|
|
1573
1571
|
)}
|
|
1574
|
-
src="/icons/site.png"
|
|
1572
|
+
src="/icons/site-flag.png"
|
|
1575
1573
|
/>
|
|
1576
1574
|
<span className="font-medium text-sm">{siteNode.name || 'Site'}</span>
|
|
1577
1575
|
</div>
|