@pascal-app/editor 1.0.0-beta.5 → 1.0.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/LICENSE +21 -0
- package/bunfig.toml +4 -0
- package/package.json +15 -8
- package/scripts/generate-print-golden-house.ts +89 -0
- package/src/components/editor/bake-exporter.tsx +5 -0
- package/src/components/editor/bake-thumbnail.tsx +4 -1
- package/src/components/editor/camera-dragging-lifecycle.test.ts +41 -0
- package/src/components/editor/camera-dragging-lifecycle.ts +10 -2
- package/src/components/editor/capture-camera-rig.tsx +59 -0
- package/src/components/editor/custom-camera-controls.tsx +20 -11
- package/src/components/editor/editor-layout-mobile.tsx +6 -6
- package/src/components/editor/editor-layout-v2.tsx +9 -2
- package/src/components/editor/export-manager.tsx +175 -33
- package/src/components/editor/first-person/build-collider-world.test.ts +30 -0
- package/src/components/editor/first-person-controls.tsx +180 -12
- package/src/components/editor/floating-action-menu.tsx +55 -51
- package/src/components/editor/floorplan-panel.tsx +99 -15
- package/src/components/editor/grid.tsx +12 -7
- package/src/components/editor/group-actions.ts +34 -50
- package/src/components/editor/group-move-3d.ts +51 -21
- package/src/components/editor/group-transform-shared.test.ts +78 -0
- package/src/components/editor/group-transform-shared.ts +74 -7
- package/src/components/editor/handles/handle-arrow-raycast.test.ts +86 -0
- package/src/components/editor/handles/handle-arrow.tsx +109 -48
- package/src/components/editor/handles/handle-placement.test.ts +111 -0
- package/src/components/editor/handles/handle-placement.ts +69 -0
- package/src/components/editor/handles/linear-resize-drag.ts +78 -0
- package/src/components/editor/handles/resize-snap.test.ts +46 -0
- package/src/components/editor/handles/resize-snap.ts +10 -1
- package/src/components/editor/handles/use-handle-drag.ts +23 -3
- package/src/components/editor/index.tsx +151 -24
- package/src/components/editor/node-action-menu.tsx +14 -1
- package/src/components/editor/node-arrow-handles.tsx +67 -119
- package/src/components/editor/selection-manager.tsx +153 -42
- package/src/components/editor/snapshot-capture-overlay.tsx +298 -39
- package/src/components/editor/snapshot-capture.test.ts +422 -0
- package/src/components/editor/snapshot-capture.ts +145 -0
- package/src/components/editor/thumbnail-generator.tsx +295 -239
- package/src/components/editor/use-floorplan-background-placement.ts +53 -5
- package/src/components/editor/wall-move-side-handles.tsx +299 -7
- package/src/components/editor-2d/floorplan-group-move.tsx +67 -33
- package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +3 -1
- package/src/components/editor-2d/floorplan-registry-action-menu.tsx +12 -1
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +129 -38
- package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +14 -1
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.test.tsx +16 -0
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +2 -0
- package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +52 -3
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +196 -1
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +226 -35
- package/src/components/procedural-items/inspector.tsx +223 -0
- package/src/components/systems/ceiling/ceiling-bracket-batch.test.ts +402 -0
- package/src/components/systems/ceiling/ceiling-bracket-batch.ts +371 -0
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.test.ts +347 -0
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +281 -305
- package/src/components/systems/ceiling/ceiling-system.tsx +3 -1
- package/src/components/systems/roof/roof-edit-system.test.tsx +120 -0
- package/src/components/systems/roof/roof-edit-system.tsx +10 -19
- package/src/components/systems/selection-affordance-manager.tsx +59 -9
- package/src/components/systems/selection-affordance-services.ts +19 -0
- package/src/components/systems/stair/stair-edit-system.tsx +5 -2
- package/src/components/tools/fence/fence-drafting.test.ts +100 -0
- package/src/components/tools/fence/fence-drafting.ts +6 -25
- package/src/components/tools/item/draft-creation.test.ts +12 -0
- package/src/components/tools/item/draft-creation.ts +10 -0
- package/src/components/tools/item/face-host-commit.test.ts +65 -0
- package/src/components/tools/item/face-host-commit.ts +56 -0
- package/src/components/tools/item/face-host-preview.test.ts +77 -0
- package/src/components/tools/item/face-host-preview.ts +82 -0
- package/src/components/tools/item/item-surface-placement.test.ts +175 -0
- package/src/components/tools/item/item-surface-strategy-resolver.test.ts +210 -0
- package/src/components/tools/item/move-tool.tsx +4 -3
- package/src/components/tools/item/placement-math.test.ts +11 -1
- package/src/components/tools/item/placement-math.ts +0 -20
- package/src/components/tools/item/placement-strategies.test.ts +331 -4
- package/src/components/tools/item/placement-strategies.ts +304 -171
- package/src/components/tools/item/placement-surface.test.ts +61 -0
- package/src/components/tools/item/placement-surface.ts +24 -0
- package/src/components/tools/item/placement-types.ts +10 -0
- package/src/components/tools/item/shelf-surface-strategy-resolver.test.ts +257 -0
- package/src/components/tools/item/test-face-host.ts +121 -0
- package/src/components/tools/item/use-draft-node.test.tsx +336 -0
- package/src/components/tools/item/use-draft-node.ts +116 -44
- package/src/components/tools/item/use-placement-coordinator.tsx +672 -256
- package/src/components/tools/registered-tool-install-lifecycle.test.tsx +256 -0
- package/src/components/tools/registry/item-surface-child-frame.test.ts +90 -0
- package/src/components/tools/registry/item-surface-move.test.ts +501 -0
- package/src/components/tools/registry/item-surface-move.ts +441 -0
- package/src/components/tools/registry/item-surface-resolver.test.ts +257 -0
- package/src/components/tools/registry/move-registry-node-tool.test.ts +10 -0
- package/src/components/tools/registry/move-registry-node-tool.tsx +505 -164
- package/src/components/tools/registry-tool-context.tsx +30 -0
- package/src/components/tools/select/box-select-tool.tsx +3 -1
- package/src/components/tools/select/marquee-footprint.test.ts +53 -0
- package/src/components/tools/select/marquee-footprint.ts +29 -0
- package/src/components/tools/shared/placement-box.tsx +57 -7
- package/src/components/tools/shared/placement-dimension-guides.tsx +121 -0
- package/src/components/tools/shared/pointer-support-cap.test.ts +319 -17
- package/src/components/tools/shared/pointer-support-cap.ts +56 -17
- package/src/components/tools/shared/shelf-stickiness.ts +54 -0
- package/src/components/tools/shared/surface-hit.test.ts +56 -0
- package/src/components/tools/shared/surface-hit.ts +23 -0
- package/src/components/tools/shared/surface-rejection.test.tsx +106 -0
- package/src/components/tools/shared/surface-rejection.tsx +29 -0
- package/src/components/tools/site/terrain-sculpt-grid.tsx +9 -5
- package/src/components/tools/site/terrain-sculpt-tool.tsx +1 -1
- package/src/components/tools/stair/stair-tool.tsx +127 -74
- package/src/components/tools/tool-manager.tsx +25 -8
- package/src/components/tools/wall/wall-drafting.test.ts +320 -2
- package/src/components/tools/wall/wall-drafting.ts +23 -114
- package/src/components/tools/zone/zone-tool.tsx +13 -4
- package/src/components/ui/action-menu/control-modes.tsx +9 -6
- package/src/components/ui/action-menu/index.tsx +11 -1
- package/src/components/ui/command-palette/editor-commands.tsx +63 -13
- package/src/components/ui/command-palette/index.tsx +7 -2
- package/src/components/ui/controls/material-paint-panel.tsx +6 -4
- package/src/components/ui/controls/material-picker.tsx +4 -4
- package/src/components/ui/controls/metric-control.tsx +29 -13
- package/src/components/ui/controls/scene-material-list.tsx +6 -5
- package/src/components/ui/controls/segmented-control.tsx +8 -1
- package/src/components/ui/controls/slider-control.tsx +44 -21
- package/src/components/ui/controls/toggle-control.tsx +18 -5
- package/src/components/ui/controls/tool-options-panel.tsx +126 -0
- package/src/components/ui/floating-level-selector.tsx +103 -10
- package/src/components/ui/helpers/contextual-helper-panel.tsx +9 -0
- package/src/components/ui/helpers/helper-manager.tsx +47 -11
- package/src/components/ui/helpers/item-helper.tsx +1 -1
- package/src/components/ui/helpers/registered-tool-helper.tsx +21 -0
- package/src/components/ui/panels/homogeneous-selection.test.ts +80 -0
- package/src/components/ui/panels/homogeneous-selection.ts +47 -0
- package/src/components/ui/panels/mobile-selection-bar.tsx +11 -5
- package/src/components/ui/panels/multi-field-value.test.ts +209 -0
- package/src/components/ui/panels/multi-field-value.ts +163 -0
- package/src/components/ui/panels/multi-height-mode.tsx +185 -0
- package/src/components/ui/panels/multi-parametric-inspector.tsx +266 -0
- package/src/components/ui/panels/multi-selection-panel.tsx +48 -37
- package/src/components/ui/panels/node-display.ts +4 -0
- package/src/components/ui/panels/panel-manager.tsx +77 -3
- package/src/components/ui/panels/parametric-field-control.tsx +173 -0
- package/src/components/ui/panels/parametric-field-utils.ts +18 -0
- package/src/components/ui/panels/parametric-inspector.tsx +35 -164
- package/src/components/ui/panels/reference-panel.tsx +34 -4
- package/src/components/ui/primitives/checkbox.tsx +34 -0
- package/src/components/ui/primitives/shortcut-token.tsx +23 -1
- package/src/components/ui/scene-loader.tsx +35 -0
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +502 -31
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +20 -23
- package/src/components/ui/sidebar/panels/settings-panel/print-export-button.test.ts +167 -0
- package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +128 -0
- package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +8 -1
- package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +19 -5
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +222 -50
- package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +2 -7
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +5 -2
- package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +28 -11
- package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +3 -8
- package/src/components/ui/sidebar/panels/site-panel/tree-node.test.ts +8 -0
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +20 -7
- package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +224 -0
- package/src/components/ui/sidebar/panels/site-panel/zone-membership-checkbox.tsx +22 -0
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +24 -7
- package/src/components/ui/sidebar/tab-bar.tsx +2 -0
- package/src/components/ui/sidebar/use-plugin-panels.tsx +17 -2
- package/src/components/ui/snap-target-badge.tsx +2 -0
- package/src/components/viewer/viewer-scene-header.tsx +5 -1
- package/src/components/viewer/viewer-stage.tsx +4 -1
- package/src/components/viewer/viewer-units-panel.tsx +116 -0
- package/src/components/viewer-overlay.tsx +1 -22
- package/src/components/viewer-zone-system.tsx +6 -2
- package/src/components/walkthrough-hud.tsx +19 -10
- package/src/hooks/use-auto-save.test.ts +73 -1
- package/src/hooks/use-auto-save.ts +76 -5
- package/src/hooks/use-ceiling-events.test.ts +144 -0
- package/src/hooks/use-ceiling-events.ts +5 -1
- package/src/hooks/use-drag-action.ts +17 -3
- package/src/hooks/use-grid-events.test.ts +240 -0
- package/src/hooks/use-grid-events.ts +190 -11
- package/src/hooks/use-keyboard.test.ts +244 -0
- package/src/hooks/use-keyboard.ts +132 -63
- package/src/hooks/use-registered-tool-enabled.ts +20 -0
- package/src/hooks/use-save-shortcut.ts +33 -0
- package/src/index.tsx +87 -4
- package/src/lib/active-placement-surface.test.ts +16 -0
- package/src/lib/active-placement-surface.ts +14 -0
- package/src/lib/ceiling-surface-raycast.test.ts +64 -0
- package/src/lib/ceiling-surface-raycast.ts +12 -0
- package/src/lib/contextual-help-extension.ts +17 -0
- package/src/lib/continuation.test.ts +11 -0
- package/src/lib/continuation.ts +8 -1
- package/src/lib/direct-manipulation.test.ts +132 -0
- package/src/lib/direct-manipulation.ts +38 -1
- package/src/lib/editor-api.ts +6 -3
- package/src/lib/elevation-guides.test.ts +28 -1
- package/src/lib/elevation-guides.ts +22 -7
- package/src/lib/export-texture-utils.test.ts +167 -0
- package/src/lib/export-texture-utils.ts +121 -0
- package/src/lib/export-user-data.test.ts +48 -0
- package/src/lib/export-user-data.ts +69 -0
- package/src/lib/floating-menu-scale.test.ts +29 -0
- package/src/lib/floating-menu-scale.ts +14 -0
- package/src/lib/floorplan/apply-alignment.test.ts +47 -2
- package/src/lib/floorplan/apply-alignment.ts +10 -1
- package/src/lib/floorplan/floorplan-export.test.ts +536 -2
- package/src/lib/floorplan/floorplan-export.tsx +120 -19
- package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +26 -0
- package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +32 -24
- package/src/lib/floorplan/floorplan-readonly.ts +5 -0
- package/src/lib/floorplan-grid-event-point.test.ts +29 -0
- package/src/lib/floorplan-grid-event-point.ts +13 -0
- package/src/lib/fresh-planar-placement.ts +13 -5
- package/src/lib/glb-export-serialize.test.ts +280 -0
- package/src/lib/glb-export.test.ts +1375 -21
- package/src/lib/glb-export.ts +781 -106
- package/src/lib/grid-event-presentation.ts +16 -0
- package/src/lib/history.test.ts +528 -147
- package/src/lib/history.ts +81 -7
- package/src/lib/host-tree-children.test.ts +27 -0
- package/src/lib/host-tree-children.ts +79 -0
- package/src/lib/interaction/hot-set.test.ts +10 -1
- package/src/lib/interaction/hot-set.ts +3 -1
- package/src/lib/interaction/overlay-policy.test.ts +33 -1
- package/src/lib/interaction/overlay-policy.ts +16 -0
- package/src/lib/interaction/registered-drafting.ts +36 -0
- package/src/lib/interaction/scope.ts +31 -0
- package/src/lib/level-duplication.test.ts +73 -0
- package/src/lib/level-duplication.ts +32 -2
- package/src/lib/level-print-export.test.ts +484 -0
- package/src/lib/level-print-export.ts +526 -0
- package/src/lib/linear-display.test.ts +59 -0
- package/src/lib/linear-display.ts +39 -0
- package/src/lib/local-guide-image.test.ts +24 -0
- package/src/lib/local-guide-image.ts +34 -1
- package/src/lib/local-project-presentation-persistence.test.ts +302 -0
- package/src/lib/local-project-presentation-persistence.ts +335 -0
- package/src/lib/material-paint.ts +2 -0
- package/src/lib/model-export.ts +29 -0
- package/src/lib/node-action-movement.ts +21 -0
- package/src/lib/paint-preview-owner.ts +71 -0
- package/src/lib/planar-cursor-placement.test.ts +178 -1
- package/src/lib/planar-cursor-placement.ts +61 -5
- package/src/lib/plugin-panels.test.ts +92 -1
- package/src/lib/plugin-panels.ts +48 -0
- package/src/lib/portable-export.test.ts +455 -0
- package/src/lib/portable-export.ts +1042 -0
- package/src/lib/print-3mf.test.ts +86 -0
- package/src/lib/print-3mf.ts +161 -0
- package/src/lib/print-content-scope.test.ts +107 -0
- package/src/lib/print-content-scope.ts +90 -0
- package/src/lib/print-export.test.ts +294 -0
- package/src/lib/print-export.ts +735 -0
- package/src/lib/print-feature-thickness.test.ts +147 -0
- package/src/lib/print-feature-thickness.ts +180 -0
- package/src/lib/print-golden-house.test-fixture.ts +352 -0
- package/src/lib/print-golden-house.test.ts +249 -0
- package/src/lib/print-roof-solids.test.ts +133 -0
- package/src/lib/print-roof-solids.ts +630 -0
- package/src/lib/print-shell-compiler-baseline.test.ts +419 -0
- package/src/lib/print-shell-compiler-baseline.ts +225 -0
- package/src/lib/print-shell-compiler-manifold-core.ts +379 -0
- package/src/lib/print-shell-compiler-manifold-worker.ts +200 -0
- package/src/lib/print-shell-compiler-manifold.worker.ts +20 -0
- package/src/lib/print-shell-compiler-mesh-data.ts +32 -0
- package/src/lib/print-shell-compiler-protocol.ts +38 -0
- package/src/lib/print-shell-compiler.ts +273 -0
- package/src/lib/print-wall-solids.test.ts +194 -0
- package/src/lib/print-wall-solids.ts +328 -0
- package/src/lib/rigid-plan-svg-transform.test.ts +35 -0
- package/src/lib/rigid-plan-svg-transform.ts +24 -0
- package/src/lib/scene-clipboard.ts +1 -1
- package/src/lib/scene.ts +22 -8
- package/src/lib/selection-routing.test.ts +105 -1
- package/src/lib/selection-routing.ts +19 -0
- package/src/lib/send-to-app.test.ts +108 -0
- package/src/lib/send-to-app.ts +121 -0
- package/src/lib/sfx/movement-tick.ts +37 -0
- package/src/lib/sfx-bus.ts +4 -0
- package/src/lib/sfx-player.ts +8 -0
- package/src/lib/snapping-mode.test.ts +32 -4
- package/src/lib/snapping-mode.ts +11 -7
- package/src/lib/surface-attachment.ts +77 -0
- package/src/lib/units.ts +254 -0
- package/src/lib/usdz-export.ts +65 -0
- package/src/lib/use-linear-display.ts +8 -32
- package/src/lib/walkthrough-pointer-lock.ts +47 -0
- package/src/store/live-draft-preview-stores.test.ts +3 -0
- package/src/store/terrain-sculpt-mode.test.ts +3 -6
- package/src/store/tool-mode.test.ts +87 -0
- package/src/store/use-camera-hint-focus.ts +48 -0
- package/src/store/use-editor.tsx +299 -188
- package/src/store/use-floorplan-draft-preview.ts +9 -0
- package/src/store/use-handle-group.ts +4 -0
- package/src/store/use-interaction-scope.test.ts +23 -1
- package/src/store/use-interaction-scope.ts +63 -2
- package/src/store/use-placement-preview.ts +56 -3
- package/src/store/use-send-to-app.ts +21 -0
- package/src/components/tools/roof/roof-tool.tsx +0 -656
- package/src/components/ui/helpers/roof-helper.tsx +0 -14
|
@@ -11,11 +11,15 @@ import {
|
|
|
11
11
|
useScene,
|
|
12
12
|
type ZoneNode,
|
|
13
13
|
} from '@pascal-app/core'
|
|
14
|
-
import { useViewer } from '@pascal-app/viewer'
|
|
14
|
+
import { markPerfAction, useViewer } from '@pascal-app/viewer'
|
|
15
15
|
import {
|
|
16
16
|
Camera,
|
|
17
17
|
ChevronDown,
|
|
18
|
+
ChevronRight,
|
|
18
19
|
Copy,
|
|
20
|
+
Eye,
|
|
21
|
+
EyeOff,
|
|
22
|
+
Group,
|
|
19
23
|
Loader2,
|
|
20
24
|
MoreHorizontal,
|
|
21
25
|
Pencil,
|
|
@@ -25,7 +29,7 @@ import {
|
|
|
25
29
|
X,
|
|
26
30
|
} from 'lucide-react'
|
|
27
31
|
import { AnimatePresence, LayoutGroup, motion } from 'motion/react'
|
|
28
|
-
import { memo, useEffect, useRef, useState } from 'react'
|
|
32
|
+
import { memo, useEffect, useRef, useState, useSyncExternalStore } from 'react'
|
|
29
33
|
import { useShallow } from 'zustand/react/shallow'
|
|
30
34
|
import { ColorDot } from './../../../../../components/ui/primitives/color-dot'
|
|
31
35
|
import {
|
|
@@ -47,14 +51,17 @@ import {
|
|
|
47
51
|
metersToLinearUnit,
|
|
48
52
|
squareMetersToAreaUnit,
|
|
49
53
|
} from './../../../../../lib/measurements'
|
|
50
|
-
import { createLocalGuideImage } from './../../../../../lib/local-guide-image'
|
|
54
|
+
import { createLocalGuideImage, createLocalScan } from './../../../../../lib/local-guide-image'
|
|
55
|
+
import { editorHostTreeChildrenRegistry } from './../../../../../lib/host-tree-children'
|
|
56
|
+
import { createUnitInBuilding, toggleZoneMembership } from './../../../../../lib/units'
|
|
51
57
|
import { cn } from './../../../../../lib/utils'
|
|
52
58
|
import useEditor from './../../../../../store/use-editor'
|
|
53
59
|
import { useUploadStore } from '../../../../../store/use-upload'
|
|
54
60
|
import { MetricControl } from '../../../controls/metric-control'
|
|
55
61
|
import { LevelDuplicateDialog } from '../../../level-duplicate-dialog'
|
|
56
62
|
import { InlineRenameInput } from './inline-rename-input'
|
|
57
|
-
import {
|
|
63
|
+
import { ZoneMembershipCheckbox } from './zone-membership-checkbox'
|
|
64
|
+
import { focusTreeNode, TreeNode, TreeNodeWrapper } from './tree-node'
|
|
58
65
|
import { TreeNodeDragProvider } from './tree-node-drag'
|
|
59
66
|
|
|
60
67
|
// ============================================================================
|
|
@@ -350,8 +357,25 @@ const ReferenceItem = memo(function ReferenceItem({
|
|
|
350
357
|
handleDelete: (id: string, e: React.MouseEvent) => void
|
|
351
358
|
}) {
|
|
352
359
|
const [isEditing, setIsEditing] = useState(false)
|
|
360
|
+
const [isExpanded, setIsExpanded] = useState(true)
|
|
361
|
+
const updateNode = useScene((state) => state.updateNode)
|
|
362
|
+
const selectedReferenceId = useEditor((state) => state.selectedReferenceId)
|
|
363
|
+
const isCapture = refNode.type === 'scan'
|
|
364
|
+
const isVisible = refNode.visible !== false
|
|
365
|
+
useSyncExternalStore(
|
|
366
|
+
editorHostTreeChildrenRegistry.subscribe,
|
|
367
|
+
editorHostTreeChildrenRegistry.getSnapshot,
|
|
368
|
+
editorHostTreeChildrenRegistry.getSnapshot,
|
|
369
|
+
)
|
|
370
|
+
const hostChildren = isCapture
|
|
371
|
+
? editorHostTreeChildrenRegistry.childrenForKind(refNode.type)
|
|
372
|
+
: undefined
|
|
373
|
+
const hasHostChildren = Boolean(hostChildren?.hasChildren(refNode))
|
|
374
|
+
const HostChildren = hasHostChildren ? hostChildren?.component : undefined
|
|
375
|
+
|
|
353
376
|
const handleSelect = () => {
|
|
354
377
|
setSelectedReferenceId(refNode.id)
|
|
378
|
+
useViewer.getState().setSelection({ selectedIds: [], zoneId: null })
|
|
355
379
|
}
|
|
356
380
|
|
|
357
381
|
const handleDoubleClick = () => {
|
|
@@ -359,53 +383,98 @@ const ReferenceItem = memo(function ReferenceItem({
|
|
|
359
383
|
}
|
|
360
384
|
|
|
361
385
|
return (
|
|
362
|
-
<div
|
|
363
|
-
className="group/ref relative flex h-8 cursor-pointer select-none items-center border-border/50 border-b pr-2 text-xs transition-colors hover:bg-accent/30"
|
|
364
|
-
onClick={handleSelect}
|
|
365
|
-
onDoubleClick={handleDoubleClick}
|
|
366
|
-
>
|
|
386
|
+
<div className="relative flex flex-col">
|
|
367
387
|
<div
|
|
368
388
|
className={cn(
|
|
369
|
-
'pointer-
|
|
370
|
-
|
|
389
|
+
'group/ref relative flex h-8 cursor-pointer select-none items-center border-border/50 border-b pr-2 text-xs transition-colors hover:bg-accent/30',
|
|
390
|
+
selectedReferenceId === refNode.id && 'bg-accent/50 text-foreground',
|
|
391
|
+
!isVisible && 'opacity-50',
|
|
371
392
|
)}
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
393
|
+
onClick={handleSelect}
|
|
394
|
+
onDoubleClick={handleDoubleClick}
|
|
395
|
+
>
|
|
396
|
+
<div
|
|
397
|
+
className={cn(
|
|
398
|
+
'pointer-events-none absolute z-10 w-px bg-border/50',
|
|
399
|
+
isLastRow && !(hasHostChildren && isExpanded) ? 'top-0 bottom-1/2' : 'top-0 bottom-0',
|
|
400
|
+
)}
|
|
401
|
+
style={{ left: 45 }}
|
|
402
|
+
/>
|
|
403
|
+
<div
|
|
404
|
+
className="pointer-events-none absolute top-1/2 z-10 h-px bg-border/50"
|
|
405
|
+
style={{ left: 45, width: 8 }}
|
|
406
|
+
/>
|
|
378
407
|
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
408
|
+
{isCapture ? (
|
|
409
|
+
<button
|
|
410
|
+
className="z-20 ml-[52px] flex h-4 w-4 shrink-0 items-center justify-center"
|
|
411
|
+
onClick={(event) => {
|
|
412
|
+
event.stopPropagation()
|
|
413
|
+
if (hasHostChildren) setIsExpanded((expanded) => !expanded)
|
|
414
|
+
}}
|
|
415
|
+
type="button"
|
|
416
|
+
>
|
|
417
|
+
{hasHostChildren ? (
|
|
418
|
+
<ChevronRight
|
|
419
|
+
className={cn('h-3 w-3 transition-transform', isExpanded && 'rotate-90')}
|
|
420
|
+
/>
|
|
421
|
+
) : null}
|
|
422
|
+
</button>
|
|
423
|
+
) : null}
|
|
424
|
+
|
|
425
|
+
<div
|
|
426
|
+
className={cn(
|
|
427
|
+
'flex h-8 min-w-0 flex-1 cursor-pointer items-center gap-2 py-0 text-muted-foreground group-hover/ref:text-foreground',
|
|
428
|
+
!isCapture && 'pl-[60px]',
|
|
429
|
+
)}
|
|
430
|
+
>
|
|
431
|
+
{isCapture ? (
|
|
432
|
+
<img
|
|
433
|
+
alt="Capture"
|
|
434
|
+
className="h-3.5 w-3.5 shrink-0 object-contain opacity-70 transition-opacity group-hover/ref:opacity-100"
|
|
435
|
+
src="/icons/mesh.webp"
|
|
436
|
+
/>
|
|
437
|
+
) : (
|
|
438
|
+
<img
|
|
439
|
+
alt="Guide"
|
|
440
|
+
className="h-3.5 w-3.5 shrink-0 object-contain opacity-70 transition-opacity group-hover/ref:opacity-100"
|
|
441
|
+
src="/icons/floorplan.webp"
|
|
442
|
+
/>
|
|
443
|
+
)}
|
|
444
|
+
<InlineRenameInput
|
|
445
|
+
defaultName={isCapture ? 'Capture' : 'Guide Image'}
|
|
446
|
+
isEditing={isEditing}
|
|
447
|
+
nodeId={refNode.id}
|
|
448
|
+
onStartEditing={() => setIsEditing(true)}
|
|
449
|
+
onStopEditing={() => setIsEditing(false)}
|
|
391
450
|
/>
|
|
392
|
-
|
|
393
|
-
<InlineRenameInput
|
|
394
|
-
defaultName={refNode.type === 'scan' ? '3D Scan' : 'Guide Image'}
|
|
395
|
-
isEditing={isEditing}
|
|
396
|
-
nodeId={refNode.id}
|
|
397
|
-
onStartEditing={() => setIsEditing(true)}
|
|
398
|
-
onStopEditing={() => setIsEditing(false)}
|
|
399
|
-
/>
|
|
400
|
-
</div>
|
|
451
|
+
</div>
|
|
401
452
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
453
|
+
{isCapture ? (
|
|
454
|
+
<button
|
|
455
|
+
className="z-20 flex h-5 w-5 shrink-0 cursor-pointer items-center justify-center rounded-md text-muted-foreground opacity-0 transition-colors hover:bg-black/5 hover:text-foreground group-hover/ref:opacity-100 dark:hover:bg-white/10"
|
|
456
|
+
onClick={(event) => {
|
|
457
|
+
event.stopPropagation()
|
|
458
|
+
updateNode(refNode.id, { visible: !isVisible })
|
|
459
|
+
}}
|
|
460
|
+
title={isVisible ? 'Hide' : 'Show'}
|
|
461
|
+
type="button"
|
|
462
|
+
>
|
|
463
|
+
{isVisible ? <Eye className="h-3 w-3" /> : <EyeOff className="h-3 w-3" />}
|
|
464
|
+
</button>
|
|
465
|
+
) : null}
|
|
466
|
+
<button
|
|
467
|
+
className="z-20 flex h-5 w-5 shrink-0 cursor-pointer items-center justify-center rounded-md text-muted-foreground opacity-0 transition-colors hover:bg-black/5 hover:text-foreground group-hover/ref:opacity-100 dark:hover:bg-white/10"
|
|
468
|
+
onClick={(e) => handleDelete(refNode.id, e)}
|
|
469
|
+
title="Delete"
|
|
470
|
+
type="button"
|
|
471
|
+
>
|
|
472
|
+
<Trash2 className="h-3 w-3" />
|
|
473
|
+
</button>
|
|
474
|
+
</div>
|
|
475
|
+
{isExpanded && HostChildren ? (
|
|
476
|
+
<HostChildren depth={3} nodeId={refNode.id} parentVisible={isVisible} />
|
|
477
|
+
) : null}
|
|
409
478
|
</div>
|
|
410
479
|
)
|
|
411
480
|
})
|
|
@@ -431,6 +500,7 @@ const LevelReferences = memo(function LevelReferences({
|
|
|
431
500
|
const deleteNode = useScene((s) => s.deleteNode)
|
|
432
501
|
const setSelection = useViewer((s) => s.setSelection)
|
|
433
502
|
const setShowGuides = useViewer((s) => s.setShowGuides)
|
|
503
|
+
const setShowScans = useViewer((s) => s.setShowScans)
|
|
434
504
|
const references = useScene(
|
|
435
505
|
useShallow((s) =>
|
|
436
506
|
Object.values(s.nodes).filter(
|
|
@@ -500,6 +570,23 @@ const LevelReferences = memo(function LevelReferences({
|
|
|
500
570
|
return
|
|
501
571
|
}
|
|
502
572
|
|
|
573
|
+
if (!onUploadAsset) {
|
|
574
|
+
useUploadStore.getState().startUpload(levelId, 'scan', file.name)
|
|
575
|
+
useUploadStore.getState().setStatus(levelId, 'uploading')
|
|
576
|
+
|
|
577
|
+
try {
|
|
578
|
+
const { scan, url } = await createLocalScan({ createNode, file, levelId })
|
|
579
|
+
setShowScans(true)
|
|
580
|
+
setSelectedReferenceId(scan.id)
|
|
581
|
+
setSelection({ selectedIds: [], zoneId: null })
|
|
582
|
+
useUploadStore.getState().setResult(levelId, url)
|
|
583
|
+
window.setTimeout(() => useUploadStore.getState().clearUpload(levelId), 600)
|
|
584
|
+
} catch {
|
|
585
|
+
useUploadStore.getState().setError(levelId, 'Could not add that scan.')
|
|
586
|
+
}
|
|
587
|
+
return
|
|
588
|
+
}
|
|
589
|
+
|
|
503
590
|
if (!projectId) {
|
|
504
591
|
useUploadStore.getState().startUpload(levelId, 'scan', file.name)
|
|
505
592
|
useUploadStore.getState().setError(levelId, 'No active project. Please open a project first.')
|
|
@@ -507,7 +594,7 @@ const LevelReferences = memo(function LevelReferences({
|
|
|
507
594
|
}
|
|
508
595
|
|
|
509
596
|
clearUpload(levelId)
|
|
510
|
-
onUploadAsset
|
|
597
|
+
onUploadAsset(projectId, levelId, file, type)
|
|
511
598
|
}
|
|
512
599
|
|
|
513
600
|
const handleDelete = async (nodeId: string, e: React.MouseEvent) => {
|
|
@@ -643,7 +730,8 @@ const LevelItem = memo(function LevelItem({
|
|
|
643
730
|
? (level.parentId as BuildingNode['id'])
|
|
644
731
|
: undefined
|
|
645
732
|
|
|
646
|
-
const selectLevel = (levelId: LevelNode['id']) => {
|
|
733
|
+
const selectLevel = (levelId: LevelNode['id'], measure = true) => {
|
|
734
|
+
if (measure && selectedLevelId !== levelId) markPerfAction('level-switch', levelId)
|
|
647
735
|
setSelection(buildingId ? { buildingId, levelId } : { levelId })
|
|
648
736
|
}
|
|
649
737
|
|
|
@@ -682,7 +770,7 @@ const LevelItem = memo(function LevelItem({
|
|
|
682
770
|
)
|
|
683
771
|
}
|
|
684
772
|
createNodes(createOps)
|
|
685
|
-
selectLevel(newLevelId as LevelNode['id'])
|
|
773
|
+
selectLevel(newLevelId as LevelNode['id'], false)
|
|
686
774
|
setDuplicateDialogOpen(false)
|
|
687
775
|
}
|
|
688
776
|
|
|
@@ -882,7 +970,7 @@ const LevelItem = memo(function LevelItem({
|
|
|
882
970
|
precision={2}
|
|
883
971
|
step={0.05}
|
|
884
972
|
unit="m"
|
|
885
|
-
value={
|
|
973
|
+
value={(level.baseElevation ?? 0)}
|
|
886
974
|
/>
|
|
887
975
|
</div>
|
|
888
976
|
<LevelReferences
|
|
@@ -993,6 +1081,74 @@ const LevelsSection = memo(function LevelsSection({
|
|
|
993
1081
|
)
|
|
994
1082
|
})
|
|
995
1083
|
|
|
1084
|
+
const UnitsSection = memo(function UnitsSection({
|
|
1085
|
+
buildingId,
|
|
1086
|
+
}: {
|
|
1087
|
+
buildingId: BuildingNode['id']
|
|
1088
|
+
}) {
|
|
1089
|
+
const unitIds = useScene(
|
|
1090
|
+
useShallow((s) => {
|
|
1091
|
+
const building = s.nodes[buildingId] as BuildingNode | undefined
|
|
1092
|
+
return (building?.children ?? []).filter((id) => s.nodes[id]?.type === 'unit')
|
|
1093
|
+
}),
|
|
1094
|
+
)
|
|
1095
|
+
const hasUnits = unitIds.length > 0
|
|
1096
|
+
const [expanded, setExpanded] = useState(hasUnits)
|
|
1097
|
+
|
|
1098
|
+
useEffect(() => {
|
|
1099
|
+
if (hasUnits) setExpanded(true)
|
|
1100
|
+
}, [hasUnits])
|
|
1101
|
+
|
|
1102
|
+
const handleNewUnit = (event: React.MouseEvent) => {
|
|
1103
|
+
event.stopPropagation()
|
|
1104
|
+
createUnitInBuilding(buildingId)
|
|
1105
|
+
setExpanded(true)
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
return (
|
|
1109
|
+
<div className="subtle-scrollbar max-h-72 shrink-0 overflow-y-auto overflow-x-hidden">
|
|
1110
|
+
<TreeNodeWrapper
|
|
1111
|
+
actions={
|
|
1112
|
+
<button
|
|
1113
|
+
className="flex h-6 w-6 cursor-pointer items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-black/5 hover:text-foreground dark:hover:bg-white/10"
|
|
1114
|
+
onClick={handleNewUnit}
|
|
1115
|
+
title="New unit"
|
|
1116
|
+
type="button"
|
|
1117
|
+
>
|
|
1118
|
+
<Plus className="h-3 w-3" />
|
|
1119
|
+
</button>
|
|
1120
|
+
}
|
|
1121
|
+
depth={1}
|
|
1122
|
+
expanded={expanded}
|
|
1123
|
+
hasChildren
|
|
1124
|
+
icon={<Group className="h-3.5 w-3.5" />}
|
|
1125
|
+
label={
|
|
1126
|
+
<span className="flex items-center gap-1.5">
|
|
1127
|
+
Units
|
|
1128
|
+
{hasUnits && <span className="text-muted-foreground text-xs">{unitIds.length}</span>}
|
|
1129
|
+
</span>
|
|
1130
|
+
}
|
|
1131
|
+
onClick={() => setExpanded((value) => !value)}
|
|
1132
|
+
onToggle={() => setExpanded((value) => !value)}
|
|
1133
|
+
>
|
|
1134
|
+
{unitIds.map((unitId) => (
|
|
1135
|
+
<TreeNode depth={2} key={unitId} nodeId={unitId} />
|
|
1136
|
+
))}
|
|
1137
|
+
<TreeNodeWrapper
|
|
1138
|
+
depth={2}
|
|
1139
|
+
expanded={false}
|
|
1140
|
+
hasChildren={false}
|
|
1141
|
+
icon={<Plus className="h-3.5 w-3.5" />}
|
|
1142
|
+
isLast
|
|
1143
|
+
label="New unit"
|
|
1144
|
+
onClick={handleNewUnit}
|
|
1145
|
+
onToggle={() => {}}
|
|
1146
|
+
/>
|
|
1147
|
+
</TreeNodeWrapper>
|
|
1148
|
+
</div>
|
|
1149
|
+
)
|
|
1150
|
+
})
|
|
1151
|
+
|
|
996
1152
|
const LayerToggle = memo(function LayerToggle() {
|
|
997
1153
|
const structureLayer = useEditor((state) => state.structureLayer)
|
|
998
1154
|
const setStructureLayer = useEditor((state) => state.setStructureLayer)
|
|
@@ -1128,6 +1284,11 @@ const ZoneItem = memo(function ZoneItem({ zone, isLast }: { zone: ZoneNode; isLa
|
|
|
1128
1284
|
const [cameraPopoverOpen, setCameraPopoverOpen] = useState(false)
|
|
1129
1285
|
const deleteNode = useScene((state) => state.deleteNode)
|
|
1130
1286
|
const updateNode = useScene((state) => state.updateNode)
|
|
1287
|
+
const focusedUnitId = useViewer((state) => state.focusedUnitId)
|
|
1288
|
+
const focusedUnit = useScene((s) => {
|
|
1289
|
+
const unit = focusedUnitId ? s.nodes[focusedUnitId] : undefined
|
|
1290
|
+
return unit?.type === 'unit' ? unit : null
|
|
1291
|
+
})
|
|
1131
1292
|
const selectedZoneId = useViewer((state) => state.selection.zoneId)
|
|
1132
1293
|
const hoveredId = useViewer((state) => state.hoveredId)
|
|
1133
1294
|
const setSelection = useViewer((state) => state.setSelection)
|
|
@@ -1201,6 +1362,13 @@ const ZoneItem = memo(function ZoneItem({ zone, isLast }: { zone: ZoneNode; isLa
|
|
|
1201
1362
|
style={{ left: 8, width: 4 }}
|
|
1202
1363
|
/>
|
|
1203
1364
|
|
|
1365
|
+
{focusedUnit && (
|
|
1366
|
+
<ZoneMembershipCheckbox
|
|
1367
|
+
checked={focusedUnit.members.includes(zone.id)}
|
|
1368
|
+
onToggle={() => toggleZoneMembership(focusedUnit.id, zone.id)}
|
|
1369
|
+
unitName={focusedUnit.name || 'Unit'}
|
|
1370
|
+
/>
|
|
1371
|
+
)}
|
|
1204
1372
|
<span className={cn('mr-2', !isSelected && 'opacity-40')}>
|
|
1205
1373
|
<ColorDot color={zone.color} onChange={handleColorChange} />
|
|
1206
1374
|
</span>
|
|
@@ -1332,7 +1500,10 @@ const ContentSection = memo(function ContentSection() {
|
|
|
1332
1500
|
if (!selectedLevelId) return []
|
|
1333
1501
|
const lvl = s.nodes[selectedLevelId] as LevelNode | undefined
|
|
1334
1502
|
if (!lvl) return []
|
|
1335
|
-
return lvl.children.filter((childId) =>
|
|
1503
|
+
return lvl.children.filter((childId) => {
|
|
1504
|
+
const type = s.nodes[childId]?.type
|
|
1505
|
+
return type !== 'zone' && type !== 'scan'
|
|
1506
|
+
})
|
|
1336
1507
|
}),
|
|
1337
1508
|
)
|
|
1338
1509
|
|
|
@@ -1541,6 +1712,7 @@ const BuildingItem = memo(function BuildingItem({
|
|
|
1541
1712
|
onUploadAsset={onUploadAsset}
|
|
1542
1713
|
projectId={projectId}
|
|
1543
1714
|
/>
|
|
1715
|
+
<UnitsSection buildingId={building.id} />
|
|
1544
1716
|
<LayerToggle />
|
|
1545
1717
|
</div>
|
|
1546
1718
|
<div className="subtle-scrollbar relative min-h-0 flex-1 overflow-y-auto overflow-x-hidden">
|
|
@@ -70,12 +70,7 @@ export const ItemTreeNode = memo(function ItemTreeNode({
|
|
|
70
70
|
const handleClick = useCallback(
|
|
71
71
|
(e: React.MouseEvent) => {
|
|
72
72
|
e.stopPropagation()
|
|
73
|
-
handleTreeSelection(
|
|
74
|
-
e,
|
|
75
|
-
nodeId,
|
|
76
|
-
useViewer.getState().selection.selectedIds,
|
|
77
|
-
setSelection,
|
|
78
|
-
)
|
|
73
|
+
handleTreeSelection(e, nodeId, useViewer.getState().selection.selectedIds, setSelection)
|
|
79
74
|
routeTreeSelectionToNode(node)
|
|
80
75
|
},
|
|
81
76
|
[node, nodeId, setSelection],
|
|
@@ -134,7 +129,7 @@ export const ItemTreeNode = memo(function ItemTreeNode({
|
|
|
134
129
|
depth={depth + 1}
|
|
135
130
|
isLast={index === children.length - 1}
|
|
136
131
|
key={childId}
|
|
137
|
-
nodeId={childId}
|
|
132
|
+
nodeId={childId as AnyNodeId}
|
|
138
133
|
/>
|
|
139
134
|
))}
|
|
140
135
|
</TreeNodeWrapper>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type LevelNode, useScene } from '@pascal-app/core'
|
|
2
|
-
import { useViewer } from '@pascal-app/viewer'
|
|
2
|
+
import { markPerfAction, useViewer } from '@pascal-app/viewer'
|
|
3
3
|
import { Layers } from 'lucide-react'
|
|
4
4
|
import { memo, useCallback, useState } from 'react'
|
|
5
5
|
import { useShallow } from 'zustand/react/shallow'
|
|
@@ -30,7 +30,10 @@ export const LevelTreeNode = memo(function LevelTreeNode({
|
|
|
30
30
|
const isHovered = useViewer((state) => state.hoveredId === nodeId)
|
|
31
31
|
const setSelection = useViewer((state) => state.setSelection)
|
|
32
32
|
|
|
33
|
-
const handleClick = useCallback(() =>
|
|
33
|
+
const handleClick = useCallback(() => {
|
|
34
|
+
if (!isSelected) markPerfAction('level-switch', nodeId)
|
|
35
|
+
setSelection({ levelId: nodeId })
|
|
36
|
+
}, [isSelected, nodeId, setSelection])
|
|
34
37
|
const handleDoubleClick = useCallback(() => focusTreeNode(nodeId), [nodeId])
|
|
35
38
|
const handleToggle = useCallback(() => setExpanded((prev) => !prev), [])
|
|
36
39
|
const handleStartEditing = useCallback(() => setIsEditing(true), [])
|
|
@@ -2,8 +2,9 @@ import { Icon as IconifyIcon } from '@iconify/react'
|
|
|
2
2
|
import { type AnyNodeId, nodeRegistry, useScene } from '@pascal-app/core'
|
|
3
3
|
import { useViewer } from '@pascal-app/viewer'
|
|
4
4
|
import Image from 'next/image'
|
|
5
|
-
import { memo, useCallback, useEffect, useState } from 'react'
|
|
5
|
+
import { memo, useCallback, useEffect, useState, useSyncExternalStore } from 'react'
|
|
6
6
|
import { useShallow } from 'zustand/react/shallow'
|
|
7
|
+
import { editorHostTreeChildrenRegistry } from '../../../../../lib/host-tree-children'
|
|
7
8
|
import { resolveNodeSnapTarget, SnapTargetIcon } from '../../../snap-target-badge'
|
|
8
9
|
import { InlineRenameInput } from './inline-rename-input'
|
|
9
10
|
import {
|
|
@@ -43,9 +44,19 @@ export const RegistryTreeNode = memo(function RegistryTreeNode({
|
|
|
43
44
|
const isHovered = useViewer((state) => state.hoveredId === nodeId)
|
|
44
45
|
const setSelection = useViewer((state) => state.setSelection)
|
|
45
46
|
const setHoveredId = useViewer((state) => state.setHoveredId)
|
|
47
|
+
useSyncExternalStore(
|
|
48
|
+
editorHostTreeChildrenRegistry.subscribe,
|
|
49
|
+
editorHostTreeChildrenRegistry.getSnapshot,
|
|
50
|
+
editorHostTreeChildrenRegistry.getSnapshot,
|
|
51
|
+
)
|
|
46
52
|
|
|
47
53
|
const presentation = node ? nodeRegistry.get(node.type)?.presentation : undefined
|
|
48
54
|
const tree = node ? nodeRegistry.get(node.type)?.tree : undefined
|
|
55
|
+
const hostChildren = node
|
|
56
|
+
? editorHostTreeChildrenRegistry.childrenForKind(node.type)
|
|
57
|
+
: undefined
|
|
58
|
+
const hasHostChildren = Boolean(node && hostChildren?.hasChildren(node))
|
|
59
|
+
const HostChildren = hasHostChildren ? hostChildren?.component : undefined
|
|
49
60
|
const icon = presentation?.icon
|
|
50
61
|
const iconSrc = icon?.kind === 'url' ? icon.src : '/icons/roof.webp'
|
|
51
62
|
const iconElement =
|
|
@@ -63,7 +74,7 @@ export const RegistryTreeNode = memo(function RegistryTreeNode({
|
|
|
63
74
|
const snapTarget = resolveNodeSnapTarget(node)
|
|
64
75
|
const defaultName =
|
|
65
76
|
node ? tree?.label?.(node, useScene.getState().nodes) || node.name || presentation?.label || 'Node' : 'Node'
|
|
66
|
-
const hasChildren = children.length > 0
|
|
77
|
+
const hasChildren = children.length > 0 || hasHostChildren
|
|
67
78
|
|
|
68
79
|
useEffect(() => {
|
|
69
80
|
return useViewer.subscribe((state) => {
|
|
@@ -126,15 +137,21 @@ export const RegistryTreeNode = memo(function RegistryTreeNode({
|
|
|
126
137
|
onMouseLeave={() => setHoveredId(null)}
|
|
127
138
|
onToggle={() => setExpanded((prev) => !prev)}
|
|
128
139
|
>
|
|
129
|
-
{hasChildren &&
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
140
|
+
{hasChildren && (
|
|
141
|
+
<>
|
|
142
|
+
{children.map((childId, index) => (
|
|
143
|
+
<TreeNode
|
|
144
|
+
depth={depth + 1}
|
|
145
|
+
isLast={!hasHostChildren && index === children.length - 1}
|
|
146
|
+
key={childId}
|
|
147
|
+
nodeId={childId as AnyNodeId}
|
|
148
|
+
/>
|
|
149
|
+
))}
|
|
150
|
+
{HostChildren ? (
|
|
151
|
+
<HostChildren depth={depth + 1} nodeId={nodeId} parentVisible={isVisible} />
|
|
152
|
+
) : null}
|
|
153
|
+
</>
|
|
154
|
+
)}
|
|
138
155
|
</TreeNodeWrapper>
|
|
139
156
|
)
|
|
140
157
|
})
|
|
@@ -24,7 +24,7 @@ interface ShelfTreeNodeProps {
|
|
|
24
24
|
/**
|
|
25
25
|
* Sidebar tree entry for shelf. Mirrors `item-tree-node`'s shape so the
|
|
26
26
|
* shelf's hosted items list as collapsible children — same pattern items
|
|
27
|
-
* use for their nested items. The shelf has its own `children:
|
|
27
|
+
* use for their nested items. The shelf has its own `children: string[]`
|
|
28
28
|
* field on the schema; items reparent into it via `def.surfaces` + the
|
|
29
29
|
* placement coordinator's shelf strategy.
|
|
30
30
|
*/
|
|
@@ -69,12 +69,7 @@ export const ShelfTreeNode = memo(function ShelfTreeNode({
|
|
|
69
69
|
const handleClick = useCallback(
|
|
70
70
|
(e: React.MouseEvent) => {
|
|
71
71
|
e.stopPropagation()
|
|
72
|
-
handleTreeSelection(
|
|
73
|
-
e,
|
|
74
|
-
nodeId,
|
|
75
|
-
useViewer.getState().selection.selectedIds,
|
|
76
|
-
setSelection,
|
|
77
|
-
)
|
|
72
|
+
handleTreeSelection(e, nodeId, useViewer.getState().selection.selectedIds, setSelection)
|
|
78
73
|
routeTreeSelectionToNode(node)
|
|
79
74
|
},
|
|
80
75
|
[node, nodeId, setSelection],
|
|
@@ -124,7 +119,7 @@ export const ShelfTreeNode = memo(function ShelfTreeNode({
|
|
|
124
119
|
depth={depth + 1}
|
|
125
120
|
isLast={index === children.length - 1}
|
|
126
121
|
key={childId}
|
|
127
|
-
nodeId={childId}
|
|
122
|
+
nodeId={childId as AnyNodeId}
|
|
128
123
|
/>
|
|
129
124
|
))}
|
|
130
125
|
</TreeNodeWrapper>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import { getTreeNodeComponent } from './tree-node'
|
|
3
|
+
|
|
4
|
+
describe('site tree node routing', () => {
|
|
5
|
+
test('renders plugin node kinds through the generic tree row', () => {
|
|
6
|
+
expect(getTreeNodeComponent('lean-to-extension')).toBe(getTreeNodeComponent('plugin-kind'))
|
|
7
|
+
})
|
|
8
|
+
})
|
|
@@ -107,6 +107,7 @@ import { SlabTreeNode } from './slab-tree-node'
|
|
|
107
107
|
import { SolarPanelTreeNode } from './solar-panel-tree-node'
|
|
108
108
|
import { SpawnTreeNode } from './spawn-tree-node'
|
|
109
109
|
import { StairTreeNode } from './stair-tree-node'
|
|
110
|
+
import { UnitTreeNode } from './unit-tree-node'
|
|
110
111
|
import { WallTreeNode } from './wall-tree-node'
|
|
111
112
|
import { WindowTreeNode } from './window-tree-node'
|
|
112
113
|
import { ZoneTreeNode } from './zone-tree-node'
|
|
@@ -117,6 +118,12 @@ interface TreeNodeProps {
|
|
|
117
118
|
isLast?: boolean
|
|
118
119
|
}
|
|
119
120
|
|
|
121
|
+
type TreeNodeComponent = React.ComponentType<{
|
|
122
|
+
depth: number
|
|
123
|
+
isLast?: boolean
|
|
124
|
+
nodeId: AnyNodeId
|
|
125
|
+
}>
|
|
126
|
+
|
|
120
127
|
// Per-kind tree-node components keyed by `node.type`. Lookup replaces
|
|
121
128
|
// the legacy switch — adding a kind to this map is now the only edit
|
|
122
129
|
// needed in this file (the switch's `case '<kind>':` clauses were
|
|
@@ -124,10 +131,7 @@ interface TreeNodeProps {
|
|
|
124
131
|
// outside the registry; future work moves these to a
|
|
125
132
|
// `def.presentation`-driven generic tree-node and removes this map
|
|
126
133
|
// entirely).
|
|
127
|
-
const treeNodeByType: Record<
|
|
128
|
-
string,
|
|
129
|
-
React.ComponentType<{ depth: number; isLast?: boolean; nodeId: AnyNodeId }>
|
|
130
|
-
> = {
|
|
134
|
+
const treeNodeByType: Record<string, TreeNodeComponent> = {
|
|
131
135
|
building: BuildingTreeNode as React.ComponentType<{
|
|
132
136
|
depth: number
|
|
133
137
|
isLast?: boolean
|
|
@@ -136,6 +140,7 @@ const treeNodeByType: Record<
|
|
|
136
140
|
cabinet: RegistryTreeNode,
|
|
137
141
|
'cabinet-module': RegistryTreeNode,
|
|
138
142
|
'box-vent': RegistryTreeNode,
|
|
143
|
+
'block': RegistryTreeNode,
|
|
139
144
|
ceiling: CeilingTreeNode,
|
|
140
145
|
chimney: ChimneyTreeNode,
|
|
141
146
|
dormer: DormerTreeNode,
|
|
@@ -169,7 +174,9 @@ const treeNodeByType: Record<
|
|
|
169
174
|
'eyebrow-vent': RegistryTreeNode,
|
|
170
175
|
skylight: RegistryTreeNode,
|
|
171
176
|
roof: RoofTreeNode,
|
|
177
|
+
scan: RegistryTreeNode,
|
|
172
178
|
stair: StairTreeNode,
|
|
179
|
+
unit: UnitTreeNode,
|
|
173
180
|
door: DoorTreeNode,
|
|
174
181
|
window: WindowTreeNode,
|
|
175
182
|
zone: ZoneTreeNode as React.ComponentType<{
|
|
@@ -180,6 +187,10 @@ const treeNodeByType: Record<
|
|
|
180
187
|
item: ItemTreeNode,
|
|
181
188
|
}
|
|
182
189
|
|
|
190
|
+
export function getTreeNodeComponent(nodeType: string): TreeNodeComponent {
|
|
191
|
+
return treeNodeByType[nodeType] ?? RegistryTreeNode
|
|
192
|
+
}
|
|
193
|
+
|
|
183
194
|
export const TreeNode = memo(function TreeNode({ nodeId, depth = 0, isLast }: TreeNodeProps) {
|
|
184
195
|
// Registry-driven row hiding (`def.tree.hidden`) — primitive boolean
|
|
185
196
|
// selector so unrelated scene updates don't re-render every row.
|
|
@@ -191,14 +202,15 @@ export const TreeNode = memo(function TreeNode({ nodeId, depth = 0, isLast }: Tr
|
|
|
191
202
|
const nodeType = useScene((state) => state.nodes[nodeId]?.type)
|
|
192
203
|
if (shouldHide) return null
|
|
193
204
|
if (!nodeType) return null
|
|
194
|
-
const Component =
|
|
195
|
-
if (!Component) return null
|
|
205
|
+
const Component = getTreeNodeComponent(nodeType)
|
|
196
206
|
return <Component depth={depth} isLast={isLast} nodeId={nodeId} />
|
|
197
207
|
})
|
|
198
208
|
|
|
199
209
|
interface TreeNodeWrapperProps {
|
|
200
210
|
nodeId?: string
|
|
201
211
|
icon: React.ReactNode
|
|
212
|
+
/** Keep the icon's own color when unselected (color dots are the identity). */
|
|
213
|
+
keepIconColor?: boolean
|
|
202
214
|
label: React.ReactNode
|
|
203
215
|
depth: number
|
|
204
216
|
hasChildren: boolean
|
|
@@ -224,6 +236,7 @@ export const TreeNodeWrapper = forwardRef<HTMLDivElement, TreeNodeWrapperProps>(
|
|
|
224
236
|
{
|
|
225
237
|
nodeId,
|
|
226
238
|
icon,
|
|
239
|
+
keepIconColor,
|
|
227
240
|
label,
|
|
228
241
|
depth,
|
|
229
242
|
hasChildren,
|
|
@@ -318,7 +331,7 @@ export const TreeNodeWrapper = forwardRef<HTMLDivElement, TreeNodeWrapperProps>(
|
|
|
318
331
|
<span
|
|
319
332
|
className={cn(
|
|
320
333
|
'flex h-5 w-5 shrink-0 items-center justify-center transition-all duration-200',
|
|
321
|
-
!isSelected && 'opacity-60 grayscale',
|
|
334
|
+
!isSelected && !keepIconColor && 'opacity-60 grayscale',
|
|
322
335
|
)}
|
|
323
336
|
>
|
|
324
337
|
{icon}
|