@pascal-app/editor 1.0.0-beta.5 → 1.0.0
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 +13 -7
- package/scripts/generate-print-golden-house.ts +89 -0
- package/src/components/editor/bake-exporter.tsx +1 -0
- 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 +42 -51
- package/src/components/editor/floorplan-panel.tsx +76 -12
- 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/linear-resize-drag.ts +67 -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 +18 -1
- package/src/components/editor/index.tsx +140 -21
- package/src/components/editor/node-action-menu.tsx +14 -1
- package/src/components/editor/node-arrow-handles.tsx +47 -58
- package/src/components/editor/selection-manager.tsx +104 -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 +284 -237
- 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-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 +180 -32
- 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/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/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 +135 -2
- 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 +6 -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 +26 -4
- package/src/components/tools/item/use-placement-coordinator.tsx +428 -153
- package/src/components/tools/registered-tool-install-lifecycle.test.tsx +256 -0
- package/src/components/tools/registry/move-registry-node-tool.test.ts +10 -0
- package/src/components/tools/registry/move-registry-node-tool.tsx +306 -110
- 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 +45 -8
- 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/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 +9 -12
- 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/scene-material-list.tsx +6 -5
- package/src/components/ui/controls/segmented-control.tsx +8 -1
- package/src/components/ui/controls/slider-control.tsx +19 -3
- 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 +22 -5
- 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 +263 -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 +29 -162
- package/src/components/ui/panels/reference-panel.tsx +31 -1
- 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 +390 -36
- 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 +140 -0
- package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +109 -0
- package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +19 -5
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +116 -46
- 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/tree-node.test.ts +8 -0
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +14 -6
- package/src/components/ui/sidebar/tab-bar.tsx +2 -0
- package/src/components/ui/sidebar/use-plugin-panels.tsx +17 -2
- package/src/components/viewer/viewer-scene-header.tsx +2 -1
- package/src/components/viewer/viewer-stage.tsx +4 -1
- 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 +122 -0
- 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 +129 -62
- package/src/hooks/use-registered-tool-enabled.ts +20 -0
- package/src/hooks/use-save-shortcut.ts +33 -0
- package/src/index.tsx +82 -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/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 +397 -1
- package/src/lib/floorplan/floorplan-export.tsx +111 -16
- 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 +1 -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/glb-export.test.ts +1169 -21
- package/src/lib/glb-export.ts +629 -101
- 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-print-export.test.ts +484 -0
- package/src/lib/level-print-export.ts +526 -0
- 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/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 +366 -0
- package/src/lib/portable-export.ts +969 -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 +330 -0
- package/src/lib/print-shell-compiler-manifold-worker.ts +187 -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 +30 -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.ts +22 -8
- package/src/lib/selection-routing.test.ts +105 -1
- package/src/lib/selection-routing.ts +19 -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/usdz-export.ts +38 -0
- 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-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/components/tools/roof/roof-tool.tsx +0 -656
- package/src/components/ui/helpers/roof-helper.tsx +0 -14
|
@@ -35,7 +35,7 @@ export type MaterialPaintPanelProps = {
|
|
|
35
35
|
export function MaterialPaintPanel({ onCreateMaterialRequest }: MaterialPaintPanelProps) {
|
|
36
36
|
const activePaintMaterial = useEditor((state) => state.activePaintMaterial)
|
|
37
37
|
const activePaintTarget = useEditor((state) => state.activePaintTarget)
|
|
38
|
-
const
|
|
38
|
+
const armMaterialPaint = useEditor((state) => state.armMaterialPaint)
|
|
39
39
|
const setActivePaintTarget = useEditor((state) => state.setActivePaintTarget)
|
|
40
40
|
const paintEraser = useEditor((state) => state.paintEraser)
|
|
41
41
|
const setPaintEraser = useEditor((state) => state.setPaintEraser)
|
|
@@ -81,7 +81,7 @@ export function MaterialPaintPanel({ onCreateMaterialRequest }: MaterialPaintPan
|
|
|
81
81
|
},
|
|
82
82
|
},
|
|
83
83
|
})
|
|
84
|
-
|
|
84
|
+
armMaterialPaint({ materialPreset: toSceneMaterialRef(id), sourceTarget: activePaintTarget })
|
|
85
85
|
setAutoEditMaterialId(id)
|
|
86
86
|
}
|
|
87
87
|
|
|
@@ -112,11 +112,13 @@ export function MaterialPaintPanel({ onCreateMaterialRequest }: MaterialPaintPan
|
|
|
112
112
|
</div>
|
|
113
113
|
|
|
114
114
|
{/* Scrolls: category tabs (fixed inside) + catalog grid (the scroll). */}
|
|
115
|
-
|
|
115
|
+
{/* A stable hook for host-app onboarding to point at. Static, and read
|
|
116
|
+
only from outside: nothing here depends on it. */}
|
|
117
|
+
<div className="min-h-0 flex-1" data-guide-target="paint-material">
|
|
116
118
|
<MaterialPicker
|
|
117
119
|
onCreateMaterialRequest={onCreateMaterialRequest}
|
|
118
120
|
onSelectMaterialPreset={(materialPreset) => {
|
|
119
|
-
|
|
121
|
+
armMaterialPaint({ materialPreset, sourceTarget: activePaintTarget })
|
|
120
122
|
}}
|
|
121
123
|
selectedMaterialPreset={activePaintMaterial?.materialPreset}
|
|
122
124
|
/>
|
|
@@ -17,7 +17,7 @@ import { Plus } from 'lucide-react'
|
|
|
17
17
|
import { useEffect, useMemo, useState, useSyncExternalStore } from 'react'
|
|
18
18
|
import { triggerSFX } from '../../../lib/sfx-bus'
|
|
19
19
|
|
|
20
|
-
export type MaterialSourceFilter =
|
|
20
|
+
export type MaterialSourceFilter = MaterialSource
|
|
21
21
|
|
|
22
22
|
export type MaterialPickerProps = {
|
|
23
23
|
selectedMaterialPreset?: string
|
|
@@ -28,8 +28,9 @@ export type MaterialPickerProps = {
|
|
|
28
28
|
onCreateMaterialRequest?: () => void
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
// No 'All': the browse surfaces (Items / Rooms / Build) dropped it and default
|
|
32
|
+
// to the Pascal library — the combined list buried the curated set.
|
|
31
33
|
const SOURCE_FILTERS: { id: MaterialSourceFilter; label: string }[] = [
|
|
32
|
-
{ id: 'all', label: 'All' },
|
|
33
34
|
{ id: 'pascal', label: 'Pascal' },
|
|
34
35
|
{ id: 'mine', label: 'Mine' },
|
|
35
36
|
{ id: 'workspace', label: 'Workspace' },
|
|
@@ -41,7 +42,6 @@ function getCategoryLabel(category: (typeof MATERIAL_CATEGORIES)[number]) {
|
|
|
41
42
|
}
|
|
42
43
|
|
|
43
44
|
function filterBySource(items: MaterialCatalogItem[], filter: MaterialSourceFilter) {
|
|
44
|
-
if (filter === 'all') return items
|
|
45
45
|
return items.filter((item) => (item.source ?? 'pascal') === filter)
|
|
46
46
|
}
|
|
47
47
|
|
|
@@ -60,7 +60,7 @@ export function MaterialPicker({
|
|
|
60
60
|
const [selectedCategory, setSelectedCategory] = useState<(typeof MATERIAL_CATEGORIES)[number]>(
|
|
61
61
|
MATERIAL_CATEGORIES[0],
|
|
62
62
|
)
|
|
63
|
-
const [sourceFilter, setSourceFilter] = useState<MaterialSourceFilter>('
|
|
63
|
+
const [sourceFilter, setSourceFilter] = useState<MaterialSourceFilter>('pascal')
|
|
64
64
|
// Version counter so host registrations/unregistrations re-render the picker.
|
|
65
65
|
const libraryVersion = useSyncExternalStore(
|
|
66
66
|
subscribeLibraryMaterials,
|
|
@@ -33,7 +33,7 @@ export function SceneMaterialList({ autoEditId }: { autoEditId?: SceneMaterialId
|
|
|
33
33
|
const removeSceneMaterial = useScene((state) => state.removeSceneMaterial)
|
|
34
34
|
const activePaintTarget = useEditor((state) => state.activePaintTarget)
|
|
35
35
|
const activePaintRef = useEditor((state) => state.activePaintMaterial?.materialPreset)
|
|
36
|
-
const
|
|
36
|
+
const armMaterialPaint = useEditor((state) => state.armMaterialPaint)
|
|
37
37
|
|
|
38
38
|
const materialEntries = useMemo(
|
|
39
39
|
() => Object.entries(materials) as [SceneMaterialId, SceneMaterial][],
|
|
@@ -76,7 +76,7 @@ export function SceneMaterialList({ autoEditId }: { autoEditId?: SceneMaterialId
|
|
|
76
76
|
key={id}
|
|
77
77
|
removeSceneMaterial={removeSceneMaterial}
|
|
78
78
|
sceneMaterial={sceneMaterial}
|
|
79
|
-
|
|
79
|
+
armMaterialPaint={armMaterialPaint}
|
|
80
80
|
updateSceneMaterial={updateSceneMaterial}
|
|
81
81
|
usageCount={usageCounts.get(id) ?? 0}
|
|
82
82
|
/>
|
|
@@ -95,7 +95,7 @@ function SceneMaterialRow({
|
|
|
95
95
|
addSceneMaterial,
|
|
96
96
|
updateSceneMaterial,
|
|
97
97
|
removeSceneMaterial,
|
|
98
|
-
|
|
98
|
+
armMaterialPaint,
|
|
99
99
|
}: {
|
|
100
100
|
id: SceneMaterialId
|
|
101
101
|
sceneMaterial: SceneMaterial
|
|
@@ -106,7 +106,7 @@ function SceneMaterialRow({
|
|
|
106
106
|
addSceneMaterial: ReturnType<typeof useScene.getState>['addSceneMaterial']
|
|
107
107
|
updateSceneMaterial: ReturnType<typeof useScene.getState>['updateSceneMaterial']
|
|
108
108
|
removeSceneMaterial: ReturnType<typeof useScene.getState>['removeSceneMaterial']
|
|
109
|
-
|
|
109
|
+
armMaterialPaint: ReturnType<typeof useEditor.getState>['armMaterialPaint']
|
|
110
110
|
}) {
|
|
111
111
|
// A freshly-created material (via "+ Custom") mounts with its editor open.
|
|
112
112
|
const [isEditingMaterial, setIsEditingMaterial] = useState(autoEdit)
|
|
@@ -141,6 +141,7 @@ function SceneMaterialRow({
|
|
|
141
141
|
className={`rounded-md border border-border/60 bg-background/40 p-2 ${
|
|
142
142
|
isActive ? 'ring-1 ring-primary ring-inset' : ''
|
|
143
143
|
}`}
|
|
144
|
+
data-testid={`scene-material-row-${id}`}
|
|
144
145
|
>
|
|
145
146
|
<div className="flex items-center gap-2">
|
|
146
147
|
<span
|
|
@@ -174,7 +175,7 @@ function SceneMaterialRow({
|
|
|
174
175
|
<Button
|
|
175
176
|
aria-label="Paint with"
|
|
176
177
|
onClick={() =>
|
|
177
|
-
|
|
178
|
+
armMaterialPaint({
|
|
178
179
|
materialPreset: toSceneMaterialRef(id),
|
|
179
180
|
sourceTarget: activePaintTarget,
|
|
180
181
|
})
|
|
@@ -7,6 +7,8 @@ interface SegmentedControlProps<T extends string> {
|
|
|
7
7
|
onChange: (value: T) => void
|
|
8
8
|
options: { label: React.ReactNode; value: T }[]
|
|
9
9
|
className?: string
|
|
10
|
+
disabled?: boolean
|
|
11
|
+
mixed?: boolean
|
|
10
12
|
}
|
|
11
13
|
|
|
12
14
|
export function SegmentedControl<T extends string>({
|
|
@@ -14,16 +16,19 @@ export function SegmentedControl<T extends string>({
|
|
|
14
16
|
onChange,
|
|
15
17
|
options,
|
|
16
18
|
className,
|
|
19
|
+
disabled = false,
|
|
20
|
+
mixed = false,
|
|
17
21
|
}: SegmentedControlProps<T>) {
|
|
18
22
|
return (
|
|
19
23
|
<div
|
|
20
24
|
className={cn(
|
|
21
25
|
'flex h-9 w-full items-center rounded-lg border border-border/50 bg-[#2C2C2E] p-[3px]',
|
|
26
|
+
disabled && 'opacity-60',
|
|
22
27
|
className,
|
|
23
28
|
)}
|
|
24
29
|
>
|
|
25
30
|
{options.map((option) => {
|
|
26
|
-
const isSelected = value === option.value
|
|
31
|
+
const isSelected = !mixed && value === option.value
|
|
27
32
|
return (
|
|
28
33
|
<button
|
|
29
34
|
className={cn(
|
|
@@ -31,7 +36,9 @@ export function SegmentedControl<T extends string>({
|
|
|
31
36
|
isSelected
|
|
32
37
|
? 'bg-[#3e3e3e] text-foreground shadow-sm ring-1 ring-border/50'
|
|
33
38
|
: 'text-muted-foreground hover:bg-white/5 hover:text-foreground',
|
|
39
|
+
disabled && 'cursor-not-allowed hover:bg-transparent hover:text-muted-foreground',
|
|
34
40
|
)}
|
|
41
|
+
disabled={disabled}
|
|
35
42
|
key={option.value}
|
|
36
43
|
onClick={() => onChange(option.value)}
|
|
37
44
|
type="button"
|
|
@@ -22,6 +22,7 @@ interface SliderControlProps {
|
|
|
22
22
|
className?: string
|
|
23
23
|
unit?: string
|
|
24
24
|
restoreOnCommit?: boolean
|
|
25
|
+
mixed?: boolean
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
function stepPrecision(s: number): number {
|
|
@@ -64,6 +65,7 @@ export function SliderControl({
|
|
|
64
65
|
className,
|
|
65
66
|
unit = '',
|
|
66
67
|
restoreOnCommit = true,
|
|
68
|
+
mixed = false,
|
|
67
69
|
}: SliderControlProps) {
|
|
68
70
|
// Display/storage conversion so the value honors the metric/imperial toggle.
|
|
69
71
|
// `value`, `onChange`, `onCommit`, `min`/`max`/`clamp` are always in the
|
|
@@ -76,6 +78,10 @@ export function SliderControl({
|
|
|
76
78
|
const [isDragging, setIsDragging] = useState(false)
|
|
77
79
|
const [isHovered, setIsHovered] = useState(false)
|
|
78
80
|
const [inputValue, setInputValue] = useState(toDisplay(value).toFixed(precision))
|
|
81
|
+
// Live readout while dragging. Multi-edit previews write live overrides
|
|
82
|
+
// instead of the scene, so `value` would otherwise stay frozen even though
|
|
83
|
+
// the meshes are moving.
|
|
84
|
+
const [dragDisplay, setDragDisplay] = useState<number | null>(null)
|
|
79
85
|
|
|
80
86
|
const dragRef = useRef<{
|
|
81
87
|
// Original value at drag start — preserved across modifier re-anchors so
|
|
@@ -89,8 +95,9 @@ export function SliderControl({
|
|
|
89
95
|
stepMultiplier: number
|
|
90
96
|
} | null>(null)
|
|
91
97
|
const labelRef = useRef<HTMLDivElement>(null)
|
|
92
|
-
const
|
|
93
|
-
valueRef
|
|
98
|
+
const shown = dragDisplay ?? value
|
|
99
|
+
const valueRef = useRef(shown)
|
|
100
|
+
valueRef.current = shown
|
|
94
101
|
|
|
95
102
|
const clamp = useCallback((val: number) => Math.min(Math.max(val, min), max), [min, max])
|
|
96
103
|
// Apply a signed display-unit delta to a stored value, rounding in the
|
|
@@ -185,6 +192,7 @@ export function SliderControl({
|
|
|
185
192
|
const newValue = applyDisplayDelta(anchorValue, (dx / 4) * s, s)
|
|
186
193
|
if (newValue !== valueRef.current) {
|
|
187
194
|
valueRef.current = newValue
|
|
195
|
+
setDragDisplay(newValue)
|
|
188
196
|
onChange(newValue)
|
|
189
197
|
}
|
|
190
198
|
},
|
|
@@ -209,6 +217,7 @@ export function SliderControl({
|
|
|
209
217
|
useScene.temporal.getState().resume()
|
|
210
218
|
onCommit?.(finalVal)
|
|
211
219
|
}
|
|
220
|
+
setDragDisplay(null)
|
|
212
221
|
},
|
|
213
222
|
[onChange, onCommit, restoreOnCommit],
|
|
214
223
|
)
|
|
@@ -272,7 +281,7 @@ export function SliderControl({
|
|
|
272
281
|
[submitValue, value, precision, step, applyDisplayDelta, onChange, toDisplay],
|
|
273
282
|
)
|
|
274
283
|
|
|
275
|
-
const displayValue = toDisplay(
|
|
284
|
+
const displayValue = toDisplay(shown)
|
|
276
285
|
|
|
277
286
|
return (
|
|
278
287
|
<div
|
|
@@ -340,6 +349,13 @@ export function SliderControl({
|
|
|
340
349
|
</span>
|
|
341
350
|
)}
|
|
342
351
|
</>
|
|
352
|
+
) : mixed && !isDragging ? (
|
|
353
|
+
<div
|
|
354
|
+
className="flex cursor-text items-center text-muted-foreground transition-colors hover:text-foreground"
|
|
355
|
+
onClick={handleValueClick}
|
|
356
|
+
>
|
|
357
|
+
<span className="font-mono tracking-tight">Mixed</span>
|
|
358
|
+
</div>
|
|
343
359
|
) : (
|
|
344
360
|
<div
|
|
345
361
|
className="flex cursor-text items-center text-foreground/60 transition-colors hover:text-foreground"
|
|
@@ -8,9 +8,16 @@ interface ToggleControlProps {
|
|
|
8
8
|
checked: boolean
|
|
9
9
|
onChange: (checked: boolean) => void
|
|
10
10
|
className?: string
|
|
11
|
+
mixed?: boolean
|
|
11
12
|
}
|
|
12
13
|
|
|
13
|
-
export function ToggleControl({
|
|
14
|
+
export function ToggleControl({
|
|
15
|
+
label,
|
|
16
|
+
checked,
|
|
17
|
+
onChange,
|
|
18
|
+
className,
|
|
19
|
+
mixed = false,
|
|
20
|
+
}: ToggleControlProps) {
|
|
14
21
|
return (
|
|
15
22
|
<div
|
|
16
23
|
className={cn(
|
|
@@ -26,12 +33,18 @@ export function ToggleControl({ label, checked, onChange, className }: ToggleCon
|
|
|
26
33
|
<div
|
|
27
34
|
className={cn(
|
|
28
35
|
'flex h-5 w-5 items-center justify-center rounded-[4px] border transition-all duration-200',
|
|
29
|
-
|
|
30
|
-
? 'border-
|
|
31
|
-
:
|
|
36
|
+
mixed
|
|
37
|
+
? 'border-muted-foreground/70 bg-black/20 text-muted-foreground'
|
|
38
|
+
: checked
|
|
39
|
+
? 'border-primary bg-primary text-primary-foreground'
|
|
40
|
+
: 'border-border bg-black/20 text-transparent group-hover:border-muted-foreground',
|
|
32
41
|
)}
|
|
33
42
|
>
|
|
34
|
-
|
|
43
|
+
{mixed ? (
|
|
44
|
+
<div className="h-0.5 w-2.5 rounded-full bg-current" />
|
|
45
|
+
) : (
|
|
46
|
+
<Check className="h-3.5 w-3.5" strokeWidth={3} />
|
|
47
|
+
)}
|
|
35
48
|
</div>
|
|
36
49
|
</div>
|
|
37
50
|
)
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { nodeRegistry, type ToolOption } from '@pascal-app/core'
|
|
4
|
+
import Image from 'next/image'
|
|
5
|
+
import { useSyncExternalStore } from 'react'
|
|
6
|
+
import { cn } from '../../../lib/utils'
|
|
7
|
+
import { triggerSFX } from '../../../lib/sfx-bus'
|
|
8
|
+
|
|
9
|
+
const ALWAYS_VISIBLE = {
|
|
10
|
+
subscribe: () => () => {},
|
|
11
|
+
value: () => true,
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function ToolOptionRow({
|
|
15
|
+
option,
|
|
16
|
+
onSelect,
|
|
17
|
+
getChoiceThumbnail,
|
|
18
|
+
active = true,
|
|
19
|
+
}: {
|
|
20
|
+
option: ToolOption
|
|
21
|
+
onSelect?: (option: ToolOption, value: string) => void
|
|
22
|
+
getChoiceThumbnail?: (option: ToolOption, value: string) => string | undefined
|
|
23
|
+
active?: boolean
|
|
24
|
+
}) {
|
|
25
|
+
const visibility = option.visible ?? ALWAYS_VISIBLE
|
|
26
|
+
const visible = useSyncExternalStore(visibility.subscribe, visibility.value, visibility.value)
|
|
27
|
+
const value = useSyncExternalStore(option.subscribe, option.value, option.value)
|
|
28
|
+
if (
|
|
29
|
+
!visible ||
|
|
30
|
+
(!active && !option.choices.some((choice) => getChoiceThumbnail?.(option, choice.value)))
|
|
31
|
+
) return null
|
|
32
|
+
|
|
33
|
+
const activeChoice = active && option.choices.find((choice) => choice.value === value)
|
|
34
|
+
return (
|
|
35
|
+
<div className="flex flex-col gap-2">
|
|
36
|
+
<div className="px-0.5 font-medium text-muted-foreground text-xs">{option.label}</div>
|
|
37
|
+
<div
|
|
38
|
+
className="grid gap-1.5"
|
|
39
|
+
style={{
|
|
40
|
+
gridTemplateColumns: `repeat(${Math.min(option.choices.length, 3)}, minmax(0, 1fr))`,
|
|
41
|
+
}}
|
|
42
|
+
>
|
|
43
|
+
{option.choices.map((choice) => {
|
|
44
|
+
const selected = active && choice.value === value
|
|
45
|
+
const thumbnail = getChoiceThumbnail?.(option, choice.value)
|
|
46
|
+
return (
|
|
47
|
+
<button
|
|
48
|
+
aria-pressed={selected}
|
|
49
|
+
className={cn(
|
|
50
|
+
'rounded-lg px-2 py-2 text-center font-medium text-xs transition-colors',
|
|
51
|
+
selected
|
|
52
|
+
? 'bg-primary/10 text-primary ring-1 ring-primary/50'
|
|
53
|
+
: 'bg-muted/40 text-muted-foreground hover:bg-muted hover:text-foreground',
|
|
54
|
+
)}
|
|
55
|
+
key={choice.value}
|
|
56
|
+
onClick={() => {
|
|
57
|
+
triggerSFX('sfx:menu-click')
|
|
58
|
+
option.set(choice.value)
|
|
59
|
+
onSelect?.(option, choice.value)
|
|
60
|
+
}}
|
|
61
|
+
onMouseEnter={() => triggerSFX('sfx:menu-hover')}
|
|
62
|
+
type="button"
|
|
63
|
+
>
|
|
64
|
+
{thumbnail && (
|
|
65
|
+
<Image
|
|
66
|
+
alt=""
|
|
67
|
+
className={cn(
|
|
68
|
+
'mx-auto mb-1 size-14 object-contain',
|
|
69
|
+
!selected && 'opacity-70 grayscale',
|
|
70
|
+
)}
|
|
71
|
+
height={56}
|
|
72
|
+
src={thumbnail}
|
|
73
|
+
width={56}
|
|
74
|
+
/>
|
|
75
|
+
)}
|
|
76
|
+
{choice.label}
|
|
77
|
+
</button>
|
|
78
|
+
)
|
|
79
|
+
})}
|
|
80
|
+
</div>
|
|
81
|
+
{activeChoice && activeChoice.description ? (
|
|
82
|
+
<p className="px-0.5 text-[11px] text-muted-foreground leading-relaxed">
|
|
83
|
+
{activeChoice.description}
|
|
84
|
+
</p>
|
|
85
|
+
) : null}
|
|
86
|
+
</div>
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* The pick-one option rows a kind declares via `def.toolOptions` (e.g. the
|
|
92
|
+
* roof's 'Create from: Draw / Room'), for whichever sidebar the host mounts
|
|
93
|
+
* it in — the standalone Build tab and the community Build sidebar both get
|
|
94
|
+
* every kind's options with no per-kind wiring. Renders nothing for kinds
|
|
95
|
+
* without options. Selecting a choice only writes the kind's own state;
|
|
96
|
+
* hosts that want selection to also arm the tool pass `onSelect`.
|
|
97
|
+
*/
|
|
98
|
+
export function ToolOptionsPanel({
|
|
99
|
+
kind,
|
|
100
|
+
className,
|
|
101
|
+
onSelect,
|
|
102
|
+
getChoiceThumbnail,
|
|
103
|
+
active = true,
|
|
104
|
+
}: {
|
|
105
|
+
kind: string | null | undefined
|
|
106
|
+
className?: string
|
|
107
|
+
onSelect?: (option: ToolOption, value: string) => void
|
|
108
|
+
getChoiceThumbnail?: (option: ToolOption, value: string) => string | undefined
|
|
109
|
+
active?: boolean
|
|
110
|
+
}) {
|
|
111
|
+
const options = (kind ? nodeRegistry.get(kind)?.toolOptions : undefined) ?? []
|
|
112
|
+
if (options.length === 0) return null
|
|
113
|
+
return (
|
|
114
|
+
<div className={cn('flex flex-col gap-3', className)}>
|
|
115
|
+
{options.map((option) => (
|
|
116
|
+
<ToolOptionRow
|
|
117
|
+
active={active}
|
|
118
|
+
getChoiceThumbnail={getChoiceThumbnail}
|
|
119
|
+
key={option.id}
|
|
120
|
+
onSelect={onSelect}
|
|
121
|
+
option={option}
|
|
122
|
+
/>
|
|
123
|
+
))}
|
|
124
|
+
</div>
|
|
125
|
+
)
|
|
126
|
+
}
|
|
@@ -27,7 +27,7 @@ import {
|
|
|
27
27
|
LevelNode,
|
|
28
28
|
useScene,
|
|
29
29
|
} from '@pascal-app/core'
|
|
30
|
-
import { useViewer } from '@pascal-app/viewer'
|
|
30
|
+
import { markPerfAction, useViewer } from '@pascal-app/viewer'
|
|
31
31
|
import { ClipboardPaste, Copy, GripVertical, MoreVertical, Plus, Trash2 } from 'lucide-react'
|
|
32
32
|
import {
|
|
33
33
|
type ButtonHTMLAttributes,
|
|
@@ -150,6 +150,9 @@ function LevelRow({
|
|
|
150
150
|
const storeyHeight = getStoredLevelHeight(level)
|
|
151
151
|
// toFixed(2) + strip one trailing zero: "2.50" → "2.5", "2.75" stays.
|
|
152
152
|
const storeyHeightLabel = `${toDisplay(storeyHeight).toFixed(2).replace(/0$/, '')} ${displayUnit}`
|
|
153
|
+
// Same rule as the site panel and command palette: the ordinal-0 ground
|
|
154
|
+
// floor is the vertical model's zero anchor and must never be deletable.
|
|
155
|
+
const canDeleteLevel = level.level !== 0
|
|
153
156
|
|
|
154
157
|
// Clean preset values per display system; imperial stores exact meters
|
|
155
158
|
// for whole-foot storey heights.
|
|
@@ -304,11 +307,13 @@ function LevelRow({
|
|
|
304
307
|
</button>
|
|
305
308
|
)}
|
|
306
309
|
<button
|
|
307
|
-
className="flex w-full items-center gap-2 rounded-md px-2.5 py-1.5 text-left text-muted-foreground text-xs transition-colors hover:bg-white/10 hover:text-red-400"
|
|
310
|
+
className="flex w-full items-center gap-2 rounded-md px-2.5 py-1.5 text-left text-muted-foreground text-xs transition-colors enabled:hover:bg-white/10 enabled:hover:text-red-400 disabled:cursor-not-allowed disabled:opacity-50"
|
|
311
|
+
disabled={!canDeleteLevel}
|
|
308
312
|
onClick={(e) => {
|
|
309
313
|
e.stopPropagation()
|
|
310
314
|
onRequestDelete()
|
|
311
315
|
}}
|
|
316
|
+
title={canDeleteLevel ? 'Delete level' : 'The ground level cannot be deleted'}
|
|
312
317
|
type="button"
|
|
313
318
|
>
|
|
314
319
|
<Trash2 className="h-3 w-3" />
|
|
@@ -572,6 +577,9 @@ export function FloatingLevelSelector() {
|
|
|
572
577
|
{!draggingLevelId && (
|
|
573
578
|
<button
|
|
574
579
|
className={cn(addButtonClass, 'top-0 -translate-y-1/2')}
|
|
580
|
+
// A stable hook for host-app onboarding to point at. Static, and
|
|
581
|
+
// read only from outside: nothing here depends on it.
|
|
582
|
+
data-guide-target="level-add"
|
|
575
583
|
onClick={handleAddAbove}
|
|
576
584
|
title="Add level above"
|
|
577
585
|
type="button"
|
|
@@ -608,20 +616,29 @@ export function FloatingLevelSelector() {
|
|
|
608
616
|
const showGapBelow = i < reversedLevels.length - 1
|
|
609
617
|
|
|
610
618
|
return (
|
|
611
|
-
<div
|
|
619
|
+
<div
|
|
620
|
+
className="relative"
|
|
621
|
+
// A stable hook for host-app onboarding to point at, on
|
|
622
|
+
// the ground floor only — the one level a guide can name
|
|
623
|
+
// without knowing the building. Static, and read only
|
|
624
|
+
// from outside: nothing here depends on it.
|
|
625
|
+
data-guide-target={level.level === 0 ? 'level-ground' : undefined}
|
|
626
|
+
key={level.id}
|
|
627
|
+
>
|
|
612
628
|
<SortableLevelRow
|
|
613
629
|
isSelected={isSelected}
|
|
614
630
|
level={level}
|
|
615
631
|
onDuplicate={(preset) => handleDuplicateLevel(level, preset)}
|
|
616
632
|
onPaste={() => handlePasteToLevel(level)}
|
|
617
633
|
onRequestDelete={() => setDeletingLevel(level)}
|
|
618
|
-
onSelect={() =>
|
|
634
|
+
onSelect={() => {
|
|
635
|
+
if (!isSelected) markPerfAction('level-switch', level.id)
|
|
619
636
|
setSelection(
|
|
620
637
|
resolvedBuildingId
|
|
621
638
|
? { buildingId: resolvedBuildingId, levelId: level.id }
|
|
622
639
|
: { levelId: level.id },
|
|
623
640
|
)
|
|
624
|
-
}
|
|
641
|
+
}}
|
|
625
642
|
/>
|
|
626
643
|
|
|
627
644
|
{showGapBelow && !draggingLevelId && (
|
|
@@ -90,6 +90,7 @@ function ShortcutSequence({
|
|
|
90
90
|
function ChipRow({
|
|
91
91
|
ariaLabel,
|
|
92
92
|
disabled = false,
|
|
93
|
+
guideTarget,
|
|
93
94
|
icon,
|
|
94
95
|
label,
|
|
95
96
|
onClick,
|
|
@@ -98,6 +99,11 @@ function ChipRow({
|
|
|
98
99
|
}: {
|
|
99
100
|
ariaLabel?: string
|
|
100
101
|
disabled?: boolean
|
|
102
|
+
/**
|
|
103
|
+
* A static hook for a host app's first-run tour to point at, written to
|
|
104
|
+
* `data-guide-target`. Nothing here reads it.
|
|
105
|
+
*/
|
|
106
|
+
guideTarget?: string
|
|
101
107
|
icon?: string
|
|
102
108
|
label: string
|
|
103
109
|
onClick?: () => void
|
|
@@ -130,6 +136,7 @@ function ChipRow({
|
|
|
130
136
|
'pointer-events-auto cursor-pointer items-center rounded-md text-left transition-colors hover:bg-muted/60',
|
|
131
137
|
disabled && 'opacity-45 saturate-0',
|
|
132
138
|
)}
|
|
139
|
+
data-guide-target={guideTarget}
|
|
133
140
|
onClick={onClick}
|
|
134
141
|
type="button"
|
|
135
142
|
>
|
|
@@ -181,6 +188,7 @@ function SnappingChips({ context }: { context: SnapContext }) {
|
|
|
181
188
|
<>
|
|
182
189
|
<ChipRow
|
|
183
190
|
ariaLabel={`Snapping: ${SNAPPING_MODE_LABELS[snappingMode]}`}
|
|
191
|
+
guideTarget="snap-mode"
|
|
184
192
|
icon={SNAPPING_MODE_ICONS[snappingMode]}
|
|
185
193
|
label={`Snapping: ${SNAPPING_MODE_LABELS[snappingMode]}`}
|
|
186
194
|
onClick={() => {
|
|
@@ -193,6 +201,7 @@ function SnappingChips({ context }: { context: SnapContext }) {
|
|
|
193
201
|
{gridActive ? (
|
|
194
202
|
<ChipRow
|
|
195
203
|
ariaLabel={`Grid step: ${gridSnapStep.toFixed(2)} m`}
|
|
204
|
+
guideTarget="snap-grid-step"
|
|
196
205
|
label={`Grid: ${gridSnapStep.toFixed(2)} m`}
|
|
197
206
|
onClick={() => {
|
|
198
207
|
setGridSnapStep(nextGridSnapStep(gridSnapStep))
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
useScene,
|
|
9
9
|
} from '@pascal-app/core'
|
|
10
10
|
import { useViewer } from '@pascal-app/viewer'
|
|
11
|
-
import { useEffect, useMemo, useState } from 'react'
|
|
11
|
+
import { useEffect, useMemo, useState, useSyncExternalStore } from 'react'
|
|
12
12
|
import { useShallow } from 'zustand/react/shallow'
|
|
13
13
|
import { useIsMobile } from '../../../hooks/use-mobile'
|
|
14
14
|
import {
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
resolveRotateHandleHelpHints,
|
|
20
20
|
resolveSelectModeHelpHints,
|
|
21
21
|
} from '../../../lib/contextual-help'
|
|
22
|
+
import { getContextualHelpNodeExtension } from '../../../lib/contextual-help-extension'
|
|
22
23
|
import { continuationContextOf } from '../../../lib/continuation'
|
|
23
24
|
import { canDirectMoveNode, canDirectRotateNode } from '../../../lib/direct-manipulation'
|
|
24
25
|
import type { ReshapeKind } from '../../../lib/interaction/scope'
|
|
@@ -33,7 +34,6 @@ import { BuildingHelper } from './building-helper'
|
|
|
33
34
|
import { ContextualHelperPanel } from './contextual-helper-panel'
|
|
34
35
|
import { ItemHelper } from './item-helper'
|
|
35
36
|
import { RegisteredToolHelper } from './registered-tool-helper'
|
|
36
|
-
import { RoofHelper } from './roof-helper'
|
|
37
37
|
|
|
38
38
|
// Reshaping a selected node's geometry (endpoint / curve / polygon corner). The
|
|
39
39
|
// snapping chip is the main control; these just name the gesture + Esc.
|
|
@@ -88,6 +88,9 @@ type ActiveModifierKeys = {
|
|
|
88
88
|
shift: boolean
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
+
const EMPTY_CONTEXTUAL_HINTS: ContextualShortcutHint[] = []
|
|
92
|
+
const NO_CONTEXTUAL_HELP_SUBSCRIPTION = () => () => {}
|
|
93
|
+
|
|
91
94
|
function useActiveModifierKeys(): ActiveModifierKeys {
|
|
92
95
|
const [modifiers, setModifiers] = useState<ActiveModifierKeys>({
|
|
93
96
|
command: false,
|
|
@@ -143,6 +146,21 @@ export function HelperManager() {
|
|
|
143
146
|
.filter((node): node is AnyNode => node !== undefined),
|
|
144
147
|
),
|
|
145
148
|
)
|
|
149
|
+
const contextualHelpNode =
|
|
150
|
+
scope.kind === 'mesh-editing'
|
|
151
|
+
? selectedNodes.find((node) => node.id === scope.nodeId) ?? null
|
|
152
|
+
: null
|
|
153
|
+
const contextualHelpExtension = contextualHelpNode
|
|
154
|
+
? getContextualHelpNodeExtension(nodeRegistry.get(contextualHelpNode.type))
|
|
155
|
+
: undefined
|
|
156
|
+
const contextualEditHints = useSyncExternalStore(
|
|
157
|
+
contextualHelpExtension?.subscribe ?? NO_CONTEXTUAL_HELP_SUBSCRIPTION,
|
|
158
|
+
() =>
|
|
159
|
+
contextualHelpNode
|
|
160
|
+
? (contextualHelpExtension?.getHints(contextualHelpNode.id) ?? EMPTY_CONTEXTUAL_HINTS)
|
|
161
|
+
: EMPTY_CONTEXTUAL_HINTS,
|
|
162
|
+
() => EMPTY_CONTEXTUAL_HINTS,
|
|
163
|
+
)
|
|
146
164
|
// The snapping context for whatever's active (wall / item / polygon) — drives
|
|
147
165
|
// which snapping chips the HUD shows, derived once and shared by every branch.
|
|
148
166
|
const snapContext = useMemo(
|
|
@@ -152,6 +170,10 @@ export function HelperManager() {
|
|
|
152
170
|
mode,
|
|
153
171
|
tool,
|
|
154
172
|
profileOf: (typeOrTool) => nodeRegistry.get(typeOrTool)?.snapProfile,
|
|
173
|
+
profileOfNode: (nodeId) => {
|
|
174
|
+
const node = useScene.getState().nodes[nodeId as AnyNodeId]
|
|
175
|
+
return node ? nodeRegistry.get(node.type)?.snapProfile : undefined
|
|
176
|
+
},
|
|
155
177
|
draftDirectionalOf: (typeOrTool) => nodeRegistry.get(typeOrTool)?.snapDraftDirectional ?? true,
|
|
156
178
|
}),
|
|
157
179
|
[scope, mode, tool],
|
|
@@ -212,6 +234,22 @@ export function HelperManager() {
|
|
|
212
234
|
)
|
|
213
235
|
}
|
|
214
236
|
|
|
237
|
+
// A single-node resize arrow is still an active snapping interaction. Roof
|
|
238
|
+
// width/depth handles opt into grid snapping, so keep the mode and grid-step
|
|
239
|
+
// controls visible for the whole drag instead of falling through to idle
|
|
240
|
+
// selection hints.
|
|
241
|
+
if (activeHandleDrag) {
|
|
242
|
+
return (
|
|
243
|
+
<ContextualHelperPanel
|
|
244
|
+
hints={[
|
|
245
|
+
{ keys: ['Drag'], label: 'Resize' },
|
|
246
|
+
{ keys: ['Esc'], label: 'Cancel' },
|
|
247
|
+
]}
|
|
248
|
+
snapContext={snapContext}
|
|
249
|
+
/>
|
|
250
|
+
)
|
|
251
|
+
}
|
|
252
|
+
|
|
215
253
|
// Reshaping a node's geometry (endpoint / curve / polygon corner). Checked
|
|
216
254
|
// before the select branch so the idle "drag selected / add objects" hints
|
|
217
255
|
// never leak over an in-progress reshape — and it gets its own snapping chip.
|
|
@@ -227,13 +265,13 @@ export function HelperManager() {
|
|
|
227
265
|
const movingContinuationContext = isFreshPlacementMetadata(movingNode.metadata)
|
|
228
266
|
? continuationContextOf(movingNode.type)
|
|
229
267
|
: null
|
|
230
|
-
|
|
231
|
-
|
|
268
|
+
const collisionValidatesDrop =
|
|
269
|
+
nodeRegistry.get(movingNode.type)?.capabilities.floorPlaced?.collides === true
|
|
232
270
|
return (
|
|
233
271
|
<ItemHelper
|
|
234
272
|
continuationContext={movingContinuationContext}
|
|
235
273
|
showEsc
|
|
236
|
-
showForce={
|
|
274
|
+
showForce={collisionValidatesDrop}
|
|
237
275
|
snapContext={snapContext}
|
|
238
276
|
/>
|
|
239
277
|
)
|
|
@@ -255,6 +293,10 @@ export function HelperManager() {
|
|
|
255
293
|
return <ContextualHelperPanel hints={terrainSculptHints(terrainVerb, terrainSampling)} />
|
|
256
294
|
}
|
|
257
295
|
|
|
296
|
+
if (scope.kind === 'mesh-editing') {
|
|
297
|
+
return <ContextualHelperPanel hints={contextualEditHints} snapContext={snapContext} />
|
|
298
|
+
}
|
|
299
|
+
|
|
258
300
|
// Idle select only — an active scope (handle-drag, box-select, …) must not show
|
|
259
301
|
// the idle selection hints.
|
|
260
302
|
if (mode === 'select' && scope.kind === 'idle') {
|
|
@@ -273,12 +315,6 @@ export function HelperManager() {
|
|
|
273
315
|
)
|
|
274
316
|
}
|
|
275
317
|
|
|
276
|
-
// Legacy fallback — only `roof` remains because it hasn't migrated to
|
|
277
|
-
// `def.tool` / `def.toolHints` yet (no Stage D port). Checked before the
|
|
278
|
-
// generic tool branch so the snap-context fallback below doesn't capture it
|
|
279
|
-
// and drop its bespoke `RoofHelper` hints. When roof migrates, this deletes.
|
|
280
|
-
if (tool === 'roof') return <RoofHelper snapContext={snapContext} />
|
|
281
|
-
|
|
282
318
|
// Registry-first: a kind renders the generic `RegisteredToolHelper` when it
|
|
283
319
|
// declares `def.toolHints`, OR whenever its draft resolves to a snap /
|
|
284
320
|
// continuation context — so a snappable tool with NO hand-written hints (e.g.
|
|
@@ -6,7 +6,7 @@ interface ItemHelperProps {
|
|
|
6
6
|
showEsc?: boolean
|
|
7
7
|
snapContext?: SnapContext | null
|
|
8
8
|
// Whether to advertise Alt = force-place. Only meaningful for kinds that
|
|
9
|
-
// collision-validate their drop
|
|
9
|
+
// collision-validate their drop.
|
|
10
10
|
showForce?: boolean
|
|
11
11
|
// Set for a fresh point-kind placement (e.g. a positioned preset) so the
|
|
12
12
|
// once/repeat continuation chip shows; null for an existing-node move.
|