@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
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type AnyNodeId,
|
|
3
|
+
getLevelDisplayName,
|
|
4
|
+
type LevelNode,
|
|
5
|
+
type UnitNode,
|
|
6
|
+
unitWarnings,
|
|
7
|
+
useScene,
|
|
8
|
+
type ZoneNode,
|
|
9
|
+
} from '@pascal-app/core'
|
|
10
|
+
import { useViewer } from '@pascal-app/viewer'
|
|
11
|
+
import { AlertTriangle, Trash2, X } from 'lucide-react'
|
|
12
|
+
import { memo, useCallback, useState } from 'react'
|
|
13
|
+
import { useShallow } from 'zustand/react/shallow'
|
|
14
|
+
import { ColorDot } from './../../../../../components/ui/primitives/color-dot'
|
|
15
|
+
import { assignZoneToUnit, enterUnitFocus } from './../../../../../lib/units'
|
|
16
|
+
import { InlineRenameInput } from './inline-rename-input'
|
|
17
|
+
import { routeTreeSelectionToNode, TreeNodeWrapper } from './tree-node'
|
|
18
|
+
|
|
19
|
+
type UnitWarningCode = ReturnType<typeof unitWarnings>[number]['code']
|
|
20
|
+
|
|
21
|
+
const WARNING_LABELS: Record<UnitWarningCode, string> = {
|
|
22
|
+
empty: 'No zones in this unit',
|
|
23
|
+
'non-adjacent-levels': 'Members sit on non-adjacent levels',
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const ACTION_BUTTON_CLASS =
|
|
27
|
+
'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'
|
|
28
|
+
|
|
29
|
+
interface UnitZoneRowProps {
|
|
30
|
+
zoneId: ZoneNode['id']
|
|
31
|
+
depth: number
|
|
32
|
+
isLast?: boolean
|
|
33
|
+
onRemove?: (zoneId: ZoneNode['id']) => void
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** One member zone under a unit: name + level, click selects it. */
|
|
37
|
+
export const UnitZoneRow = memo(function UnitZoneRow({
|
|
38
|
+
zoneId,
|
|
39
|
+
depth,
|
|
40
|
+
isLast,
|
|
41
|
+
onRemove,
|
|
42
|
+
}: UnitZoneRowProps) {
|
|
43
|
+
const zone = useScene((s) => s.nodes[zoneId] as ZoneNode | undefined)
|
|
44
|
+
const levelLabel = useScene((s) => {
|
|
45
|
+
const level = zone?.parentId ? (s.nodes[zone.parentId as AnyNodeId] as LevelNode | undefined) : undefined
|
|
46
|
+
return level?.type === 'level' ? getLevelDisplayName(level) : null
|
|
47
|
+
})
|
|
48
|
+
const isSelected = useViewer((s) => s.selection.zoneId === zoneId)
|
|
49
|
+
const isHovered = useViewer((s) => s.hoveredId === zoneId)
|
|
50
|
+
const setSelection = useViewer((s) => s.setSelection)
|
|
51
|
+
const setHoveredId = useViewer((s) => s.setHoveredId)
|
|
52
|
+
|
|
53
|
+
const handleClick = useCallback(
|
|
54
|
+
(event: React.MouseEvent) => {
|
|
55
|
+
event.stopPropagation()
|
|
56
|
+
if (!zone) return
|
|
57
|
+
setSelection({ levelId: zone.parentId as LevelNode['id'], zoneId })
|
|
58
|
+
routeTreeSelectionToNode(zone)
|
|
59
|
+
},
|
|
60
|
+
[zone, zoneId, setSelection],
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
if (!zone) return null
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<TreeNodeWrapper
|
|
67
|
+
actions={
|
|
68
|
+
onRemove ? (
|
|
69
|
+
<button
|
|
70
|
+
className={ACTION_BUTTON_CLASS}
|
|
71
|
+
onClick={(event) => {
|
|
72
|
+
event.stopPropagation()
|
|
73
|
+
onRemove(zoneId)
|
|
74
|
+
}}
|
|
75
|
+
title="Remove from unit"
|
|
76
|
+
type="button"
|
|
77
|
+
>
|
|
78
|
+
<X className="h-3 w-3" />
|
|
79
|
+
</button>
|
|
80
|
+
) : undefined
|
|
81
|
+
}
|
|
82
|
+
depth={depth}
|
|
83
|
+
expanded={false}
|
|
84
|
+
hasChildren={false}
|
|
85
|
+
icon={
|
|
86
|
+
<span
|
|
87
|
+
className="h-3 w-3 rounded-sm border border-border/50"
|
|
88
|
+
style={{ backgroundColor: zone.color }}
|
|
89
|
+
/>
|
|
90
|
+
}
|
|
91
|
+
isHovered={isHovered}
|
|
92
|
+
isLast={isLast}
|
|
93
|
+
isSelected={isSelected}
|
|
94
|
+
keepIconColor
|
|
95
|
+
label={
|
|
96
|
+
<span className="flex min-w-0 items-baseline gap-1.5">
|
|
97
|
+
<span className="truncate">{zone.name || 'Zone'}</span>
|
|
98
|
+
{levelLabel && (
|
|
99
|
+
<span className="shrink-0 text-muted-foreground text-xs">{levelLabel}</span>
|
|
100
|
+
)}
|
|
101
|
+
</span>
|
|
102
|
+
}
|
|
103
|
+
nodeId={zoneId}
|
|
104
|
+
onClick={handleClick}
|
|
105
|
+
onMouseEnter={() => setHoveredId(zoneId)}
|
|
106
|
+
onMouseLeave={() => setHoveredId(null)}
|
|
107
|
+
onToggle={() => {}}
|
|
108
|
+
/>
|
|
109
|
+
)
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
interface UnitTreeNodeProps {
|
|
113
|
+
nodeId: AnyNodeId
|
|
114
|
+
depth: number
|
|
115
|
+
isLast?: boolean
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export const UnitTreeNode = memo(function UnitTreeNode({
|
|
119
|
+
nodeId,
|
|
120
|
+
depth,
|
|
121
|
+
isLast,
|
|
122
|
+
}: UnitTreeNodeProps) {
|
|
123
|
+
const unitId = nodeId as UnitNode['id']
|
|
124
|
+
const [isEditing, setIsEditing] = useState(false)
|
|
125
|
+
const [expanded, setExpanded] = useState(false)
|
|
126
|
+
const updateNode = useScene((s) => s.updateNode)
|
|
127
|
+
const deleteNode = useScene((s) => s.deleteNode)
|
|
128
|
+
const color = useScene((s) => (s.nodes[nodeId] as UnitNode | undefined)?.color)
|
|
129
|
+
const members = useScene(
|
|
130
|
+
useShallow((s) => (s.nodes[nodeId] as UnitNode | undefined)?.members ?? []),
|
|
131
|
+
)
|
|
132
|
+
const warningCodes = useScene(
|
|
133
|
+
useShallow((s) => {
|
|
134
|
+
const unit = s.nodes[nodeId]
|
|
135
|
+
return unit?.type === 'unit' ? unitWarnings(unit, s.nodes).map((w) => w.code) : []
|
|
136
|
+
}),
|
|
137
|
+
)
|
|
138
|
+
const isSelected = useViewer((s) => s.selection.selectedIds.includes(nodeId))
|
|
139
|
+
const isHovered = useViewer((s) => s.hoveredId === nodeId)
|
|
140
|
+
const setHoveredId = useViewer((s) => s.setHoveredId)
|
|
141
|
+
const isFocused = useViewer((s) => s.focusedUnitId === unitId)
|
|
142
|
+
|
|
143
|
+
const handleClick = useCallback(
|
|
144
|
+
(event: React.MouseEvent) => {
|
|
145
|
+
event.stopPropagation()
|
|
146
|
+
enterUnitFocus(unitId)
|
|
147
|
+
},
|
|
148
|
+
[unitId],
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
const handleRemoveMember = useCallback(
|
|
152
|
+
(zoneId: ZoneNode['id']) => assignZoneToUnit(zoneId, null),
|
|
153
|
+
[],
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
const warningTitle = [...new Set(warningCodes)].map((code) => WARNING_LABELS[code]).join('\n')
|
|
157
|
+
|
|
158
|
+
return (
|
|
159
|
+
<TreeNodeWrapper
|
|
160
|
+
actions={
|
|
161
|
+
<button
|
|
162
|
+
className={ACTION_BUTTON_CLASS}
|
|
163
|
+
onClick={(event) => {
|
|
164
|
+
event.stopPropagation()
|
|
165
|
+
deleteNode(unitId)
|
|
166
|
+
}}
|
|
167
|
+
title="Delete unit"
|
|
168
|
+
type="button"
|
|
169
|
+
>
|
|
170
|
+
<Trash2 className="h-3 w-3" />
|
|
171
|
+
</button>
|
|
172
|
+
}
|
|
173
|
+
depth={depth}
|
|
174
|
+
expanded={expanded}
|
|
175
|
+
hasChildren={members.length > 0}
|
|
176
|
+
icon={
|
|
177
|
+
<ColorDot
|
|
178
|
+
color={color ?? '#f59e0b'}
|
|
179
|
+
onChange={(next) => updateNode(unitId, { color: next })}
|
|
180
|
+
/>
|
|
181
|
+
}
|
|
182
|
+
isHovered={isHovered}
|
|
183
|
+
isLast={isLast}
|
|
184
|
+
isSelected={isSelected}
|
|
185
|
+
keepIconColor
|
|
186
|
+
label={
|
|
187
|
+
<span className="flex min-w-0 items-center gap-1.5">
|
|
188
|
+
<InlineRenameInput
|
|
189
|
+
defaultName="Unit"
|
|
190
|
+
isEditing={isEditing}
|
|
191
|
+
nodeId={nodeId}
|
|
192
|
+
onStartEditing={() => setIsEditing(true)}
|
|
193
|
+
onStopEditing={() => setIsEditing(false)}
|
|
194
|
+
/>
|
|
195
|
+
{isFocused && (
|
|
196
|
+
<span className="shrink-0 rounded-full bg-primary/15 px-1.5 text-[10px] text-primary leading-4">
|
|
197
|
+
Focused
|
|
198
|
+
</span>
|
|
199
|
+
)}
|
|
200
|
+
{warningCodes.length > 0 && (
|
|
201
|
+
<span className="shrink-0 text-muted-foreground" title={warningTitle}>
|
|
202
|
+
<AlertTriangle className="h-3 w-3" />
|
|
203
|
+
</span>
|
|
204
|
+
)}
|
|
205
|
+
</span>
|
|
206
|
+
}
|
|
207
|
+
nodeId={nodeId}
|
|
208
|
+
onClick={handleClick}
|
|
209
|
+
onMouseEnter={() => setHoveredId(nodeId)}
|
|
210
|
+
onMouseLeave={() => setHoveredId(null)}
|
|
211
|
+
onToggle={() => setExpanded((value) => !value)}
|
|
212
|
+
>
|
|
213
|
+
{members.map((zoneId, index) => (
|
|
214
|
+
<UnitZoneRow
|
|
215
|
+
depth={depth + 1}
|
|
216
|
+
isLast={index === members.length - 1}
|
|
217
|
+
key={zoneId}
|
|
218
|
+
onRemove={handleRemoveMember}
|
|
219
|
+
zoneId={zoneId}
|
|
220
|
+
/>
|
|
221
|
+
))}
|
|
222
|
+
</TreeNodeWrapper>
|
|
223
|
+
)
|
|
224
|
+
})
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Checkbox } from './../../../primitives/checkbox'
|
|
2
|
+
|
|
3
|
+
/** Membership toggle shown on zone rows while a unit is focused. */
|
|
4
|
+
export function ZoneMembershipCheckbox({
|
|
5
|
+
checked,
|
|
6
|
+
onToggle,
|
|
7
|
+
unitName,
|
|
8
|
+
}: {
|
|
9
|
+
checked: boolean
|
|
10
|
+
onToggle: () => void
|
|
11
|
+
unitName: string
|
|
12
|
+
}) {
|
|
13
|
+
return (
|
|
14
|
+
<Checkbox
|
|
15
|
+
aria-label={`In ${unitName}`}
|
|
16
|
+
checked={checked}
|
|
17
|
+
className="mr-2"
|
|
18
|
+
onCheckedChange={onToggle}
|
|
19
|
+
onDoubleClick={(event) => event.stopPropagation()}
|
|
20
|
+
/>
|
|
21
|
+
)
|
|
22
|
+
}
|
|
@@ -3,9 +3,11 @@ import { useViewer } from '@pascal-app/viewer'
|
|
|
3
3
|
import { memo, useCallback, useState } from 'react'
|
|
4
4
|
import { ColorDot } from './../../../../../components/ui/primitives/color-dot'
|
|
5
5
|
import { formatAreaLabel } from './../../../../../lib/measurements'
|
|
6
|
+
import { toggleZoneMembership } from './../../../../../lib/units'
|
|
6
7
|
import { InlineRenameInput } from './inline-rename-input'
|
|
7
8
|
import { focusTreeNode, TreeNodeWrapper } from './tree-node'
|
|
8
9
|
import { TreeNodeActions } from './tree-node-actions'
|
|
10
|
+
import { ZoneMembershipCheckbox } from './zone-membership-checkbox'
|
|
9
11
|
|
|
10
12
|
interface ZoneTreeNodeProps {
|
|
11
13
|
nodeId: ZoneNode['id']
|
|
@@ -28,6 +30,11 @@ export const ZoneTreeNode = memo(function ZoneTreeNode({
|
|
|
28
30
|
const setSelection = useViewer((state) => state.setSelection)
|
|
29
31
|
const setHoveredId = useViewer((state) => state.setHoveredId)
|
|
30
32
|
const unit = useViewer((state) => state.unit)
|
|
33
|
+
const focusedUnitId = useViewer((state) => state.focusedUnitId)
|
|
34
|
+
const focusedUnit = useScene((s) => {
|
|
35
|
+
const focused = focusedUnitId ? s.nodes[focusedUnitId] : undefined
|
|
36
|
+
return focused?.type === 'unit' ? focused : null
|
|
37
|
+
})
|
|
31
38
|
|
|
32
39
|
const handleClick = useCallback(() => setSelection({ zoneId: nodeId }), [nodeId, setSelection])
|
|
33
40
|
const handleDoubleClick = useCallback(() => focusTreeNode(nodeId), [nodeId])
|
|
@@ -50,15 +57,25 @@ export const ZoneTreeNode = memo(function ZoneTreeNode({
|
|
|
50
57
|
isHovered={isHovered}
|
|
51
58
|
isLast={isLast}
|
|
52
59
|
isSelected={isSelected}
|
|
60
|
+
keepIconColor
|
|
53
61
|
isVisible={isVisible}
|
|
54
62
|
label={
|
|
55
|
-
<
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
63
|
+
<span className="flex min-w-0 items-center">
|
|
64
|
+
{focusedUnit && (
|
|
65
|
+
<ZoneMembershipCheckbox
|
|
66
|
+
checked={focusedUnit.members.includes(nodeId)}
|
|
67
|
+
onToggle={() => toggleZoneMembership(focusedUnit.id, nodeId)}
|
|
68
|
+
unitName={focusedUnit.name || 'Unit'}
|
|
69
|
+
/>
|
|
70
|
+
)}
|
|
71
|
+
<InlineRenameInput
|
|
72
|
+
defaultName={defaultName}
|
|
73
|
+
isEditing={isEditing}
|
|
74
|
+
nodeId={nodeId}
|
|
75
|
+
onStartEditing={handleStartEditing}
|
|
76
|
+
onStopEditing={handleStopEditing}
|
|
77
|
+
/>
|
|
78
|
+
</span>
|
|
62
79
|
}
|
|
63
80
|
onClick={handleClick}
|
|
64
81
|
onDoubleClick={handleDoubleClick}
|
|
@@ -86,6 +86,8 @@ export function IconRail({ tabs, activeTab, collapsed, onIconClick }: IconRailPr
|
|
|
86
86
|
<Tooltip key={tab.id}>
|
|
87
87
|
<TooltipTrigger asChild>
|
|
88
88
|
<button
|
|
89
|
+
aria-label={tab.label}
|
|
90
|
+
aria-pressed={showActive}
|
|
89
91
|
className={cn(
|
|
90
92
|
'group flex h-11 w-11 items-center justify-center rounded-xl transition-all duration-200 [&_img]:transition-[opacity,filter] [&_img]:duration-200',
|
|
91
93
|
showActive
|
|
@@ -12,7 +12,12 @@ import {
|
|
|
12
12
|
useSyncExternalStore,
|
|
13
13
|
} from 'react'
|
|
14
14
|
import useEditor from '../../../store/use-editor'
|
|
15
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
editorHostPanelRegistry,
|
|
17
|
+
type EditorHostPanel,
|
|
18
|
+
managedPluginIds,
|
|
19
|
+
showsPluginManager,
|
|
20
|
+
} from '../../../lib/plugin-panels'
|
|
16
21
|
import { ErrorBoundary } from '../primitives/error-boundary'
|
|
17
22
|
import type { ExtraPanel } from './icon-rail'
|
|
18
23
|
import { PluginsPanel } from './panels/plugins-panel'
|
|
@@ -100,6 +105,7 @@ export function useHostPanels(hostPanels?: ExtraPanel[]): ExtraPanel[] {
|
|
|
100
105
|
)
|
|
101
106
|
const workspaceMode = useEditor((s) => s.workspaceMode)
|
|
102
107
|
const installedPlugins = useScene((s) => s.installedPlugins)
|
|
108
|
+
const readOnly = useScene((s) => s.readOnly)
|
|
103
109
|
const hostIds = new Set(hostPanels?.map((p) => p.id))
|
|
104
110
|
|
|
105
111
|
useEffect(() => {
|
|
@@ -126,7 +132,16 @@ export function useHostPanels(hostPanels?: ExtraPanel[]): ExtraPanel[] {
|
|
|
126
132
|
pluginId: p.pluginId,
|
|
127
133
|
}),
|
|
128
134
|
)
|
|
135
|
+
// The manager tab is the one panel the editor contributes itself, so it is
|
|
136
|
+
// also the one that can be alone in the rail — see `showsPluginManager`.
|
|
129
137
|
const manager =
|
|
130
|
-
|
|
138
|
+
!hostIds.has(pluginsManagerPanel.id) &&
|
|
139
|
+
showsPluginManager({
|
|
140
|
+
managedPluginCount: managedPluginIds(registered).length,
|
|
141
|
+
readOnly,
|
|
142
|
+
workspaceMode,
|
|
143
|
+
})
|
|
144
|
+
? [pluginsManagerPanel]
|
|
145
|
+
: []
|
|
131
146
|
return [...(hostPanels ?? []), ...fromRegistry, ...manager]
|
|
132
147
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { AnyNode, AssetInput } from '@pascal-app/core'
|
|
2
|
+
import { isProceduralItem } from '@pascal-app/core/procedural-items'
|
|
2
3
|
import type { ReactNode } from 'react'
|
|
3
4
|
import { cn } from '../../lib/utils'
|
|
4
5
|
|
|
@@ -35,6 +36,7 @@ export function resolveAssetSnapTarget(attachTo: AssetInput['attachTo']): SnapTa
|
|
|
35
36
|
|
|
36
37
|
export function resolveNodeSnapTarget(node: AnyNode | null | undefined): SnapTarget | null {
|
|
37
38
|
if (!node) return null
|
|
39
|
+
if (isProceduralItem(node)) return resolveAssetSnapTarget(node.recipe.mounting?.attachTo)
|
|
38
40
|
if ('roofSegmentId' in node && typeof node.roofSegmentId === 'string') return 'roof'
|
|
39
41
|
if (node.type === 'downspout') return 'roof'
|
|
40
42
|
if (node.type === 'door' || node.type === 'window') return 'wall'
|
|
@@ -9,12 +9,13 @@ import {
|
|
|
9
9
|
useScene,
|
|
10
10
|
type ZoneNode,
|
|
11
11
|
} from '@pascal-app/core'
|
|
12
|
-
import { useViewer } from '@pascal-app/viewer'
|
|
12
|
+
import { markPerfAction, useViewer } from '@pascal-app/viewer'
|
|
13
13
|
import { ArrowLeft, ChevronRight, Layers } from 'lucide-react'
|
|
14
14
|
import Link from 'next/link'
|
|
15
15
|
import type { ReactNode } from 'react'
|
|
16
16
|
import { useShallow } from 'zustand/react/shallow'
|
|
17
17
|
import { cn } from '../../lib/utils'
|
|
18
|
+
import { ViewerUnitsPanel } from './viewer-units-panel'
|
|
18
19
|
|
|
19
20
|
const getNodeName = (node: AnyNode): string => {
|
|
20
21
|
if ('name' in node && node.name) return node.name
|
|
@@ -63,6 +64,7 @@ export const ViewerSceneHeader = ({
|
|
|
63
64
|
const selectedNode = useScene((s) =>
|
|
64
65
|
firstSelectedId ? (s.nodes[firstSelectedId as AnyNodeId] as AnyNode | undefined) : null,
|
|
65
66
|
)
|
|
67
|
+
const sceneNodes = useScene((s) => s.nodes)
|
|
66
68
|
// Highest first so the list reads top-down like a building section.
|
|
67
69
|
const levels = useScene(
|
|
68
70
|
useShallow((s) => {
|
|
@@ -76,6 +78,7 @@ export const ViewerSceneHeader = ({
|
|
|
76
78
|
|
|
77
79
|
const handleLevelClick = (levelId: LevelNode['id']) => {
|
|
78
80
|
// When switching levels, deselect zone and items
|
|
81
|
+
if (levelId !== selection.levelId) markPerfAction('level-switch', levelId)
|
|
79
82
|
useViewer.getState().setSelection({ levelId })
|
|
80
83
|
}
|
|
81
84
|
|
|
@@ -230,6 +233,7 @@ export const ViewerSceneHeader = ({
|
|
|
230
233
|
</div>
|
|
231
234
|
</div>
|
|
232
235
|
)}
|
|
236
|
+
{building && <ViewerUnitsPanel nodes={sceneNodes} />}
|
|
233
237
|
</div>
|
|
234
238
|
)
|
|
235
239
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
3
|
import { type AnyNode, type AnyNodeId, useScene } from '@pascal-app/core'
|
|
4
|
-
import { useViewer } from '@pascal-app/viewer'
|
|
4
|
+
import { markPerfAction, useViewer } from '@pascal-app/viewer'
|
|
5
5
|
import type { ReactNode } from 'react'
|
|
6
6
|
import { useCallback, useEffect, useMemo, useState } from 'react'
|
|
7
7
|
import { useShallow } from 'zustand/react/shallow'
|
|
@@ -134,6 +134,9 @@ export function ViewerStage({
|
|
|
134
134
|
const chooseLevel = useCallback(
|
|
135
135
|
(nextLevelId: string, notify = true) => {
|
|
136
136
|
setInternalLevelId(nextLevelId)
|
|
137
|
+
if (notify && nextLevelId !== useViewer.getState().selection.levelId) {
|
|
138
|
+
markPerfAction('level-switch', nextLevelId)
|
|
139
|
+
}
|
|
137
140
|
selectViewerLevel(scene?.nodes ?? useScene.getState().nodes, nextLevelId)
|
|
138
141
|
if (notify) onLevelChange?.(nextLevelId)
|
|
139
142
|
},
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
type AnyNode,
|
|
5
|
+
buildUnitReport,
|
|
6
|
+
type LevelNode,
|
|
7
|
+
type UnitKind,
|
|
8
|
+
type UnitNode,
|
|
9
|
+
} from '@pascal-app/core'
|
|
10
|
+
import { useViewer } from '@pascal-app/viewer'
|
|
11
|
+
import { useMemo } from 'react'
|
|
12
|
+
import { getAreaUnitLabel, squareMetersToAreaUnit } from '../../lib/measurements'
|
|
13
|
+
import { cn } from '../../lib/utils'
|
|
14
|
+
|
|
15
|
+
const UNIT_KIND_LABELS: Record<UnitKind, string> = {
|
|
16
|
+
apartment: 'Apartment',
|
|
17
|
+
'hotel-room': 'Hotel room',
|
|
18
|
+
commercial: 'Commercial',
|
|
19
|
+
common: 'Common',
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
type UnitRow = {
|
|
23
|
+
unit: UnitNode
|
|
24
|
+
zoneCount: number
|
|
25
|
+
areaM2: number
|
|
26
|
+
/** Levels carrying member zones, lowest ordinal first. */
|
|
27
|
+
levelIds: LevelNode['id'][]
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Read-only unit list shared by the editor preview and the published viewer.
|
|
32
|
+
* Clicking a row focuses the unit and reveals a member floor through the shared
|
|
33
|
+
* `useViewer.selection`; a second click clears the focus. Hidden without units.
|
|
34
|
+
*/
|
|
35
|
+
export function ViewerUnitsPanel({ nodes }: { nodes: Readonly<Record<string, AnyNode>> }) {
|
|
36
|
+
const focusedUnitId = useViewer((s) => s.focusedUnitId)
|
|
37
|
+
const areaUnit = useViewer((s) => s.unit)
|
|
38
|
+
|
|
39
|
+
const rows = useMemo<UnitRow[]>(
|
|
40
|
+
() =>
|
|
41
|
+
Object.values(nodes)
|
|
42
|
+
.filter((node): node is UnitNode => node.type === 'unit')
|
|
43
|
+
.map((unit) => {
|
|
44
|
+
const report = buildUnitReport(unit, nodes)
|
|
45
|
+
const ordinals = new Map<LevelNode['id'], number>()
|
|
46
|
+
for (const member of report.members) {
|
|
47
|
+
if (member.levelId && member.levelOrdinal !== null) {
|
|
48
|
+
ordinals.set(member.levelId, member.levelOrdinal)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
unit,
|
|
53
|
+
zoneCount: report.memberCount,
|
|
54
|
+
areaM2: report.grossAreaM2,
|
|
55
|
+
levelIds: [...ordinals.entries()].sort((a, b) => a[1] - b[1]).map(([id]) => id),
|
|
56
|
+
}
|
|
57
|
+
}),
|
|
58
|
+
[nodes],
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
if (rows.length === 0) return null
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<div className="corner-smooth pointer-events-auto flex w-48 flex-col overflow-hidden rounded-2xl border border-border/40 bg-background/95 py-1 shadow-elevation-4 backdrop-blur-xl">
|
|
65
|
+
<span className="px-3 py-2 font-medium text-[10px] text-muted-foreground uppercase tracking-wider">
|
|
66
|
+
Units
|
|
67
|
+
</span>
|
|
68
|
+
<div className="flex flex-col">
|
|
69
|
+
{rows.map((row) => {
|
|
70
|
+
const isFocused = row.unit.id === focusedUnitId
|
|
71
|
+
const area = Math.round(squareMetersToAreaUnit(row.areaM2, areaUnit))
|
|
72
|
+
const details = `${UNIT_KIND_LABELS[row.unit.kind]} · ${row.zoneCount} ${row.zoneCount === 1 ? 'zone' : 'zones'} · ${area} ${getAreaUnitLabel(areaUnit)}`
|
|
73
|
+
return (
|
|
74
|
+
<button
|
|
75
|
+
className={cn(
|
|
76
|
+
'group/row relative flex w-full cursor-pointer select-none items-center border-border/50 border-r border-r-transparent border-b px-3 py-1.5 text-sm transition-all duration-200',
|
|
77
|
+
isFocused
|
|
78
|
+
? 'border-r-3 border-r-white bg-accent/50 text-foreground'
|
|
79
|
+
: 'text-muted-foreground hover:bg-accent/30 hover:text-foreground',
|
|
80
|
+
)}
|
|
81
|
+
key={row.unit.id}
|
|
82
|
+
onClick={() => {
|
|
83
|
+
const viewer = useViewer.getState()
|
|
84
|
+
if (isFocused) {
|
|
85
|
+
viewer.setFocusedUnit(null)
|
|
86
|
+
return
|
|
87
|
+
}
|
|
88
|
+
viewer.setFocusedUnit(row.unit.id)
|
|
89
|
+
// Same rule as the editor: keep the current floor when it
|
|
90
|
+
// already carries a member zone, otherwise reveal the lowest one.
|
|
91
|
+
const current = viewer.selection.levelId
|
|
92
|
+
const levelId =
|
|
93
|
+
current && row.levelIds.includes(current) ? current : row.levelIds[0]
|
|
94
|
+
if (levelId && levelId !== current) viewer.setSelection({ levelId })
|
|
95
|
+
}}
|
|
96
|
+
type="button"
|
|
97
|
+
>
|
|
98
|
+
<div className="flex min-w-0 flex-1 items-center gap-2">
|
|
99
|
+
<span className="flex h-4 w-4 shrink-0 items-center justify-center">
|
|
100
|
+
<span
|
|
101
|
+
className="h-2.5 w-2.5 rounded-full"
|
|
102
|
+
style={{ backgroundColor: row.unit.color }}
|
|
103
|
+
/>
|
|
104
|
+
</span>
|
|
105
|
+
<div className="min-w-0 flex-1 text-left">
|
|
106
|
+
<div className="truncate">{row.unit.name}</div>
|
|
107
|
+
<div className="truncate text-[11px] text-muted-foreground">{details}</div>
|
|
108
|
+
</div>
|
|
109
|
+
</div>
|
|
110
|
+
</button>
|
|
111
|
+
)
|
|
112
|
+
})}
|
|
113
|
+
</div>
|
|
114
|
+
</div>
|
|
115
|
+
)
|
|
116
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
3
|
import { flushSync } from 'react-dom'
|
|
4
|
+
import { requestWalkthroughPointerLock } from '../lib/walkthrough-pointer-lock'
|
|
4
5
|
import useEditor from '../store/use-editor'
|
|
5
6
|
import { ViewerControlsBar } from './viewer/viewer-controls-bar'
|
|
6
7
|
import { ViewerSceneHeader } from './viewer/viewer-scene-header'
|
|
@@ -12,28 +13,6 @@ type ProjectOwner = {
|
|
|
12
13
|
image: string | null
|
|
13
14
|
}
|
|
14
15
|
|
|
15
|
-
function requestWalkthroughPointerLock() {
|
|
16
|
-
const canvas = document.querySelector<HTMLCanvasElement>('[data-pascal-viewer-3d] canvas')
|
|
17
|
-
if (!canvas) return
|
|
18
|
-
|
|
19
|
-
if (!canvas.hasAttribute('tabindex')) {
|
|
20
|
-
canvas.tabIndex = -1
|
|
21
|
-
}
|
|
22
|
-
canvas.focus({ preventScroll: true })
|
|
23
|
-
|
|
24
|
-
if (document.pointerLockElement === canvas) return
|
|
25
|
-
|
|
26
|
-
try {
|
|
27
|
-
// The request can also reject ASYNC (browser cooldown after a recent
|
|
28
|
-
// unlock) — swallow it like the P-resume path; clicking the canvas
|
|
29
|
-
// re-requests once the cooldown passes.
|
|
30
|
-
const result = canvas.requestPointerLock?.() as Promise<void> | undefined
|
|
31
|
-
if (result && typeof result.catch === 'function') result.catch(() => {})
|
|
32
|
-
} catch {
|
|
33
|
-
return
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
16
|
interface ViewerOverlayProps {
|
|
38
17
|
projectName?: string | null
|
|
39
18
|
owner?: ProjectOwner | null
|
|
@@ -13,6 +13,9 @@ export const ViewerZoneSystem = () => {
|
|
|
13
13
|
const { levelId, zoneId } = useViewer.getState().selection
|
|
14
14
|
const structureLayer = useEditor.getState().structureLayer
|
|
15
15
|
const nodes = useScene.getState().nodes
|
|
16
|
+
// Snapshot capture is a clean, camera-only surface — zone geometry and
|
|
17
|
+
// tags stay out of the framed shot (mirrors the editor ZoneSystem's gate).
|
|
18
|
+
const isCaptureMode = useEditor.getState().isCaptureMode
|
|
16
19
|
// During any active interaction zone labels step back entirely (Sims-light).
|
|
17
20
|
const zoneLabelsHidden =
|
|
18
21
|
resolveOverlayPolicy(useInteractionScope.getState().scope).zoneLabels === 'hidden'
|
|
@@ -31,7 +34,8 @@ export const ViewerZoneSystem = () => {
|
|
|
31
34
|
// The editor ZoneSystem handles the selected zone's opacity animation.
|
|
32
35
|
const isSelected = id === zoneId
|
|
33
36
|
const shouldShowGeometry =
|
|
34
|
-
|
|
37
|
+
!isCaptureMode &&
|
|
38
|
+
((structureLayer === 'zones' && !!levelId && isOnSelectedLevel) || isSelected)
|
|
35
39
|
if (!obj.visible) obj.visible = true
|
|
36
40
|
obj.traverse((child) => {
|
|
37
41
|
if ((child as Mesh).isMesh) {
|
|
@@ -40,7 +44,7 @@ export const ViewerZoneSystem = () => {
|
|
|
40
44
|
})
|
|
41
45
|
|
|
42
46
|
// Labels: always visible on the current level (regardless of mode or zone selection)
|
|
43
|
-
const showLabel = !zoneLabelsHidden && !!levelId && isOnSelectedLevel
|
|
47
|
+
const showLabel = !isCaptureMode && !zoneLabelsHidden && !!levelId && isOnSelectedLevel
|
|
44
48
|
const targetOpacity = showLabel ? '1' : '0'
|
|
45
49
|
const labelEl = document.getElementById(`${id}-label`)
|
|
46
50
|
if (labelEl && labelEl.style.opacity !== targetOpacity) {
|
|
@@ -17,6 +17,24 @@ export type WalkthroughHudProps = {
|
|
|
17
17
|
children?: ReactNode
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
/** The centered walkthrough pointer: a dot that grows into a green ring over
|
|
21
|
+
* an interactable (door / window / elevator). Also mounted by the snapshot
|
|
22
|
+
* capture overlay so walk / drone framing keeps the same E-to-open pointer. */
|
|
23
|
+
export function WalkthroughCrosshair({ interact }: { interact: WalkthroughInteract }) {
|
|
24
|
+
return (
|
|
25
|
+
<div className="pointer-events-none absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2">
|
|
26
|
+
<div
|
|
27
|
+
className={cn(
|
|
28
|
+
'rounded-full transition-all duration-150',
|
|
29
|
+
interact
|
|
30
|
+
? 'h-4 w-4 border-2 border-emerald-400 bg-emerald-400/10'
|
|
31
|
+
: 'h-1.5 w-1.5 bg-white/80 shadow-[0_0_2px_rgba(0,0,0,0.6)]',
|
|
32
|
+
)}
|
|
33
|
+
/>
|
|
34
|
+
</div>
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
|
|
20
38
|
export function WalkthroughHud({
|
|
21
39
|
floorLabel,
|
|
22
40
|
zoneLabel,
|
|
@@ -51,16 +69,7 @@ export function WalkthroughHud({
|
|
|
51
69
|
{children}
|
|
52
70
|
</div>
|
|
53
71
|
|
|
54
|
-
<
|
|
55
|
-
<div
|
|
56
|
-
className={cn(
|
|
57
|
-
'rounded-full transition-all duration-150',
|
|
58
|
-
interact
|
|
59
|
-
? 'h-4 w-4 border-2 border-emerald-400 bg-emerald-400/10'
|
|
60
|
-
: 'h-1.5 w-1.5 bg-white/80 shadow-[0_0_2px_rgba(0,0,0,0.6)]',
|
|
61
|
-
)}
|
|
62
|
-
/>
|
|
63
|
-
</div>
|
|
72
|
+
<WalkthroughCrosshair interact={interact} />
|
|
64
73
|
|
|
65
74
|
<div className="absolute bottom-6 left-1/2 flex -translate-x-1/2 items-center gap-2">
|
|
66
75
|
{suspended ? (
|