@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,4 +1,4 @@
|
|
|
1
|
-
import { emitter, useScene } from '@pascal-app/core'
|
|
1
|
+
import { emitter, useScene, validateBuildJson } from '@pascal-app/core'
|
|
2
2
|
import { useViewer } from '@pascal-app/viewer'
|
|
3
3
|
import { TreeView, VisualJson } from '@visual-json/react'
|
|
4
4
|
import { Camera, Download, Save, Trash2, Upload } from 'lucide-react'
|
|
@@ -21,6 +21,7 @@ import { Switch } from './../../../../../components/ui/primitives/switch'
|
|
|
21
21
|
import useEditor, { selectDefaultBuildingAndLevel } from './../../../../../store/use-editor'
|
|
22
22
|
import { AudioSettingsDialog } from './audio-settings-dialog'
|
|
23
23
|
import { KeyboardShortcutsDialog } from './keyboard-shortcuts-dialog'
|
|
24
|
+
import { LoadBuildDialog, type PendingImport } from './load-build-dialog'
|
|
24
25
|
|
|
25
26
|
type SceneNode = Record<string, unknown> & {
|
|
26
27
|
id?: unknown
|
|
@@ -183,8 +184,10 @@ export function SettingsPanel({
|
|
|
183
184
|
const resetSelection = useViewer((state) => state.resetSelection)
|
|
184
185
|
const exportScene = useViewer((state) => state.exportScene)
|
|
185
186
|
const showGrid = useViewer((state) => state.showGrid)
|
|
187
|
+
const shadows = useViewer((state) => state.shadows)
|
|
186
188
|
const setPhase = useEditor((state) => state.setPhase)
|
|
187
189
|
const [isGeneratingThumbnail, setIsGeneratingThumbnail] = useState(false)
|
|
190
|
+
const [pendingImport, setPendingImport] = useState<PendingImport | null>(null)
|
|
188
191
|
const sceneGraphValue = useMemo(
|
|
189
192
|
() => buildSceneGraphValue(nodes as Record<string, SceneNode>, rootNodeIds),
|
|
190
193
|
[nodes, rootNodeIds],
|
|
@@ -221,16 +224,37 @@ export function SettingsPanel({
|
|
|
221
224
|
|
|
222
225
|
const reader = new FileReader()
|
|
223
226
|
reader.onload = (event) => {
|
|
227
|
+
const text = event.target?.result as string
|
|
228
|
+
let parsed: unknown
|
|
224
229
|
try {
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
230
|
+
parsed = JSON.parse(text)
|
|
231
|
+
} catch {
|
|
232
|
+
setPendingImport({
|
|
233
|
+
fileName: file.name,
|
|
234
|
+
fileSizeBytes: file.size,
|
|
235
|
+
result: {
|
|
236
|
+
ok: false,
|
|
237
|
+
parsed: null,
|
|
238
|
+
stats: { total: 0, byType: {}, unknownTypes: {}, floorAreaM2: 0 },
|
|
239
|
+
errors: [
|
|
240
|
+
{
|
|
241
|
+
severity: 'error',
|
|
242
|
+
code: 'invalid_json',
|
|
243
|
+
message: 'File could not be parsed as JSON.',
|
|
244
|
+
},
|
|
245
|
+
],
|
|
246
|
+
warnings: [],
|
|
247
|
+
schemaIssues: [],
|
|
248
|
+
schemaIssueCount: 0,
|
|
249
|
+
},
|
|
250
|
+
})
|
|
251
|
+
return
|
|
233
252
|
}
|
|
253
|
+
setPendingImport({
|
|
254
|
+
fileName: file.name,
|
|
255
|
+
fileSizeBytes: file.size,
|
|
256
|
+
result: validateBuildJson(parsed),
|
|
257
|
+
})
|
|
234
258
|
}
|
|
235
259
|
reader.readAsText(file)
|
|
236
260
|
|
|
@@ -238,6 +262,16 @@ export function SettingsPanel({
|
|
|
238
262
|
e.target.value = ''
|
|
239
263
|
}
|
|
240
264
|
|
|
265
|
+
const handleConfirmImport = (parsed: { nodes: Record<string, unknown>; rootNodeIds: string[] }) => {
|
|
266
|
+
setScene(
|
|
267
|
+
parsed.nodes as Parameters<typeof setScene>[0],
|
|
268
|
+
parsed.rootNodeIds as Parameters<typeof setScene>[1],
|
|
269
|
+
)
|
|
270
|
+
resetSelection()
|
|
271
|
+
setPhase('site')
|
|
272
|
+
setPendingImport(null)
|
|
273
|
+
}
|
|
274
|
+
|
|
241
275
|
const handleResetToDefault = () => {
|
|
242
276
|
clearScene()
|
|
243
277
|
resetSelection()
|
|
@@ -307,6 +341,16 @@ export function SettingsPanel({
|
|
|
307
341
|
onCheckedChange={(checked) => useViewer.getState().setShowGrid(checked)}
|
|
308
342
|
/>
|
|
309
343
|
</div>
|
|
344
|
+
<div className="flex items-center justify-between">
|
|
345
|
+
<div>
|
|
346
|
+
<div className="font-medium text-sm">Shadows</div>
|
|
347
|
+
<div className="text-muted-foreground text-xs">Cast shadows from lights</div>
|
|
348
|
+
</div>
|
|
349
|
+
<Switch
|
|
350
|
+
checked={shadows}
|
|
351
|
+
onCheckedChange={(checked) => useViewer.getState().setShadows(checked)}
|
|
352
|
+
/>
|
|
353
|
+
</div>
|
|
310
354
|
</div>
|
|
311
355
|
)}
|
|
312
356
|
|
|
@@ -380,6 +424,12 @@ export function SettingsPanel({
|
|
|
380
424
|
ref={fileInputRef}
|
|
381
425
|
type="file"
|
|
382
426
|
/>
|
|
427
|
+
|
|
428
|
+
<LoadBuildDialog
|
|
429
|
+
onCancel={() => setPendingImport(null)}
|
|
430
|
+
onConfirm={handleConfirmImport}
|
|
431
|
+
pending={pendingImport}
|
|
432
|
+
/>
|
|
383
433
|
</div>
|
|
384
434
|
|
|
385
435
|
{/* Audio Section */}
|
|
@@ -37,7 +37,6 @@ const SHORTCUT_CATEGORIES: ShortcutCategory[] = [
|
|
|
37
37
|
{ keys: ['1'], action: 'Switch to Site phase' },
|
|
38
38
|
{ keys: ['2'], action: 'Switch to Structure phase' },
|
|
39
39
|
{ keys: ['3'], action: 'Switch to Furnish phase' },
|
|
40
|
-
{ keys: ['S'], action: 'Switch to Structure layer' },
|
|
41
40
|
{ keys: ['F'], action: 'Switch to Furnish layer' },
|
|
42
41
|
{ keys: ['Z'], action: 'Switch to Zones layer' },
|
|
43
42
|
{
|
|
@@ -56,6 +55,7 @@ const SHORTCUT_CATEGORIES: ShortcutCategory[] = [
|
|
|
56
55
|
shortcuts: [
|
|
57
56
|
{ keys: ['V'], action: 'Switch to Select mode' },
|
|
58
57
|
{ keys: ['B'], action: 'Switch to Build mode' },
|
|
58
|
+
{ keys: ['X'], action: 'Switch to Delete mode' },
|
|
59
59
|
{
|
|
60
60
|
keys: ['Esc'],
|
|
61
61
|
action: 'Cancel the active tool and return to Select mode',
|
|
@@ -100,6 +100,11 @@ const SHORTCUT_CATEGORIES: ShortcutCategory[] = [
|
|
|
100
100
|
{
|
|
101
101
|
title: 'Camera',
|
|
102
102
|
shortcuts: [
|
|
103
|
+
{
|
|
104
|
+
keys: ['W', 'A', 'S', 'D'],
|
|
105
|
+
action: 'Pan camera',
|
|
106
|
+
note: 'Moves in screen space, similar to dragging the camera view.',
|
|
107
|
+
},
|
|
103
108
|
{
|
|
104
109
|
keys: ['Middle click'],
|
|
105
110
|
action: 'Pan camera',
|
|
@@ -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
|
+
})
|