@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
|
@@ -6,26 +6,26 @@ export type NodeDisplay = {
|
|
|
6
6
|
}
|
|
7
7
|
|
|
8
8
|
const TYPE_DEFAULTS: Record<string, NodeDisplay> = {
|
|
9
|
-
item: { icon: '/icons/
|
|
10
|
-
wall: { icon: '/icons/wall.
|
|
11
|
-
door: { icon: '/icons/door.
|
|
12
|
-
window: { icon: '/icons/window.
|
|
13
|
-
slab: { icon: '/icons/floor.
|
|
14
|
-
ceiling: { icon: '/icons/ceiling.
|
|
15
|
-
column: { icon: '/icons/column.
|
|
16
|
-
elevator: { icon: '/icons/elevator.
|
|
17
|
-
fence: { icon: '/icons/fence.
|
|
18
|
-
roof: { icon: '/icons/roof.
|
|
19
|
-
'roof-segment': { icon: '/icons/roof.
|
|
20
|
-
stair: { icon: '/icons/
|
|
21
|
-
'stair-segment': { icon: '/icons/
|
|
22
|
-
scan: { icon: '/icons/mesh.
|
|
23
|
-
guide: { icon: '/icons/floorplan.
|
|
9
|
+
item: { icon: '/icons/item.webp', label: 'Item' },
|
|
10
|
+
wall: { icon: '/icons/wall.webp', label: 'Wall' },
|
|
11
|
+
door: { icon: '/icons/door.webp', label: 'Door' },
|
|
12
|
+
window: { icon: '/icons/window.webp', label: 'Window' },
|
|
13
|
+
slab: { icon: '/icons/floor.webp', label: 'Slab' },
|
|
14
|
+
ceiling: { icon: '/icons/ceiling.webp', label: 'Ceiling' },
|
|
15
|
+
column: { icon: '/icons/column.webp', label: 'Column' },
|
|
16
|
+
elevator: { icon: '/icons/elevator.webp', label: 'Elevator' },
|
|
17
|
+
fence: { icon: '/icons/fence.webp', label: 'Fence' },
|
|
18
|
+
roof: { icon: '/icons/roof.webp', label: 'Roof' },
|
|
19
|
+
'roof-segment': { icon: '/icons/roof.webp', label: 'Roof segment' },
|
|
20
|
+
stair: { icon: '/icons/stairs.webp', label: 'Stair' },
|
|
21
|
+
'stair-segment': { icon: '/icons/stairs.webp', label: 'Stair segment' },
|
|
22
|
+
scan: { icon: '/icons/mesh.webp', label: '3D Scan' },
|
|
23
|
+
guide: { icon: '/icons/floorplan.webp', label: 'Guide image' },
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
export function getNodeDisplay(node: AnyNode | null | undefined): NodeDisplay {
|
|
27
|
-
if (!node) return { icon: '/icons/select.
|
|
28
|
-
const fallback = TYPE_DEFAULTS[node.type] ?? { icon: '/icons/select.
|
|
27
|
+
if (!node) return { icon: '/icons/select.webp', label: 'Selection' }
|
|
28
|
+
const fallback = TYPE_DEFAULTS[node.type] ?? { icon: '/icons/select.webp', label: node.type }
|
|
29
29
|
// Item nodes carry an asset with its own thumbnail/name
|
|
30
30
|
if (node.type === 'item') {
|
|
31
31
|
return {
|
|
@@ -29,7 +29,7 @@ import useEditor from '../../../store/use-editor'
|
|
|
29
29
|
import { MobilePanelSheet } from './mobile-panel-sheet'
|
|
30
30
|
import { MobileSelectionBar } from './mobile-selection-bar'
|
|
31
31
|
import { getNodeDisplay } from './node-display'
|
|
32
|
-
import {
|
|
32
|
+
import { resetDesktopInspectorCollapsed } from './panel-wrapper'
|
|
33
33
|
import { ParametricInspector } from './parametric-inspector'
|
|
34
34
|
import { ReferencePanel } from './reference-panel'
|
|
35
35
|
|
|
@@ -174,9 +174,6 @@ export function PanelManager({ inspectorFooter }: { inspectorFooter?: React.Reac
|
|
|
174
174
|
const selectedZoneId = useViewer((s) => s.selection.zoneId)
|
|
175
175
|
const setSelection = useViewer((s) => s.setSelection)
|
|
176
176
|
const selectedReferenceId = useEditor((s) => s.selectedReferenceId)
|
|
177
|
-
const isPaintPanelOpen = useEditor((s) => s.isPaintPanelOpen)
|
|
178
|
-
const mode = useEditor((s) => s.mode)
|
|
179
|
-
const activePaintMaterial = useEditor((s) => s.activePaintMaterial)
|
|
180
177
|
// Only subscribe to the *type* of the single-selected node — string primitive
|
|
181
178
|
// so we don't re-render on unrelated scene mutations.
|
|
182
179
|
const selectedNodeType = useScene((s) => {
|
|
@@ -190,6 +187,27 @@ export function PanelManager({ inspectorFooter }: { inspectorFooter?: React.Reac
|
|
|
190
187
|
return id ? (s.nodes[id as AnyNodeId] ?? null) : null
|
|
191
188
|
})
|
|
192
189
|
|
|
190
|
+
// Node and reference selection are mutually exclusive: selecting a guide
|
|
191
|
+
// clears the node selection (handleGuideSelect), but node selection never
|
|
192
|
+
// cleared a lingering reference — so clicking a wall with a floorplan
|
|
193
|
+
// selected kept showing the reference panel. Clear the stale reference the
|
|
194
|
+
// moment a scene selection appears.
|
|
195
|
+
const setSelectedReferenceId = useEditor((s) => s.setSelectedReferenceId)
|
|
196
|
+
useEffect(() => {
|
|
197
|
+
if (selectedIds.length > 0 || selectedZoneId) {
|
|
198
|
+
setSelectedReferenceId(null)
|
|
199
|
+
}
|
|
200
|
+
}, [selectedIds, selectedZoneId, setSelectedReferenceId])
|
|
201
|
+
|
|
202
|
+
// The inspector's expanded state is shared across panel swaps, but a fresh
|
|
203
|
+
// selection after everything was deselected should open collapsed again.
|
|
204
|
+
const hasAnySelection = selectedIds.length > 0 || Boolean(selectedZoneId) || Boolean(selectedReferenceId)
|
|
205
|
+
useEffect(() => {
|
|
206
|
+
if (!hasAnySelection) {
|
|
207
|
+
resetDesktopInspectorCollapsed()
|
|
208
|
+
}
|
|
209
|
+
}, [hasAnySelection])
|
|
210
|
+
|
|
193
211
|
if (isMobile) {
|
|
194
212
|
if (selectedReferenceId) {
|
|
195
213
|
return <MobilePanelLayer isReference={true} node={null} panel={<ReferencePanel />} />
|
|
@@ -208,15 +226,6 @@ export function PanelManager({ inspectorFooter }: { inspectorFooter?: React.Reac
|
|
|
208
226
|
return <ReferencePanel />
|
|
209
227
|
}
|
|
210
228
|
|
|
211
|
-
if (
|
|
212
|
-
isPaintPanelOpen &&
|
|
213
|
-
mode === 'material-paint' &&
|
|
214
|
-
activePaintMaterial?.material?.properties &&
|
|
215
|
-
!activePaintMaterial.materialPreset
|
|
216
|
-
) {
|
|
217
|
-
return <PaintPanel />
|
|
218
|
-
}
|
|
219
|
-
|
|
220
229
|
if (selectedZoneId && selectedIds.length === 0) {
|
|
221
230
|
return (
|
|
222
231
|
<ParametricInspector
|
|
@@ -17,6 +17,14 @@ const DRAG_MARGIN = 8
|
|
|
17
17
|
// Pointer travel (px) below which a header press is treated as a click
|
|
18
18
|
// (toggles collapse) rather than a drag.
|
|
19
19
|
const CLICK_SLOP = 4
|
|
20
|
+
let desktopInspectorCollapsed = true
|
|
21
|
+
|
|
22
|
+
/** Forget the shared expanded state. Called when the last selection clears so
|
|
23
|
+
* a fresh selection opens the inspector collapsed — the sharing is only meant
|
|
24
|
+
* to survive swaps between panels (roof ↔ segment), not a close/reopen. */
|
|
25
|
+
export function resetDesktopInspectorCollapsed() {
|
|
26
|
+
desktopInspectorCollapsed = true
|
|
27
|
+
}
|
|
20
28
|
|
|
21
29
|
function clamp(value: number, min: number, max: number): number {
|
|
22
30
|
return Math.min(Math.max(value, min), Math.max(min, max))
|
|
@@ -52,7 +60,7 @@ export const InspectorFooterContext = createContext<React.ReactNode>(null)
|
|
|
52
60
|
|
|
53
61
|
interface PanelWrapperProps {
|
|
54
62
|
title: string
|
|
55
|
-
/** Either a URL path (legacy panels pass `/icons/floor.
|
|
63
|
+
/** Either a URL path (legacy panels pass `/icons/floor.webp` etc.,
|
|
56
64
|
* rendered via next/image) OR a React node (registry-driven
|
|
57
65
|
* inspector renders `<Icon icon="lucide:fence" />` from
|
|
58
66
|
* `def.presentation.icon`). */
|
|
@@ -85,8 +93,21 @@ export function PanelWrapper({
|
|
|
85
93
|
const panelRef = useRef<HTMLDivElement>(null)
|
|
86
94
|
|
|
87
95
|
// The whole panel is collapsed to just its header by default; the chevron
|
|
88
|
-
// expands it to reveal the inspector body.
|
|
89
|
-
|
|
96
|
+
// expands it to reveal the inspector body. Keep the desktop value shared
|
|
97
|
+
// across inspector swaps (roof ↔ segment, etc.) so navigating between
|
|
98
|
+
// related panels preserves whether the user left the inspector open.
|
|
99
|
+
const [collapsed, setCollapsedState] = useState(desktopInspectorCollapsed)
|
|
100
|
+
|
|
101
|
+
const setCollapsed = useCallback(
|
|
102
|
+
(next: boolean | ((previous: boolean) => boolean)) => {
|
|
103
|
+
setCollapsedState((previous) => {
|
|
104
|
+
const resolved = typeof next === 'function' ? next(previous) : next
|
|
105
|
+
desktopInspectorCollapsed = resolved
|
|
106
|
+
return resolved
|
|
107
|
+
})
|
|
108
|
+
},
|
|
109
|
+
[],
|
|
110
|
+
)
|
|
90
111
|
|
|
91
112
|
// Drag-to-reposition from the header. `offset` is a translation applied on
|
|
92
113
|
// top of the default `top-20 right-4` anchor; null until first dragged.
|
|
@@ -62,9 +62,22 @@ export function ParametricInspector({
|
|
|
62
62
|
const handleUpdate = useCallback(
|
|
63
63
|
(patch: Partial<AnyNode>) => {
|
|
64
64
|
if (!selectedId) return
|
|
65
|
-
useScene.getState()
|
|
65
|
+
const scene = useScene.getState()
|
|
66
|
+
const node = scene.nodes[selectedId]
|
|
67
|
+
if (parametrics?.derive && node) {
|
|
68
|
+
const next = { ...node, ...patch } as AnyNode
|
|
69
|
+
patch = { ...patch, ...parametrics.derive(next, patch) }
|
|
70
|
+
}
|
|
71
|
+
// Bundle the edited node + any reconcile follow-ups into ONE
|
|
72
|
+
// updateNodes call so a single inspector edit is a single undo step.
|
|
73
|
+
const updates: { id: AnyNodeId; data: Partial<AnyNode> }[] = [{ id: selectedId, data: patch }]
|
|
74
|
+
if (parametrics?.reconcile && node) {
|
|
75
|
+
const next = { ...node, ...patch } as AnyNode
|
|
76
|
+
updates.push(...parametrics.reconcile(node as AnyNode, next))
|
|
77
|
+
}
|
|
78
|
+
scene.updateNodes(updates)
|
|
66
79
|
},
|
|
67
|
-
[selectedId],
|
|
80
|
+
[selectedId, parametrics],
|
|
68
81
|
)
|
|
69
82
|
|
|
70
83
|
const clearSelection = useCallback(() => {
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
import { useCallback, useEffect, useRef, useState } from 'react'
|
|
23
23
|
import { guideEmitter } from '../../../lib/guide-events'
|
|
24
24
|
import { getGuideImageName } from '../../../lib/local-guide-image'
|
|
25
|
+
import { cn } from '../../../lib/utils'
|
|
25
26
|
import useEditor from '../../../store/use-editor'
|
|
26
27
|
import { ActionButton, ActionGroup } from '../controls/action-button'
|
|
27
28
|
import { PanelSection } from '../controls/panel-section'
|
|
@@ -60,6 +61,9 @@ export function ReferencePanel() {
|
|
|
60
61
|
? (s.nodes[selectedReferenceId as AnyNode['id']] as ReferenceNode | undefined)
|
|
61
62
|
: undefined,
|
|
62
63
|
)
|
|
64
|
+
const isScaleFlowActive = useEditor(
|
|
65
|
+
(s) => s.referenceScaleActiveGuideId !== null && s.referenceScaleActiveGuideId === node?.id,
|
|
66
|
+
)
|
|
63
67
|
|
|
64
68
|
const handleUpdate = useCallback(
|
|
65
69
|
(updates: Partial<ReferenceNode>) => {
|
|
@@ -98,6 +102,9 @@ export function ReferencePanel() {
|
|
|
98
102
|
} as Partial<GuideNode>,
|
|
99
103
|
)
|
|
100
104
|
setGuideScaleReferenceVisible(selectedReferenceId, true)
|
|
105
|
+
// The new image starts uncalibrated — drop the calibration auto-lock
|
|
106
|
+
// so it can be resized/rotated right away.
|
|
107
|
+
setGuideLocked(selectedReferenceId, false)
|
|
101
108
|
} catch {
|
|
102
109
|
setReplaceError('Could not replace that image.')
|
|
103
110
|
} finally {
|
|
@@ -123,6 +130,13 @@ export function ReferencePanel() {
|
|
|
123
130
|
return
|
|
124
131
|
}
|
|
125
132
|
|
|
133
|
+
// The scale line is drawn on the 2D plan — starting from a 3D-only view
|
|
134
|
+
// would arm the flow invisibly inside the hidden floorplan panel.
|
|
135
|
+
const editor = useEditor.getState()
|
|
136
|
+
if (editor.viewMode === '3d') {
|
|
137
|
+
editor.setViewMode('2d')
|
|
138
|
+
}
|
|
139
|
+
|
|
126
140
|
guideEmitter.emit('guide:set-reference-scale', { guideId: node.id })
|
|
127
141
|
}, [node])
|
|
128
142
|
|
|
@@ -233,33 +247,54 @@ export function ReferencePanel() {
|
|
|
233
247
|
|
|
234
248
|
<PanelSection title="Reference Scale">
|
|
235
249
|
<div className="flex items-center gap-2 rounded-md border border-border/50 bg-background/40 px-2.5 py-2 text-sm">
|
|
236
|
-
<Ruler
|
|
250
|
+
<Ruler
|
|
251
|
+
className={cn(
|
|
252
|
+
'h-4 w-4 shrink-0',
|
|
253
|
+
node.scaleReference ? 'text-primary' : 'text-amber-600 dark:text-amber-400',
|
|
254
|
+
)}
|
|
255
|
+
/>
|
|
237
256
|
<span className="truncate text-muted-foreground">{scaleStatus}</span>
|
|
238
257
|
</div>
|
|
239
258
|
|
|
240
|
-
|
|
241
|
-
<
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
259
|
+
{!node.scaleReference && (
|
|
260
|
+
<p className="px-0.5 text-muted-foreground text-xs leading-snug">
|
|
261
|
+
{isScaleFlowActive
|
|
262
|
+
? 'Click both ends of a known distance on the plan, then type its real length.'
|
|
263
|
+
: 'Draw a line over a known dimension on the plan, then type its real length to scale the image exactly.'}
|
|
264
|
+
</p>
|
|
265
|
+
)}
|
|
247
266
|
|
|
248
267
|
<ActionGroup>
|
|
249
268
|
<ActionButton
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
label="Clear Scale"
|
|
260
|
-
onClick={() => handleUpdate({ scaleReference: null } as Partial<GuideNode>)}
|
|
269
|
+
className={cn(
|
|
270
|
+
!node.scaleReference &&
|
|
271
|
+
!isScaleFlowActive &&
|
|
272
|
+
'border-primary/50 bg-primary/15 text-primary hover:bg-primary/25 active:bg-primary/25',
|
|
273
|
+
)}
|
|
274
|
+
label={
|
|
275
|
+
isScaleFlowActive ? 'Cancel' : node.scaleReference ? 'Edit Scale' : 'Set Scale'
|
|
276
|
+
}
|
|
277
|
+
onClick={isScaleFlowActive ? handleCancelScale : handleStartScale}
|
|
261
278
|
/>
|
|
262
279
|
</ActionGroup>
|
|
280
|
+
|
|
281
|
+
{node.scaleReference && (
|
|
282
|
+
<ActionGroup>
|
|
283
|
+
<ActionButton
|
|
284
|
+
label={scaleReferenceVisible ? 'Hide Scale' : 'Show Scale'}
|
|
285
|
+
onClick={() => setGuideScaleReferenceVisible(node.id, !scaleReferenceVisible)}
|
|
286
|
+
/>
|
|
287
|
+
<ActionButton
|
|
288
|
+
label="Clear Scale"
|
|
289
|
+
onClick={() => {
|
|
290
|
+
handleUpdate({ scaleReference: null } as Partial<GuideNode>)
|
|
291
|
+
// Calibrating auto-locked the guide; clearing the scale
|
|
292
|
+
// returns it to a freely-editable reference.
|
|
293
|
+
setGuideLocked(node.id, false)
|
|
294
|
+
}}
|
|
295
|
+
/>
|
|
296
|
+
</ActionGroup>
|
|
297
|
+
)}
|
|
263
298
|
</PanelSection>
|
|
264
299
|
|
|
265
300
|
<PanelSection title="Quick Actions">
|
|
@@ -22,6 +22,17 @@ const MOUSE_SHORTCUTS = {
|
|
|
22
22
|
},
|
|
23
23
|
} as const
|
|
24
24
|
|
|
25
|
+
// The platform-agnostic command modifier. Both Cmd and Ctrl bind the action; we
|
|
26
|
+
// render the symbol for the *current* device so the hint reads native (⌘ on Mac,
|
|
27
|
+
// Ctrl elsewhere) without implying only one of them works.
|
|
28
|
+
const COMMAND_VALUES = new Set(['Cmd/Ctrl', 'Cmd', 'Command', 'Meta'])
|
|
29
|
+
|
|
30
|
+
// Resolved once on the client at module load — the editor HUD is client-only, so
|
|
31
|
+
// there's no server render to mismatch against. `navigator.platform` is enough
|
|
32
|
+
// here and matches the detection used elsewhere (floorplan rotate hint).
|
|
33
|
+
const IS_MAC =
|
|
34
|
+
typeof navigator !== 'undefined' && navigator.platform.toUpperCase().includes('MAC')
|
|
35
|
+
|
|
25
36
|
type ShortcutTokenProps = React.ComponentProps<'kbd'> & {
|
|
26
37
|
value: string
|
|
27
38
|
displayValue?: string
|
|
@@ -30,16 +41,23 @@ type ShortcutTokenProps = React.ComponentProps<'kbd'> & {
|
|
|
30
41
|
function ShortcutToken({ className, displayValue, value, ...props }: ShortcutTokenProps) {
|
|
31
42
|
const mouseShortcut =
|
|
32
43
|
value in MOUSE_SHORTCUTS ? MOUSE_SHORTCUTS[value as keyof typeof MOUSE_SHORTCUTS] : null
|
|
44
|
+
const isCommand = COMMAND_VALUES.has(value)
|
|
45
|
+
const isShift = value === 'Shift'
|
|
46
|
+
const commandDisplay = IS_MAC ? '⌘' : 'Ctrl'
|
|
47
|
+
const commandLabel = IS_MAC ? 'Command' : 'Control'
|
|
33
48
|
|
|
34
49
|
return (
|
|
35
50
|
<kbd
|
|
36
|
-
aria-label={
|
|
51
|
+
aria-label={
|
|
52
|
+
mouseShortcut?.label ??
|
|
53
|
+
(isCommand ? commandLabel : isShift ? 'Shift' : (displayValue ?? value))
|
|
54
|
+
}
|
|
37
55
|
className={cn(
|
|
38
56
|
'inline-flex h-6 items-center rounded border border-border bg-muted px-2 font-medium font-mono text-[11px] text-muted-foreground',
|
|
39
|
-
mouseShortcut && 'justify-center px-1.5',
|
|
57
|
+
(mouseShortcut || isShift) && 'justify-center px-1.5',
|
|
40
58
|
className,
|
|
41
59
|
)}
|
|
42
|
-
title={mouseShortcut?.label ?? value}
|
|
60
|
+
title={mouseShortcut?.label ?? (isCommand ? commandLabel : isShift ? 'Shift' : value)}
|
|
43
61
|
{...props}
|
|
44
62
|
>
|
|
45
63
|
{mouseShortcut ? (
|
|
@@ -54,6 +72,24 @@ function ShortcutToken({ className, displayValue, value, ...props }: ShortcutTok
|
|
|
54
72
|
/>
|
|
55
73
|
<span className="sr-only">{mouseShortcut.label}</span>
|
|
56
74
|
</>
|
|
75
|
+
) : isShift ? (
|
|
76
|
+
// Icon rather than the ⇧ text glyph — the font renders the glyph's
|
|
77
|
+
// crossbar too high to read as the shift key symbol.
|
|
78
|
+
<>
|
|
79
|
+
<Icon
|
|
80
|
+
aria-hidden="true"
|
|
81
|
+
className="shrink-0"
|
|
82
|
+
color="currentColor"
|
|
83
|
+
height={13}
|
|
84
|
+
icon="ph:arrow-fat-up"
|
|
85
|
+
width={13}
|
|
86
|
+
/>
|
|
87
|
+
<span className="sr-only">Shift</span>
|
|
88
|
+
</>
|
|
89
|
+
) : isCommand ? (
|
|
90
|
+
// The ⌘ glyph reads small next to letters at the same font size, so bump
|
|
91
|
+
// it up a touch on Mac. "Ctrl" stays at the token's normal size.
|
|
92
|
+
<span className={IS_MAC ? 'text-[13px] leading-none' : undefined}>{commandDisplay}</span>
|
|
57
93
|
) : (
|
|
58
94
|
(displayValue ?? value)
|
|
59
95
|
)}
|
|
@@ -16,6 +16,7 @@ import useEditor from './../../../store/use-editor'
|
|
|
16
16
|
import { type ExtraPanel, IconRail } from './icon-rail'
|
|
17
17
|
import { SettingsPanel, type SettingsPanelProps } from './panels/settings-panel'
|
|
18
18
|
import { SitePanel, type SitePanelProps } from './panels/site-panel'
|
|
19
|
+
import { useHostPanels } from './use-plugin-panels'
|
|
19
20
|
|
|
20
21
|
interface AppSidebarProps {
|
|
21
22
|
appMenuButton?: ReactNode
|
|
@@ -31,9 +32,12 @@ export function AppSidebar({
|
|
|
31
32
|
sidebarTop,
|
|
32
33
|
settingsPanelProps,
|
|
33
34
|
sitePanelProps,
|
|
34
|
-
extraPanels,
|
|
35
|
+
extraPanels: hostExtraPanels,
|
|
35
36
|
commandPaletteEmptyAction,
|
|
36
37
|
}: AppSidebarProps) {
|
|
38
|
+
// Host-provided panels merged with registered host panels — the icon rail and
|
|
39
|
+
// content area treat both identically.
|
|
40
|
+
const extraPanels = useHostPanels(hostExtraPanels)
|
|
37
41
|
const activePanel = useEditor((s) => s.activeSidebarPanel)
|
|
38
42
|
const setActivePanel = useEditor((s) => s.setActiveSidebarPanel)
|
|
39
43
|
const hasActivePanel =
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
+
import { Plus } from 'lucide-react'
|
|
3
4
|
import type { ComponentType, ReactNode } from 'react'
|
|
4
5
|
import {
|
|
5
6
|
Tooltip,
|
|
@@ -10,7 +11,13 @@ import { cn } from './../../../lib/utils'
|
|
|
10
11
|
|
|
11
12
|
export type PanelId = string
|
|
12
13
|
|
|
13
|
-
export type ExtraPanel = {
|
|
14
|
+
export type ExtraPanel = {
|
|
15
|
+
id: string
|
|
16
|
+
icon: ReactNode
|
|
17
|
+
label: string
|
|
18
|
+
component: ComponentType
|
|
19
|
+
pluginId?: string
|
|
20
|
+
}
|
|
14
21
|
|
|
15
22
|
interface IconRailProps {
|
|
16
23
|
activePanel: PanelId
|
|
@@ -22,13 +29,13 @@ interface IconRailProps {
|
|
|
22
29
|
|
|
23
30
|
const sitePanel: { id: PanelId; iconSrc: string; label: string } = {
|
|
24
31
|
id: 'site',
|
|
25
|
-
iconSrc: '/icons/level.
|
|
32
|
+
iconSrc: '/icons/level.webp',
|
|
26
33
|
label: 'Site',
|
|
27
34
|
}
|
|
28
35
|
|
|
29
36
|
const settingsPanel: { id: PanelId; iconSrc: string; label: string } = {
|
|
30
37
|
id: 'settings',
|
|
31
|
-
iconSrc: '/icons/settings.
|
|
38
|
+
iconSrc: '/icons/settings.webp',
|
|
32
39
|
label: 'Settings',
|
|
33
40
|
}
|
|
34
41
|
|
|
@@ -41,6 +48,38 @@ export function IconRail({
|
|
|
41
48
|
extraPanels,
|
|
42
49
|
className,
|
|
43
50
|
}: IconRailProps) {
|
|
51
|
+
const regularExtraPanels = extraPanels?.filter((panel) => !panel.pluginId && panel.id !== 'plugins')
|
|
52
|
+
const pluginPanels = extraPanels?.filter((panel) => panel.pluginId)
|
|
53
|
+
const pluginsPanel = extraPanels?.find((panel) => panel.id === 'plugins')
|
|
54
|
+
|
|
55
|
+
const renderExtraPanel = (panel: ExtraPanel) => {
|
|
56
|
+
const isActive = activePanel === panel.id
|
|
57
|
+
return (
|
|
58
|
+
<Tooltip key={panel.id}>
|
|
59
|
+
<TooltipTrigger asChild>
|
|
60
|
+
<button
|
|
61
|
+
className={cn(
|
|
62
|
+
'flex h-9 w-9 items-center justify-center rounded-lg transition-all',
|
|
63
|
+
isActive ? 'bg-accent' : 'hover:bg-accent',
|
|
64
|
+
)}
|
|
65
|
+
onClick={() => onPanelChange(panel.id)}
|
|
66
|
+
type="button"
|
|
67
|
+
>
|
|
68
|
+
<span
|
|
69
|
+
className={cn(
|
|
70
|
+
'flex h-6 w-6 items-center justify-center transition-all',
|
|
71
|
+
!isActive && 'opacity-50',
|
|
72
|
+
)}
|
|
73
|
+
>
|
|
74
|
+
{panel.id === 'plugins' ? <Plus className="h-5 w-5" /> : panel.icon}
|
|
75
|
+
</span>
|
|
76
|
+
</button>
|
|
77
|
+
</TooltipTrigger>
|
|
78
|
+
<TooltipContent side="right">{panel.label}</TooltipContent>
|
|
79
|
+
</Tooltip>
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
|
|
44
83
|
return (
|
|
45
84
|
<div
|
|
46
85
|
className={cn(
|
|
@@ -83,34 +122,7 @@ export function IconRail({
|
|
|
83
122
|
)
|
|
84
123
|
})}
|
|
85
124
|
|
|
86
|
-
{
|
|
87
|
-
{extraPanels?.map((panel) => {
|
|
88
|
-
const isActive = activePanel === panel.id
|
|
89
|
-
return (
|
|
90
|
-
<Tooltip key={panel.id}>
|
|
91
|
-
<TooltipTrigger asChild>
|
|
92
|
-
<button
|
|
93
|
-
className={cn(
|
|
94
|
-
'flex h-9 w-9 items-center justify-center rounded-lg transition-all',
|
|
95
|
-
isActive ? 'bg-accent' : 'hover:bg-accent',
|
|
96
|
-
)}
|
|
97
|
-
onClick={() => onPanelChange(panel.id)}
|
|
98
|
-
type="button"
|
|
99
|
-
>
|
|
100
|
-
<span
|
|
101
|
-
className={cn(
|
|
102
|
-
'flex h-6 w-6 items-center justify-center transition-all',
|
|
103
|
-
!isActive && 'opacity-50',
|
|
104
|
-
)}
|
|
105
|
-
>
|
|
106
|
-
{panel.icon}
|
|
107
|
-
</span>
|
|
108
|
-
</button>
|
|
109
|
-
</TooltipTrigger>
|
|
110
|
-
<TooltipContent side="right">{panel.label}</TooltipContent>
|
|
111
|
-
</Tooltip>
|
|
112
|
-
)
|
|
113
|
-
})}
|
|
125
|
+
{regularExtraPanels?.map(renderExtraPanel)}
|
|
114
126
|
|
|
115
127
|
{/* Settings panel */}
|
|
116
128
|
{[settingsPanel].map((panel) => {
|
|
@@ -140,6 +152,13 @@ export function IconRail({
|
|
|
140
152
|
</Tooltip>
|
|
141
153
|
)
|
|
142
154
|
})}
|
|
155
|
+
|
|
156
|
+
{(pluginPanels?.length || pluginsPanel) && (
|
|
157
|
+
<div className="mt-1 flex w-9 flex-col items-center gap-1 border-border/70 border-t pt-2">
|
|
158
|
+
{pluginPanels?.map(renderExtraPanel)}
|
|
159
|
+
{pluginsPanel && renderExtraPanel(pluginsPanel)}
|
|
160
|
+
</div>
|
|
161
|
+
)}
|
|
143
162
|
</div>
|
|
144
163
|
)
|
|
145
164
|
}
|