@pascal-app/editor 0.9.1 → 0.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +12 -9
- package/src/components/editor/alignment-3d-guide-layer.tsx +10 -2
- package/src/components/editor/bake-exporter.tsx +47 -0
- package/src/components/editor/custom-camera-controls.tsx +63 -25
- package/src/components/editor/editor-layout-mobile.tsx +5 -0
- package/src/components/editor/editor-layout-v2.tsx +18 -0
- package/src/components/editor/export-manager.tsx +71 -63
- package/src/components/editor/fence-tangent-lines-3d.tsx +87 -0
- package/src/components/editor/first-person/build-collider-world.test.ts +36 -3
- package/src/components/editor/first-person/build-collider-world.ts +32 -5
- package/src/components/editor/first-person-controls.tsx +118 -7
- package/src/components/editor/floating-action-menu.tsx +410 -33
- package/src/components/editor/floating-building-action-menu.tsx +1 -1
- package/src/components/editor/floorplan-background-selection.test.ts +58 -0
- package/src/components/editor/floorplan-background-selection.ts +3 -2
- package/src/components/editor/floorplan-panel.tsx +1804 -849
- package/src/components/editor/grid.tsx +166 -39
- package/src/components/editor/group-actions.ts +457 -0
- package/src/components/editor/group-floating-action-menu.tsx +130 -0
- package/src/components/editor/group-move-3d.ts +397 -0
- package/src/components/editor/group-rotate-handle.tsx +102 -64
- package/src/components/editor/group-selection-box-3d.tsx +173 -0
- package/src/components/editor/group-transform-shared.test.ts +192 -1
- package/src/components/editor/group-transform-shared.ts +240 -7
- package/src/components/editor/handles/handle-arrow.tsx +81 -23
- package/src/components/editor/handles/use-handle-drag.ts +37 -4
- package/src/components/editor/index.tsx +248 -27
- package/src/components/editor/measurement-pill.tsx +62 -22
- package/src/components/editor/node-action-menu.tsx +14 -1
- package/src/components/editor/node-arrow-handles.tsx +366 -173
- package/src/components/editor/opening-guides-3d-layer.tsx +144 -0
- package/src/components/editor/quick-measurement-card.tsx +70 -0
- package/src/components/editor/quick-measurement-hud.tsx +28 -0
- package/src/components/editor/riser-diagram-panel.tsx +137 -0
- package/src/components/editor/selection-manager.tsx +812 -466
- package/src/components/editor/site-edge-labels.tsx +4 -4
- package/src/components/editor/slab-hole-highlights.tsx +13 -5
- package/src/components/editor/snapshot-capture-overlay.tsx +255 -115
- package/src/components/editor/three-context-bridge.ts +22 -0
- package/src/components/editor/thumbnail-generator.tsx +136 -43
- package/src/components/editor/use-floorplan-background-placement.ts +113 -45
- package/src/components/editor/use-floorplan-scene-data.ts +5 -5
- package/src/components/editor/use-mesh-settle-epoch.ts +26 -0
- package/src/components/editor/wall-measurement-label.tsx +3 -13
- package/src/components/editor/wall-move-side-handles.tsx +36 -21
- package/src/components/editor/wall-snap-beacon-layer.tsx +160 -6
- package/src/components/editor-2d/floorplan-action-menu-layer.tsx +9 -5
- package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +1 -1
- package/src/components/editor-2d/floorplan-group-action-menu.tsx +87 -0
- package/src/components/editor-2d/floorplan-group-move.tsx +701 -0
- package/src/components/editor-2d/floorplan-measurement-tool-layer.test.ts +113 -0
- package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +1656 -0
- package/src/components/editor-2d/floorplan-quick-measure-layer.tsx +212 -0
- package/src/components/editor-2d/floorplan-registry-action-menu.tsx +169 -4
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +300 -59
- package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +5 -3
- package/src/components/editor-2d/renderers/floorplan-label-angle.test.ts +15 -0
- package/src/components/editor-2d/renderers/floorplan-label-angle.ts +14 -0
- package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +4 -1
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +308 -0
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +1704 -342
- package/src/components/editor-2d/renderers/floorplan-voronoi-layer.tsx +128 -0
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +419 -60
- package/src/components/systems/ceiling/ceiling-system.tsx +97 -4
- package/src/components/systems/roof/roof-edit-system.tsx +1768 -13
- package/src/components/systems/selection-affordance-manager.tsx +38 -0
- package/src/components/systems/zone/zone-label-editor-system.tsx +14 -1
- package/src/components/systems/zone/zone-system.tsx +42 -13
- package/src/components/tools/elevator/elevator-tool.tsx +34 -8
- package/src/components/tools/elevator/move-elevator-tool.tsx +13 -2
- package/src/components/tools/fence/fence-drafting.ts +80 -8
- package/src/components/tools/item/move-tool.tsx +17 -13
- package/src/components/tools/item/placement-math.test.ts +13 -1
- package/src/components/tools/item/placement-math.ts +28 -2
- package/src/components/tools/item/placement-strategies.ts +246 -5
- package/src/components/tools/item/placement-types.ts +13 -1
- package/src/components/tools/item/use-draft-node.ts +31 -1
- package/src/components/tools/item/use-placement-coordinator.tsx +761 -121
- package/src/components/tools/registry/move-registry-node-tool.tsx +561 -99
- package/src/components/tools/roof/roof-tool.tsx +320 -94
- package/src/components/tools/select/box-select-state.ts +18 -7
- package/src/components/tools/select/box-select-tool.tsx +176 -40
- package/src/components/tools/select/marquee-geometry.test.ts +161 -0
- package/src/components/tools/select/marquee-geometry.ts +155 -0
- package/src/components/tools/select/plane-box-select-tool.tsx +1 -8
- package/src/components/tools/select/select-candidates.ts +1 -1
- package/src/components/tools/shared/cursor-sphere.tsx +62 -26
- package/src/components/tools/shared/drag-bounding-box.tsx +28 -4
- package/src/components/tools/shared/facing-indicator.tsx +87 -0
- package/src/components/tools/shared/facing-pose-indicator.tsx +54 -0
- package/src/components/tools/shared/placement-box.tsx +183 -1
- package/src/components/tools/shared/polygon-editor.tsx +276 -35
- package/src/components/tools/site/site-boundary-editor.tsx +88 -36
- package/src/components/tools/stair/stair-defaults.ts +1 -0
- package/src/components/tools/stair/stair-tool.tsx +82 -11
- package/src/components/tools/tool-manager.tsx +86 -25
- package/src/components/tools/wall/wall-drafting.test.ts +330 -0
- package/src/components/tools/wall/wall-drafting.ts +180 -103
- package/src/components/tools/wall/wall-snap-geometry.test.ts +44 -0
- package/src/components/tools/wall/wall-snap-geometry.ts +67 -6
- package/src/components/tools/zone/zone-boundary-editor.tsx +5 -1
- package/src/components/tools/zone/zone-tool.tsx +82 -88
- package/src/components/ui/action-menu/camera-actions.tsx +24 -17
- package/src/components/ui/action-menu/control-modes.tsx +40 -93
- package/src/components/ui/action-menu/furnish-tools.tsx +5 -5
- package/src/components/ui/action-menu/index.tsx +2 -4
- package/src/components/ui/action-menu/measurement-control.tsx +188 -0
- package/src/components/ui/action-menu/structure-tools.tsx +22 -13
- package/src/components/ui/action-menu/view-toggles.tsx +42 -90
- package/src/components/ui/command-palette/editor-commands.tsx +2 -2
- package/src/components/ui/command-palette/index.tsx +4 -3
- package/src/components/ui/controls/material-paint-panel.tsx +86 -17
- package/src/components/ui/controls/material-picker.tsx +83 -152
- package/src/components/ui/controls/material-properties-editor.tsx +108 -0
- package/src/components/ui/controls/metric-control.tsx +118 -44
- package/src/components/ui/controls/scene-material-list.tsx +247 -0
- package/src/components/ui/controls/slider-control.tsx +88 -38
- package/src/components/ui/floating-level-selector.tsx +1 -1
- package/src/components/ui/helpers/building-helper.tsx +10 -23
- package/src/components/ui/helpers/contextual-helper-panel.tsx +428 -0
- package/src/components/ui/helpers/helper-manager.tsx +227 -14
- package/src/components/ui/helpers/item-helper.tsx +28 -32
- package/src/components/ui/helpers/registered-tool-helper.tsx +45 -11
- package/src/components/ui/helpers/roof-helper.tsx +10 -12
- package/src/components/ui/icon-ref.tsx +47 -0
- package/src/components/ui/item-catalog/catalog-items.tsx +39 -1
- package/src/components/ui/item-catalog/item-catalog.tsx +13 -36
- package/src/components/ui/level-duplicate-dialog.tsx +1 -1
- package/src/components/ui/panels/node-display.ts +17 -17
- package/src/components/ui/panels/panel-manager.tsx +22 -13
- package/src/components/ui/panels/panel-wrapper.tsx +24 -3
- package/src/components/ui/panels/parametric-inspector.tsx +15 -2
- package/src/components/ui/panels/reference-panel.tsx +54 -19
- package/src/components/ui/primitives/shortcut-token.tsx +39 -3
- package/src/components/ui/primitives/sidebar.tsx +1 -0
- package/src/components/ui/sidebar/app-sidebar.tsx +5 -1
- package/src/components/ui/sidebar/icon-rail.tsx +50 -31
- package/src/components/ui/sidebar/panels/plugins-panel.tsx +218 -0
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +71 -40
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +60 -6
- package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +12 -4
- package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -3
- package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +10 -7
- package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +7 -2
- package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +10 -7
- package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +10 -7
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +40 -17
- package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +31 -17
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +72 -25
- package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +7 -3
- package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +12 -8
- package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +4 -3
- package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +10 -7
- package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +2 -2
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +44 -3
- package/src/components/ui/sidebar/panels/site-panel/tree-structure.ts +36 -0
- package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +7 -2
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +3 -3
- package/src/components/ui/sidebar/tab-bar.tsx +42 -28
- package/src/components/ui/sidebar/use-plugin-panels.tsx +132 -0
- package/src/components/ui/snap-target-badge.test.tsx +40 -0
- package/src/components/ui/snap-target-badge.tsx +88 -0
- package/src/components/viewer-overlay.tsx +85 -10
- package/src/components/viewer-zone-system.tsx +6 -1
- package/src/hooks/use-auto-save.test.ts +14 -0
- package/src/hooks/use-auto-save.ts +54 -7
- package/src/hooks/use-ceiling-events.ts +3 -2
- package/src/hooks/use-drag-action.ts +4 -1
- package/src/hooks/use-keyboard.ts +385 -40
- package/src/index.tsx +219 -6
- package/src/lib/active-placement-surface.ts +35 -0
- package/src/lib/ceiling-plan-snap.ts +24 -0
- package/src/lib/contextual-help.test.ts +112 -0
- package/src/lib/contextual-help.ts +144 -0
- package/src/lib/continuation.ts +64 -0
- package/src/lib/direct-manipulation.test.ts +192 -0
- package/src/lib/direct-manipulation.ts +109 -0
- package/src/lib/editor-api.ts +23 -35
- package/src/lib/floorplan/apply-alignment.test.ts +25 -0
- package/src/lib/floorplan/apply-alignment.ts +18 -12
- package/src/lib/floorplan/floorplan-export.tsx +364 -0
- package/src/lib/floorplan/geometry.ts +53 -0
- package/src/lib/floorplan/index.ts +3 -0
- package/src/lib/floorplan/items.ts +5 -2
- package/src/lib/floorplan/plan-coords.ts +21 -0
- package/src/lib/fresh-planar-placement.test.ts +240 -3
- package/src/lib/fresh-planar-placement.ts +68 -1
- package/src/lib/glb-export.test.ts +441 -0
- package/src/lib/glb-export.ts +874 -0
- package/src/lib/history.ts +9 -0
- package/src/lib/interaction/hot-set.test.ts +133 -0
- package/src/lib/interaction/hot-set.ts +67 -0
- package/src/lib/interaction/overlay-policy.test.ts +51 -0
- package/src/lib/interaction/overlay-policy.ts +59 -0
- package/src/lib/interaction/scope.ts +211 -0
- package/src/lib/level-selection.ts +2 -2
- package/src/lib/material-paint.ts +36 -49
- package/src/lib/measurement-kind.test.ts +30 -0
- package/src/lib/measurement-kind.ts +19 -0
- package/src/lib/measurement-label.test.ts +47 -0
- package/src/lib/measurement-label.ts +68 -0
- package/src/lib/measurement-parser.ts +116 -0
- package/src/lib/measurements.test.ts +183 -0
- package/src/lib/measurements.ts +199 -0
- package/src/lib/paint-scope.test.ts +454 -0
- package/src/lib/paint-scope.ts +461 -0
- package/src/lib/placement-drag-release.ts +23 -0
- package/src/lib/planar-cursor-placement.test.ts +57 -0
- package/src/lib/plugin-panels.test.ts +19 -0
- package/src/lib/plugin-panels.ts +91 -0
- package/src/lib/quick-measurement.test.ts +77 -0
- package/src/lib/quick-measurement.ts +109 -0
- package/src/lib/roof-duplication.ts +6 -6
- package/src/lib/roof-hover-outline-proxy.ts +22 -0
- package/src/lib/roof-wall-hit.ts +164 -0
- package/src/lib/scene-clipboard.test.ts +196 -0
- package/src/lib/scene-clipboard.ts +73 -8
- package/src/lib/scene.ts +28 -5
- package/src/lib/selection-routing.test.ts +298 -0
- package/src/lib/selection-routing.ts +175 -0
- package/src/lib/sfx/index.ts +1 -0
- package/src/lib/sfx/movement-tick.test.ts +65 -0
- package/src/lib/sfx/movement-tick.ts +18 -0
- package/src/lib/sfx-bus.ts +50 -13
- package/src/lib/sfx-player.test.ts +124 -0
- package/src/lib/sfx-player.ts +111 -66
- package/src/lib/slab-plan-snap.test.ts +93 -0
- package/src/lib/slab-plan-snap.ts +108 -0
- package/src/lib/snapping-mode.test.ts +138 -0
- package/src/lib/snapping-mode.ts +179 -0
- package/src/lib/stair-duplication.ts +4 -4
- package/src/lib/surface-plan-snap.test.ts +71 -0
- package/src/lib/surface-plan-snap.ts +256 -0
- package/src/lib/use-linear-display.ts +40 -0
- package/src/lib/world-grid-snap.ts +37 -5
- package/src/store/use-direct-manipulation-feedback.ts +20 -0
- package/src/store/use-editor.tsx +484 -114
- package/src/store/use-facing-pose.ts +44 -0
- package/src/store/use-fence-curve-draft.ts +21 -0
- package/src/store/use-floorplan-draft-preview.ts +101 -0
- package/src/store/use-floorplan-marquee.ts +50 -0
- package/src/store/use-interaction-scope.test.ts +186 -0
- package/src/store/use-interaction-scope.ts +132 -0
- package/src/store/use-measurement-draft.test.ts +530 -0
- package/src/store/use-measurement-draft.ts +543 -0
- package/src/store/use-opening-guides.ts +41 -0
- package/src/store/use-placement-preview.ts +11 -3
- package/src/store/use-quick-measurement-hud.test.ts +53 -0
- package/src/store/use-quick-measurement-hud.ts +77 -0
- package/src/store/use-segment-draft-chain.ts +28 -0
- package/src/store/use-stair-build-preview.ts +43 -0
- package/src/store/use-wall-snap-indicator.ts +2 -0
- package/src/components/editor/first-person/bvh-ecctrl.tsx +0 -860
- package/src/components/editor/group-move-handle.tsx +0 -316
- package/src/components/ui/panels/paint-panel.tsx +0 -163
- package/src/lib/level-name.ts +0 -11
|
@@ -1,44 +1,257 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
type AnyNode,
|
|
5
|
+
type AnyNodeId,
|
|
6
|
+
nodeRegistry,
|
|
7
|
+
useScene,
|
|
8
|
+
} from '@pascal-app/core'
|
|
9
|
+
import { useViewer } from '@pascal-app/viewer'
|
|
10
|
+
import { useEffect, useMemo, useState } from 'react'
|
|
11
|
+
import { useShallow } from 'zustand/react/shallow'
|
|
4
12
|
import { useIsMobile } from '../../../hooks/use-mobile'
|
|
5
|
-
import
|
|
13
|
+
import {
|
|
14
|
+
type ContextualShortcutHint,
|
|
15
|
+
GROUP_MOVE_DRAG_LABEL,
|
|
16
|
+
GROUP_ROTATE_DRAG_LABEL,
|
|
17
|
+
ROTATE_HANDLE_DRAG_LABEL,
|
|
18
|
+
resolveRotateHandleHelpHints,
|
|
19
|
+
resolveSelectModeHelpHints,
|
|
20
|
+
} from '../../../lib/contextual-help'
|
|
21
|
+
import { continuationContextOf } from '../../../lib/continuation'
|
|
22
|
+
import { canDirectMoveNode, canDirectRotateNode } from '../../../lib/direct-manipulation'
|
|
23
|
+
import type { ReshapeKind } from '../../../lib/interaction/scope'
|
|
24
|
+
import { isFreshPlacementMetadata } from '../../../lib/placement-metadata'
|
|
25
|
+
import { snapContextOf } from '../../../lib/snapping-mode'
|
|
26
|
+
import useEditor, { getActiveContinuationContext } from '../../../store/use-editor'
|
|
27
|
+
import useInteractionScope, {
|
|
28
|
+
useActiveHandleDrag,
|
|
29
|
+
useMovingNode,
|
|
30
|
+
} from '../../../store/use-interaction-scope'
|
|
6
31
|
import { BuildingHelper } from './building-helper'
|
|
32
|
+
import { ContextualHelperPanel } from './contextual-helper-panel'
|
|
7
33
|
import { ItemHelper } from './item-helper'
|
|
8
34
|
import { RegisteredToolHelper } from './registered-tool-helper'
|
|
9
35
|
import { RoofHelper } from './roof-helper'
|
|
10
36
|
|
|
37
|
+
// Reshaping a selected node's geometry (endpoint / curve / polygon corner). The
|
|
38
|
+
// snapping chip is the main control; these just name the gesture + Esc.
|
|
39
|
+
function reshapingHints(reshape: ReshapeKind): ContextualShortcutHint[] {
|
|
40
|
+
const action =
|
|
41
|
+
reshape === 'curve'
|
|
42
|
+
? 'Curve'
|
|
43
|
+
: reshape === 'control-point'
|
|
44
|
+
? 'Move control point'
|
|
45
|
+
: reshape === 'tangent'
|
|
46
|
+
? 'Move tangent'
|
|
47
|
+
: reshape === 'endpoint'
|
|
48
|
+
? 'Move endpoint'
|
|
49
|
+
: 'Move corner'
|
|
50
|
+
return [
|
|
51
|
+
{ keys: ['Drag'], label: action },
|
|
52
|
+
{ keys: ['Esc'], label: 'Cancel' },
|
|
53
|
+
]
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
type ActiveModifierKeys = {
|
|
57
|
+
command: boolean
|
|
58
|
+
shift: boolean
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function useActiveModifierKeys(): ActiveModifierKeys {
|
|
62
|
+
const [modifiers, setModifiers] = useState<ActiveModifierKeys>({
|
|
63
|
+
command: false,
|
|
64
|
+
shift: false,
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
const updateModifiers = (event: KeyboardEvent) => {
|
|
69
|
+
const isKeyDown = event.type === 'keydown'
|
|
70
|
+
setModifiers({
|
|
71
|
+
command:
|
|
72
|
+
event.metaKey ||
|
|
73
|
+
event.ctrlKey ||
|
|
74
|
+
(isKeyDown && (event.key === 'Meta' || event.key === 'Control')),
|
|
75
|
+
shift: event.shiftKey || (isKeyDown && event.key === 'Shift'),
|
|
76
|
+
})
|
|
77
|
+
}
|
|
78
|
+
const clearModifiers = () => {
|
|
79
|
+
setModifiers({ command: false, shift: false })
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
window.addEventListener('keydown', updateModifiers)
|
|
83
|
+
window.addEventListener('keyup', updateModifiers)
|
|
84
|
+
window.addEventListener('blur', clearModifiers)
|
|
85
|
+
return () => {
|
|
86
|
+
window.removeEventListener('keydown', updateModifiers)
|
|
87
|
+
window.removeEventListener('keyup', updateModifiers)
|
|
88
|
+
window.removeEventListener('blur', clearModifiers)
|
|
89
|
+
}
|
|
90
|
+
}, [])
|
|
91
|
+
|
|
92
|
+
return modifiers
|
|
93
|
+
}
|
|
94
|
+
|
|
11
95
|
export function HelperManager() {
|
|
12
96
|
const mode = useEditor((s) => s.mode)
|
|
13
97
|
const tool = useEditor((s) => s.tool)
|
|
14
|
-
const
|
|
98
|
+
const measurementToolKind = useEditor((s) => s.toolDefaults.measurement?.kind)
|
|
99
|
+
const workspaceMode = useEditor((s) => s.workspaceMode)
|
|
100
|
+
const scope = useInteractionScope((s) => s.scope)
|
|
101
|
+
const movingNode = useMovingNode()
|
|
102
|
+
const activeHandleDrag = useActiveHandleDrag()
|
|
103
|
+
const selectedIds = useViewer((s) => s.selection.selectedIds)
|
|
15
104
|
const isMobile = useIsMobile()
|
|
105
|
+
const modifiers = useActiveModifierKeys()
|
|
106
|
+
const selectedNodes = useScene(
|
|
107
|
+
useShallow((s) =>
|
|
108
|
+
selectedIds
|
|
109
|
+
.map((id) => s.nodes[id as AnyNodeId])
|
|
110
|
+
.filter((node): node is AnyNode => node !== undefined),
|
|
111
|
+
),
|
|
112
|
+
)
|
|
113
|
+
// The snapping context for whatever's active (wall / item / polygon) — drives
|
|
114
|
+
// which snapping chips the HUD shows, derived once and shared by every branch.
|
|
115
|
+
const snapContext = useMemo(
|
|
116
|
+
() =>
|
|
117
|
+
snapContextOf({
|
|
118
|
+
scope,
|
|
119
|
+
mode,
|
|
120
|
+
tool,
|
|
121
|
+
profileOf: (typeOrTool) => nodeRegistry.get(typeOrTool)?.snapProfile,
|
|
122
|
+
draftDirectionalOf: (typeOrTool) => nodeRegistry.get(typeOrTool)?.snapDraftDirectional ?? true,
|
|
123
|
+
}),
|
|
124
|
+
[scope, mode, tool],
|
|
125
|
+
)
|
|
126
|
+
const continuationContext = useMemo(
|
|
127
|
+
() => getActiveContinuationContext(),
|
|
128
|
+
[scope, mode, tool],
|
|
129
|
+
)
|
|
130
|
+
const selectModeHints = useMemo(() => {
|
|
131
|
+
const single = selectedNodes.length === 1 ? selectedNodes[0] : null
|
|
132
|
+
const mepSelection =
|
|
133
|
+
single?.type === 'duct-segment' || single?.type === 'pipe-segment'
|
|
134
|
+
? 'run'
|
|
135
|
+
: single?.type === 'duct-fitting' || single?.type === 'pipe-fitting'
|
|
136
|
+
? 'fitting'
|
|
137
|
+
: null
|
|
138
|
+
return resolveSelectModeHelpHints({
|
|
139
|
+
selectedCount: selectedNodes.length,
|
|
140
|
+
hasMovableSelection: selectedNodes.some((node) => canDirectMoveNode(node)),
|
|
141
|
+
hasRotatableSelection: selectedNodes.some((node) => canDirectRotateNode(node)),
|
|
142
|
+
commandPressed: modifiers.command,
|
|
143
|
+
shiftPressed: modifiers.shift,
|
|
144
|
+
mepSelection,
|
|
145
|
+
})
|
|
146
|
+
}, [modifiers.command, modifiers.shift, selectedNodes])
|
|
16
147
|
|
|
17
148
|
// Helpers are keyboard-driven hints (Esc, R, etc.) — irrelevant on touch.
|
|
18
149
|
if (isMobile) return null
|
|
19
150
|
|
|
151
|
+
// The studio workspace (compose panel / gallery) has no scene selection or
|
|
152
|
+
// tools — editor shortcut hints would only mislead there.
|
|
153
|
+
if (workspaceMode === 'studio') return null
|
|
154
|
+
|
|
155
|
+
// Rotating a node (or a multi-selection group) via its in-world gizmo:
|
|
156
|
+
// advertise Shift = free rotation, the same angle-step bypass wall drafting
|
|
157
|
+
// exposes. Takes priority over the idle select-mode hints since a handle
|
|
158
|
+
// drag is the active interaction.
|
|
159
|
+
if (
|
|
160
|
+
activeHandleDrag?.label === ROTATE_HANDLE_DRAG_LABEL ||
|
|
161
|
+
activeHandleDrag?.label === GROUP_ROTATE_DRAG_LABEL
|
|
162
|
+
) {
|
|
163
|
+
return <ContextualHelperPanel hints={resolveRotateHandleHelpHints(modifiers.shift)} />
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Group-move drag / pick-up: the drag resolves to the 'item' snap context
|
|
167
|
+
// (see `snapContextOf`), so surface the snapping chips — mode + grid step,
|
|
168
|
+
// with their Shift / Ctrl cycle shortcuts — plus the mid-move R/T rotate.
|
|
169
|
+
if (activeHandleDrag?.label === GROUP_MOVE_DRAG_LABEL) {
|
|
170
|
+
return (
|
|
171
|
+
<ContextualHelperPanel
|
|
172
|
+
hints={[{ keys: ['R / T'], label: 'Rotate the selection ±45°' }]}
|
|
173
|
+
snapContext={snapContext}
|
|
174
|
+
/>
|
|
175
|
+
)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Reshaping a node's geometry (endpoint / curve / polygon corner). Checked
|
|
179
|
+
// before the select branch so the idle "drag selected / add objects" hints
|
|
180
|
+
// never leak over an in-progress reshape — and it gets its own snapping chip.
|
|
181
|
+
if (scope.kind === 'reshaping') {
|
|
182
|
+
return <ContextualHelperPanel hints={reshapingHints(scope.reshape)} snapContext={snapContext} />
|
|
183
|
+
}
|
|
184
|
+
|
|
20
185
|
if (movingNode) {
|
|
21
186
|
if (movingNode.type === 'building') return <BuildingHelper showRotate />
|
|
22
|
-
|
|
187
|
+
// A fresh placement (e.g. a positioned preset like a shelf) advertises its
|
|
188
|
+
// once/repeat continuation, exactly like the GLB item tool — but an existing
|
|
189
|
+
// node being *moved* is not a placement, so it gets no continuation chip.
|
|
190
|
+
const movingContinuationContext = isFreshPlacementMetadata(movingNode.metadata)
|
|
191
|
+
? continuationContextOf(movingNode.type)
|
|
192
|
+
: null
|
|
193
|
+
// Force-place only makes sense for kinds that collision-validate their drop;
|
|
194
|
+
// structural kinds (wall/slab/…) never reject, so don't advertise Alt.
|
|
195
|
+
return (
|
|
196
|
+
<ItemHelper
|
|
197
|
+
continuationContext={movingContinuationContext}
|
|
198
|
+
showEsc
|
|
199
|
+
showForce={nodeRegistry.get(movingNode.type)?.snapProfile !== 'structural'}
|
|
200
|
+
snapContext={snapContext}
|
|
201
|
+
/>
|
|
202
|
+
)
|
|
23
203
|
}
|
|
24
204
|
|
|
205
|
+
// Paint mode advertises (and cycles, via Shift) the application scope — the
|
|
206
|
+
// only contextual control here. The chip hides itself for targets that only
|
|
207
|
+
// paint one surface, so this renders nothing until a scoped target is active.
|
|
25
208
|
if (mode === 'material-paint') {
|
|
26
|
-
return
|
|
209
|
+
return <ContextualHelperPanel hints={[]} showPaintScope />
|
|
27
210
|
}
|
|
28
211
|
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
|
|
212
|
+
// Idle select only — an active scope (handle-drag, box-select, …) must not show
|
|
213
|
+
// the idle selection hints.
|
|
214
|
+
if (mode === 'select' && scope.kind === 'idle') {
|
|
215
|
+
return <ContextualHelperPanel hints={selectModeHints} />
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (tool === 'measurement' && measurementToolKind === 'smart') {
|
|
219
|
+
return (
|
|
220
|
+
<ContextualHelperPanel
|
|
221
|
+
hints={[
|
|
222
|
+
{ keys: ['Hover'], label: 'Inspect surface dimensions' },
|
|
223
|
+
{ keys: ['Click'], label: 'Pin measurement lens' },
|
|
224
|
+
{ keys: ['Esc'], label: 'Exit smart measure' },
|
|
225
|
+
]}
|
|
226
|
+
/>
|
|
227
|
+
)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Legacy fallback — only `roof` remains because it hasn't migrated to
|
|
231
|
+
// `def.tool` / `def.toolHints` yet (no Stage D port). Checked before the
|
|
232
|
+
// generic tool branch so the snap-context fallback below doesn't capture it
|
|
233
|
+
// and drop its bespoke `RoofHelper` hints. When roof migrates, this deletes.
|
|
234
|
+
if (tool === 'roof') return <RoofHelper snapContext={snapContext} />
|
|
235
|
+
|
|
236
|
+
// Registry-first: a kind renders the generic `RegisteredToolHelper` when it
|
|
237
|
+
// declares `def.toolHints`, OR whenever its draft resolves to a snap /
|
|
238
|
+
// continuation context — so a snappable tool with NO hand-written hints (e.g.
|
|
239
|
+
// `zone`) still advertises the snapping chip it already honors (Shift = cycle).
|
|
240
|
+
// `RegisteredToolHelper` self-hides when there's genuinely nothing to show.
|
|
32
241
|
if (tool) {
|
|
33
242
|
const def = nodeRegistry.get(tool)
|
|
34
|
-
|
|
35
|
-
|
|
243
|
+
const hints = def?.toolHints ?? []
|
|
244
|
+
if (hints.length > 0 || snapContext || continuationContext) {
|
|
245
|
+
return (
|
|
246
|
+
<RegisteredToolHelper
|
|
247
|
+
continuationContext={continuationContext}
|
|
248
|
+
hints={hints}
|
|
249
|
+
shiftPressed={modifiers.shift}
|
|
250
|
+
snapContext={snapContext}
|
|
251
|
+
/>
|
|
252
|
+
)
|
|
36
253
|
}
|
|
37
254
|
}
|
|
38
255
|
|
|
39
|
-
// Legacy fallback — only `roof` remains because it hasn't migrated to
|
|
40
|
-
// `def.tool` / `def.toolHints` yet (no Stage D port). When roof
|
|
41
|
-
// migrates, this switch deletes outright.
|
|
42
|
-
if (tool === 'roof') return <RoofHelper />
|
|
43
256
|
return null
|
|
44
257
|
}
|
|
@@ -1,40 +1,36 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { ContinuationContext } from '../../../lib/continuation'
|
|
2
|
+
import type { SnapContext } from '../../../lib/snapping-mode'
|
|
3
|
+
import { ContextualHelperPanel } from './contextual-helper-panel'
|
|
2
4
|
|
|
3
5
|
interface ItemHelperProps {
|
|
4
6
|
showEsc?: boolean
|
|
7
|
+
snapContext?: SnapContext | null
|
|
8
|
+
// Whether to advertise Alt = force-place. Only meaningful for kinds that
|
|
9
|
+
// collision-validate their drop (structural kinds never reject, so it's hidden).
|
|
10
|
+
showForce?: boolean
|
|
11
|
+
// Set for a fresh point-kind placement (e.g. a positioned preset) so the
|
|
12
|
+
// once/repeat continuation chip shows; null for an existing-node move.
|
|
13
|
+
continuationContext?: ContinuationContext | null
|
|
5
14
|
}
|
|
6
15
|
|
|
7
|
-
|
|
16
|
+
// Snapping mode is the chip on the right (Shift cycles it), so it's not repeated
|
|
17
|
+
// as a key hint. Rotate is the two keys; Alt forces an invalid (red) drop.
|
|
18
|
+
export function ItemHelper({
|
|
19
|
+
showEsc,
|
|
20
|
+
snapContext,
|
|
21
|
+
showForce,
|
|
22
|
+
continuationContext = null,
|
|
23
|
+
}: ItemHelperProps) {
|
|
8
24
|
return (
|
|
9
|
-
<
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
<ShortcutToken value="T" />
|
|
20
|
-
<span className="text-muted-foreground">Rotate clockwise</span>
|
|
21
|
-
</div>
|
|
22
|
-
<div className="flex items-center gap-2 text-sm">
|
|
23
|
-
<ShortcutToken value="Shift" />
|
|
24
|
-
<span className="text-muted-foreground">Free place</span>
|
|
25
|
-
</div>
|
|
26
|
-
{showEsc && (
|
|
27
|
-
<div className="flex items-center gap-2 text-sm">
|
|
28
|
-
<ShortcutToken value="Esc" />
|
|
29
|
-
<span className="text-muted-foreground">Cancel</span>
|
|
30
|
-
</div>
|
|
31
|
-
)}
|
|
32
|
-
{!showEsc && (
|
|
33
|
-
<div className="flex items-center gap-2 text-sm">
|
|
34
|
-
<ShortcutToken value="Right click" />
|
|
35
|
-
<span className="text-muted-foreground">Cancel</span>
|
|
36
|
-
</div>
|
|
37
|
-
)}
|
|
38
|
-
</div>
|
|
25
|
+
<ContextualHelperPanel
|
|
26
|
+
continuationContext={continuationContext}
|
|
27
|
+
hints={[
|
|
28
|
+
{ keys: ['Left click'], label: 'Place' },
|
|
29
|
+
{ keys: ['R', 'T'], label: 'Rotate' },
|
|
30
|
+
...(showForce ? [{ keys: ['Alt'], label: 'Force place' }] : []),
|
|
31
|
+
{ keys: [showEsc ? 'Esc' : 'Right click'], label: 'Cancel' },
|
|
32
|
+
]}
|
|
33
|
+
snapContext={snapContext}
|
|
34
|
+
/>
|
|
39
35
|
)
|
|
40
36
|
}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import type { ToolHint } from '@pascal-app/core'
|
|
2
|
-
import {
|
|
2
|
+
import type { ContinuationContext } from '../../../lib/continuation'
|
|
3
|
+
import type { SnapContext } from '../../../lib/snapping-mode'
|
|
4
|
+
import useEditor from '../../../store/use-editor'
|
|
5
|
+
import { ContextualHelperPanel } from './contextual-helper-panel'
|
|
3
6
|
|
|
4
7
|
/**
|
|
5
8
|
* Generic helper panel rendered from `def.toolHints` data. Matches the
|
|
@@ -10,16 +13,47 @@ import { ShortcutToken } from '../primitives/shortcut-token'
|
|
|
10
13
|
* Drops the need for per-kind helper files entirely — kinds declare
|
|
11
14
|
* their hints as static data in their `NodeDefinition`.
|
|
12
15
|
*/
|
|
13
|
-
export function RegisteredToolHelper({
|
|
14
|
-
|
|
16
|
+
export function RegisteredToolHelper({
|
|
17
|
+
hints,
|
|
18
|
+
shiftPressed = false,
|
|
19
|
+
snapContext = null,
|
|
20
|
+
continuationContext = null,
|
|
21
|
+
}: {
|
|
22
|
+
hints: ToolHint[]
|
|
23
|
+
shiftPressed?: boolean
|
|
24
|
+
snapContext?: SnapContext | null
|
|
25
|
+
continuationContext?: ContinuationContext | null
|
|
26
|
+
}) {
|
|
27
|
+
// Live vertex count of an in-progress polygon draft, so hints gated on a
|
|
28
|
+
// minimum (e.g. "Finish" at ≥ 3) only appear once they're actually possible.
|
|
29
|
+
const draftVertexCount = useEditor((s) => s.draftVertexCount)
|
|
30
|
+
// Some hints are replaced by live contextual chips, so keep the generic
|
|
31
|
+
// registry renderer from duplicating stale/static versions.
|
|
32
|
+
const visible = hints.filter(
|
|
33
|
+
(hint) =>
|
|
34
|
+
!(hint.key === 'Shift' && hint.label === 'Cycle snapping mode') &&
|
|
35
|
+
(hint.minDraftVertices == null || draftVertexCount >= hint.minDraftVertices),
|
|
36
|
+
)
|
|
37
|
+
if (visible.length === 0 && !snapContext && !continuationContext) return null
|
|
38
|
+
// Hints carrying a live-state `chip` render as mode chips next to the
|
|
39
|
+
// snapping / continuation rows; the rest stay static key rows.
|
|
40
|
+
const chipHints = visible.filter((hint) => hint.chip)
|
|
41
|
+
const staticHints = visible.filter((hint) => !hint.chip)
|
|
15
42
|
return (
|
|
16
|
-
<
|
|
17
|
-
{
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
43
|
+
<ContextualHelperPanel
|
|
44
|
+
chipHints={chipHints}
|
|
45
|
+
hints={staticHints.map((hint) => {
|
|
46
|
+
// Shift is a per-kind bypass for opening / zone / duct placement ("Free
|
|
47
|
+
// place", "Free angle", …) — those flip to a bypassed state while held.
|
|
48
|
+
const isBypassHint = hint.key === 'Shift'
|
|
49
|
+
return {
|
|
50
|
+
keys: [hint.key],
|
|
51
|
+
label: shiftPressed && isBypassHint ? 'Guided constraints bypassed' : hint.label,
|
|
52
|
+
active: shiftPressed && isBypassHint,
|
|
53
|
+
}
|
|
54
|
+
})}
|
|
55
|
+
continuationContext={continuationContext}
|
|
56
|
+
snapContext={snapContext}
|
|
57
|
+
/>
|
|
24
58
|
)
|
|
25
59
|
}
|
|
@@ -1,16 +1,14 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { SnapContext } from '../../../lib/snapping-mode'
|
|
2
|
+
import { ContextualHelperPanel } from './contextual-helper-panel'
|
|
2
3
|
|
|
3
|
-
export function RoofHelper() {
|
|
4
|
+
export function RoofHelper({ snapContext }: { snapContext?: SnapContext | null }) {
|
|
4
5
|
return (
|
|
5
|
-
<
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
<span className="text-muted-foreground">Cancel</span>
|
|
13
|
-
</div>
|
|
14
|
-
</div>
|
|
6
|
+
<ContextualHelperPanel
|
|
7
|
+
hints={[
|
|
8
|
+
{ keys: ['Left click'], label: 'Set corner' },
|
|
9
|
+
{ keys: ['Esc'], label: 'Cancel' },
|
|
10
|
+
]}
|
|
11
|
+
snapContext={snapContext}
|
|
12
|
+
/>
|
|
15
13
|
)
|
|
16
14
|
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { Icon } from '@iconify/react'
|
|
4
|
+
import type { IconRef } from '@pascal-app/core'
|
|
5
|
+
import { type ComponentType, lazy, Suspense } from 'react'
|
|
6
|
+
|
|
7
|
+
// `React.lazy` must be called once per loader so the resolved component keeps
|
|
8
|
+
// a stable identity across renders (otherwise every parent re-render remounts
|
|
9
|
+
// the icon). Cache by the loader function — same pattern as the plugin-panel
|
|
10
|
+
// component cache.
|
|
11
|
+
const lazyIconCache = new WeakMap<() => Promise<{ default: ComponentType }>, ComponentType>()
|
|
12
|
+
|
|
13
|
+
function resolveLazyIcon(module: () => Promise<{ default: ComponentType }>): ComponentType {
|
|
14
|
+
const cached = lazyIconCache.get(module)
|
|
15
|
+
if (cached) return cached
|
|
16
|
+
const Lazy = lazy(module)
|
|
17
|
+
lazyIconCache.set(module, Lazy)
|
|
18
|
+
return Lazy
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Generic renderer for a registry {@link IconRef} — url / iconify / inline-svg
|
|
23
|
+
* marks are sized by `size` (px); `component`-kind icons size themselves.
|
|
24
|
+
* Shared by the quick-action menus; the icon rail and inspector keep their
|
|
25
|
+
* own copies with bespoke wrappers for now.
|
|
26
|
+
*/
|
|
27
|
+
export function IconRefGlyph({ icon, size = 16 }: { icon: IconRef; size?: number }) {
|
|
28
|
+
if (icon.kind === 'url') {
|
|
29
|
+
return <img alt="" className="shrink-0 object-contain" height={size} src={icon.src} width={size} />
|
|
30
|
+
}
|
|
31
|
+
if (icon.kind === 'iconify') {
|
|
32
|
+
return <Icon height={size} icon={icon.name} width={size} />
|
|
33
|
+
}
|
|
34
|
+
if (icon.kind === 'svg') {
|
|
35
|
+
return (
|
|
36
|
+
<svg height={size} viewBox={icon.viewBox} width={size}>
|
|
37
|
+
<path d={icon.path} fill="currentColor" />
|
|
38
|
+
</svg>
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
const LazyIcon = resolveLazyIcon(icon.module)
|
|
42
|
+
return (
|
|
43
|
+
<Suspense fallback={null}>
|
|
44
|
+
<LazyIcon />
|
|
45
|
+
</Suspense>
|
|
46
|
+
)
|
|
47
|
+
}
|
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import type { AssetInput } from '@pascal-app/core'
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
/**
|
|
4
|
+
* A catalog tile: the asset plus optional editor placement metadata.
|
|
5
|
+
* `tool` names the placement tool the tile arms (defaults to the generic
|
|
6
|
+
* `'item'` drop tool) — kinds drawn by their own registry tool (e.g. the
|
|
7
|
+
* modular cabinet) point at that tool id instead.
|
|
8
|
+
*/
|
|
9
|
+
export type CatalogItem = AssetInput & { tool?: string }
|
|
10
|
+
|
|
11
|
+
export const CATALOG_ITEMS: CatalogItem[] = [
|
|
4
12
|
{
|
|
5
13
|
id: 'cactus',
|
|
6
14
|
category: 'furniture',
|
|
@@ -437,6 +445,36 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
|
|
437
445
|
scale: [1, 1, 1],
|
|
438
446
|
surface: { height: 0.48 },
|
|
439
447
|
},
|
|
448
|
+
{
|
|
449
|
+
id: 'cabinet',
|
|
450
|
+
tool: 'cabinet',
|
|
451
|
+
category: 'furniture',
|
|
452
|
+
name: 'Modular Cabinet',
|
|
453
|
+
tags: [
|
|
454
|
+
'cabinet',
|
|
455
|
+
'cupboard',
|
|
456
|
+
'storage',
|
|
457
|
+
'kitchen',
|
|
458
|
+
'pantry',
|
|
459
|
+
'wood',
|
|
460
|
+
'timber',
|
|
461
|
+
'modern',
|
|
462
|
+
'contemporary',
|
|
463
|
+
'minimalist',
|
|
464
|
+
'organization',
|
|
465
|
+
'furniture',
|
|
466
|
+
],
|
|
467
|
+
thumbnail:
|
|
468
|
+
'https://byrpxoiotywskoojsrzd.supabase.co/storage/v1/object/public/items/system/kitchen-cabinet/thumbnail.png',
|
|
469
|
+
src: 'https://byrpxoiotywskoojsrzd.supabase.co/storage/v1/object/public/items/system/kitchen-cabinet/model.glb',
|
|
470
|
+
floorPlanUrl:
|
|
471
|
+
'https://byrpxoiotywskoojsrzd.supabase.co/storage/v1/object/public/items/system/kitchen-cabinet/floor-plan.png',
|
|
472
|
+
dimensions: [1.65, 1.09, 0.77],
|
|
473
|
+
offset: [0, 0.0004, 0],
|
|
474
|
+
rotation: [0, 0, 0],
|
|
475
|
+
scale: [1, 1, 1],
|
|
476
|
+
surface: { height: 1.09 },
|
|
477
|
+
},
|
|
440
478
|
{
|
|
441
479
|
id: 'sofa',
|
|
442
480
|
category: 'furniture',
|
|
@@ -1,18 +1,13 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
3
|
import type { AssetInput } from '@pascal-app/core'
|
|
4
|
-
import { resolveCdnUrl } from '@pascal-app/viewer'
|
|
5
|
-
import Image from 'next/image'
|
|
4
|
+
import { resolveCdnUrl, useViewer } from '@pascal-app/viewer'
|
|
6
5
|
import { useEffect } from 'react'
|
|
7
|
-
import {
|
|
8
|
-
Tooltip,
|
|
9
|
-
TooltipContent,
|
|
10
|
-
TooltipTrigger,
|
|
11
|
-
} from './../../../components/ui/primitives/tooltip'
|
|
12
6
|
import { triggerSFX } from './../../../lib/sfx-bus'
|
|
13
7
|
import { cn } from './../../../lib/utils'
|
|
14
8
|
import useEditor, { type CatalogCategory } from './../../../store/use-editor'
|
|
15
|
-
import {
|
|
9
|
+
import { resolveAssetSnapTarget, SnapTargetBadge } from '../snap-target-badge'
|
|
10
|
+
import { CATALOG_ITEMS, type CatalogItem } from './catalog-items'
|
|
16
11
|
|
|
17
12
|
export function ItemCatalog({
|
|
18
13
|
category,
|
|
@@ -41,9 +36,9 @@ export function ItemCatalog({
|
|
|
41
36
|
const setMode = useEditor((state) => state.setMode)
|
|
42
37
|
const setTool = useEditor((state) => state.setTool)
|
|
43
38
|
|
|
44
|
-
const sourceItems = itemsOverride ?? CATALOG_ITEMS
|
|
39
|
+
const sourceItems: CatalogItem[] = itemsOverride ?? CATALOG_ITEMS
|
|
45
40
|
// Server-provided results bypass all local filtering; otherwise filter by category/search/tags
|
|
46
|
-
const filteredItems =
|
|
41
|
+
const filteredItems: CatalogItem[] =
|
|
47
42
|
overrideItems ??
|
|
48
43
|
(() => {
|
|
49
44
|
const categoryItems = search
|
|
@@ -58,22 +53,6 @@ export function ItemCatalog({
|
|
|
58
53
|
})
|
|
59
54
|
})()
|
|
60
55
|
|
|
61
|
-
const categoryItems = filteredItems
|
|
62
|
-
|
|
63
|
-
// Auto-select first item if current selection is not in the filtered list
|
|
64
|
-
useEffect(() => {
|
|
65
|
-
const isCurrentItemInCategory = categoryItems.some((item) => item.src === selectedItem?.src)
|
|
66
|
-
if (!isCurrentItemInCategory && categoryItems.length > 0) {
|
|
67
|
-
setSelectedItem(categoryItems[0] as AssetInput)
|
|
68
|
-
}
|
|
69
|
-
}, [categoryItems, selectedItem?.src, setSelectedItem])
|
|
70
|
-
|
|
71
|
-
const getAttachmentIcon = (attachTo: AssetInput['attachTo']) => {
|
|
72
|
-
if (attachTo === 'wall' || attachTo === 'wall-side') return '/icons/wall.png'
|
|
73
|
-
if (attachTo === 'ceiling') return '/icons/ceiling.png'
|
|
74
|
-
return null
|
|
75
|
-
}
|
|
76
|
-
|
|
77
56
|
if (filteredItems.length === 0 && emptyState) {
|
|
78
57
|
return <>{emptyState}</>
|
|
79
58
|
}
|
|
@@ -86,7 +65,7 @@ export function ItemCatalog({
|
|
|
86
65
|
{leadingTile}
|
|
87
66
|
{filteredItems.map((item, index) => {
|
|
88
67
|
const isSelected = selectedItem?.src === item?.src
|
|
89
|
-
const
|
|
68
|
+
const snapTarget = resolveAssetSnapTarget(item?.attachTo)
|
|
90
69
|
return (
|
|
91
70
|
<button
|
|
92
71
|
className={cn(
|
|
@@ -96,8 +75,12 @@ export function ItemCatalog({
|
|
|
96
75
|
key={index}
|
|
97
76
|
onClick={() => {
|
|
98
77
|
triggerSFX('sfx:menu-click')
|
|
78
|
+
// Drop the current selection before arming placement — keeping
|
|
79
|
+
// it would route shortcuts (rotate & co) to both the ghost and
|
|
80
|
+
// the selected node.
|
|
81
|
+
useViewer.getState().setSelection({ selectedIds: [], zoneId: null })
|
|
99
82
|
setSelectedItem(item)
|
|
100
|
-
setTool('item')
|
|
83
|
+
setTool(item.tool ?? 'item')
|
|
101
84
|
setMode('build')
|
|
102
85
|
}}
|
|
103
86
|
onMouseEnter={() => triggerSFX('sfx:menu-hover')}
|
|
@@ -110,14 +93,8 @@ export function ItemCatalog({
|
|
|
110
93
|
loading="eager"
|
|
111
94
|
src={resolveCdnUrl(item.thumbnail) || ''}
|
|
112
95
|
/>
|
|
113
|
-
{
|
|
114
|
-
<
|
|
115
|
-
<img
|
|
116
|
-
alt={item.attachTo === 'ceiling' ? 'Ceiling attachment' : 'Wall attachment'}
|
|
117
|
-
className="h-4 w-4"
|
|
118
|
-
src={attachmentIcon}
|
|
119
|
-
/>
|
|
120
|
-
</div>
|
|
96
|
+
{snapTarget && (
|
|
97
|
+
<SnapTargetBadge className="absolute right-1 bottom-1" target={snapTarget} />
|
|
121
98
|
)}
|
|
122
99
|
</div>
|
|
123
100
|
<span className="truncate px-0.5 text-left font-medium text-[11px] text-muted-foreground group-hover:text-foreground">
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import type { LevelNode } from '@pascal-app/core'
|
|
4
4
|
import { useEffect, useState } from 'react'
|
|
5
5
|
import type { LevelDuplicatePreset } from '../../lib/level-duplication'
|
|
6
|
-
import { getLevelDisplayName } from '
|
|
6
|
+
import { getLevelDisplayName } from '@pascal-app/core'
|
|
7
7
|
import { cn } from '../../lib/utils'
|
|
8
8
|
import {
|
|
9
9
|
Dialog,
|