@pascal-app/editor 1.0.0-beta.4 → 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 +15 -8
- package/scripts/generate-print-golden-house.ts +89 -0
- package/src/components/editor/alignment-3d-guide-layer.tsx +12 -3
- 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 +17 -5
- package/src/components/editor/elevation-3d-guide-layer.tsx +3 -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 +182 -57
- package/src/components/editor/floating-action-menu.tsx +55 -55
- package/src/components/editor/floorplan-background-selection.test.ts +31 -4
- package/src/components/editor/floorplan-background-selection.ts +27 -7
- package/src/components/editor/floorplan-panel.tsx +81 -57
- package/src/components/editor/grid.tsx +12 -7
- package/src/components/editor/group-actions.ts +34 -50
- package/src/components/editor/group-floating-action-menu.tsx +30 -15
- 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 +252 -46
- package/src/components/editor/measurement-pill.tsx +12 -12
- package/src/components/editor/node-action-menu.tsx +42 -1
- package/src/components/editor/node-arrow-handles.tsx +47 -58
- package/src/components/editor/opening-guides-3d-layer.tsx +13 -4
- package/src/components/editor/selection-manager.tsx +132 -47
- package/src/components/editor/site-edge-labels.tsx +2 -1
- package/src/components/editor/snapshot-capture-overlay.tsx +309 -41
- package/src/components/editor/snapshot-capture.test.ts +422 -0
- package/src/components/editor/snapshot-capture.ts +145 -0
- package/src/components/editor/thumbnail-generator.tsx +295 -232
- package/src/components/editor/use-floorplan-background-placement.ts +53 -5
- package/src/components/editor/wall-measurement-label.tsx +3 -2
- package/src/components/editor/wall-move-side-handles.tsx +299 -7
- package/src/components/editor-2d/floorplan-group-action-menu.tsx +23 -16
- 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 +204 -114
- 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 +494 -0
- package/src/components/tools/item/placement-strategies.ts +192 -24
- 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 +447 -180
- 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 +63 -8
- 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-brush-cursor.tsx +2 -1
- 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 +527 -4
- package/src/components/tools/wall/wall-drafting.ts +55 -462
- 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 +12 -13
- package/src/components/ui/command-palette/index.tsx +7 -2
- package/src/components/ui/controls/material-paint-panel.tsx +6 -4
- package/src/components/ui/controls/material-picker.tsx +4 -4
- package/src/components/ui/controls/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 +23 -6
- 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 +78 -26
- 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/panel-wrapper.tsx +228 -12
- 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/items-panel/function-tree-panel.tsx +7 -2
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +390 -36
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +31 -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 +128 -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 +22 -7
- package/src/components/ui/sidebar/tab-bar.tsx +9 -1
- package/src/components/ui/sidebar/use-plugin-panels.tsx +17 -2
- package/src/components/viewer/floorplan-compass-button.tsx +50 -0
- package/src/components/viewer/floorplan-preview-geometry.test.ts +107 -0
- package/src/components/viewer/floorplan-preview-geometry.ts +281 -0
- package/src/components/viewer/floorplan-preview-navigation.test.ts +43 -0
- package/src/components/viewer/floorplan-preview-navigation.ts +62 -0
- package/src/components/viewer/floorplan-preview.tsx +1069 -0
- package/src/components/viewer/use-viewer-camera-navigation-sync.ts +140 -0
- package/src/components/viewer/viewer-scene-header.tsx +2 -1
- package/src/components/viewer/viewer-stage-modes.test.ts +38 -0
- package/src/components/viewer/viewer-stage-modes.ts +49 -0
- package/src/components/viewer/viewer-stage-switcher.tsx +95 -0
- package/src/components/viewer/viewer-stage.test.tsx +47 -0
- package/src/components/viewer/viewer-stage.tsx +221 -0
- package/src/components/viewer-overlay.tsx +13 -30
- 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 +122 -1
- package/src/hooks/use-auto-save.ts +119 -13
- 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 +164 -63
- package/src/hooks/use-registered-tool-enabled.ts +20 -0
- package/src/hooks/use-save-shortcut.ts +33 -0
- package/src/index.tsx +109 -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/contextual-help.test.ts +8 -0
- package/src/lib/contextual-help.ts +8 -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.test.ts +47 -0
- package/src/lib/floorplan/floorplan-readonly.ts +85 -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 +523 -77
- package/src/lib/history.ts +133 -14
- package/src/lib/host-tree-children.test.ts +27 -0
- package/src/lib/host-tree-children.ts +79 -0
- package/src/lib/inspector-card-mode.test.ts +92 -0
- package/src/lib/inspector-card-mode.ts +60 -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/measurement-parser.ts +3 -0
- package/src/lib/measurements.test.ts +4 -0
- package/src/lib/model-export.ts +29 -0
- package/src/lib/paint-preview-owner.ts +71 -0
- package/src/lib/paint-scope.test.ts +58 -2
- package/src/lib/paint-scope.ts +8 -2
- 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 +125 -6
- package/src/lib/selection-routing.ts +34 -1
- package/src/lib/session-groups.test.ts +88 -0
- package/src/lib/session-groups.ts +201 -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/structured-clone-fallback.test.ts +45 -0
- package/src/lib/structured-clone-fallback.ts +24 -0
- 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/store/use-session-groups.ts +103 -0
- package/src/components/tools/roof/roof-tool.tsx +0 -656
- package/src/components/ui/helpers/roof-helper.tsx +0 -14
|
@@ -37,7 +37,7 @@ import {
|
|
|
37
37
|
Video,
|
|
38
38
|
} from 'lucide-react'
|
|
39
39
|
import { useEffect } from 'react'
|
|
40
|
-
import { runRedo, runUndo } from '../../../lib/history'
|
|
40
|
+
import { getHistoryCommandState, runRedo, runUndo } from '../../../lib/history'
|
|
41
41
|
import { deleteLevelWithFallbackSelection } from '../../../lib/level-selection'
|
|
42
42
|
import { useCommandRegistry } from '../../../store/use-command-registry'
|
|
43
43
|
import type { StructureTool } from '../../../store/use-editor'
|
|
@@ -49,10 +49,9 @@ export function EditorCommands() {
|
|
|
49
49
|
const { navigateTo, setInputValue, setOpen } = useCommandPalette()
|
|
50
50
|
|
|
51
51
|
const setPhase = useEditor((s) => s.setPhase)
|
|
52
|
-
const
|
|
53
|
-
const
|
|
52
|
+
const armToolMode = useEditor((s) => s.armToolMode)
|
|
53
|
+
const armMaterialPaint = useEditor((s) => s.armMaterialPaint)
|
|
54
54
|
const setStructureLayer = useEditor((s) => s.setStructureLayer)
|
|
55
|
-
const primeMaterialPaintFromSelection = useEditor((s) => s.primeMaterialPaintFromSelection)
|
|
56
55
|
const isPreviewMode = useEditor((s) => s.isPreviewMode)
|
|
57
56
|
const setPreviewMode = useEditor((s) => s.setPreviewMode)
|
|
58
57
|
|
|
@@ -68,9 +67,9 @@ export function EditorCommands() {
|
|
|
68
67
|
const activateTool = (tool: StructureTool) => {
|
|
69
68
|
run(() => {
|
|
70
69
|
setPhase('structure')
|
|
71
|
-
setMode('build')
|
|
72
70
|
if (tool === 'zone') setStructureLayer('zones')
|
|
73
|
-
|
|
71
|
+
else setStructureLayer('elements')
|
|
72
|
+
armToolMode({ mode: 'build', tool })
|
|
74
73
|
})
|
|
75
74
|
}
|
|
76
75
|
|
|
@@ -163,10 +162,9 @@ export function EditorCommands() {
|
|
|
163
162
|
shortcut: ['P'],
|
|
164
163
|
execute: () =>
|
|
165
164
|
run(() => {
|
|
166
|
-
primeMaterialPaintFromSelection()
|
|
167
165
|
setPhase('structure')
|
|
168
166
|
setStructureLayer('elements')
|
|
169
|
-
|
|
167
|
+
armMaterialPaint()
|
|
170
168
|
}),
|
|
171
169
|
},
|
|
172
170
|
{
|
|
@@ -176,9 +174,8 @@ export function EditorCommands() {
|
|
|
176
174
|
icon: <Mountain className="h-4 w-4" />,
|
|
177
175
|
keywords: ['terrain', 'ground', 'elevation', 'sculpt', 'hill', 'slope', 'grade', 'dig'],
|
|
178
176
|
shortcut: ['G'],
|
|
179
|
-
//
|
|
180
|
-
|
|
181
|
-
execute: () => run(() => setMode('terrain-sculpt')),
|
|
177
|
+
// The ToolMode transition moves to the site phase itself.
|
|
178
|
+
execute: () => run(() => armToolMode({ mode: 'terrain-sculpt' })),
|
|
182
179
|
},
|
|
183
180
|
|
|
184
181
|
// ── Levels ───────────────────────────────────────────────────────────
|
|
@@ -351,6 +348,7 @@ export function EditorCommands() {
|
|
|
351
348
|
group: 'History',
|
|
352
349
|
icon: <Undo2 className="h-4 w-4" />,
|
|
353
350
|
keywords: ['undo', 'revert', 'back'],
|
|
351
|
+
when: () => getHistoryCommandState().canUndo,
|
|
354
352
|
execute: () => run(() => runUndo()),
|
|
355
353
|
},
|
|
356
354
|
{
|
|
@@ -359,6 +357,7 @@ export function EditorCommands() {
|
|
|
359
357
|
group: 'History',
|
|
360
358
|
icon: <Redo2 className="h-4 w-4" />,
|
|
361
359
|
keywords: ['redo', 'forward', 'repeat'],
|
|
360
|
+
when: () => getHistoryCommandState().canRedo,
|
|
362
361
|
execute: () => run(() => runRedo()),
|
|
363
362
|
},
|
|
364
363
|
|
|
@@ -426,8 +425,8 @@ export function EditorCommands() {
|
|
|
426
425
|
setInputValue,
|
|
427
426
|
setOpen,
|
|
428
427
|
setPhase,
|
|
429
|
-
|
|
430
|
-
|
|
428
|
+
armToolMode,
|
|
429
|
+
armMaterialPaint,
|
|
431
430
|
setStructureLayer,
|
|
432
431
|
isPreviewMode,
|
|
433
432
|
setPreviewMode,
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import type { AnyNodeId, LevelNode } from '@pascal-app/core'
|
|
4
4
|
import { useScene } from '@pascal-app/core'
|
|
5
|
-
import { useViewer } from '@pascal-app/viewer'
|
|
5
|
+
import { markPerfAction, useViewer } from '@pascal-app/viewer'
|
|
6
6
|
import { Command, useCommandState } from 'cmdk'
|
|
7
7
|
import { ChevronRight, Search } from 'lucide-react'
|
|
8
8
|
import type { ReactNode } from 'react'
|
|
@@ -408,7 +408,12 @@ export function CommandPalette({ emptyAction }: { emptyAction?: CommandPaletteEm
|
|
|
408
408
|
key={level.id}
|
|
409
409
|
label={getLevelDisplayName(level)}
|
|
410
410
|
onSelect={() =>
|
|
411
|
-
run(() =>
|
|
411
|
+
run(() => {
|
|
412
|
+
if (level.id !== useViewer.getState().selection.levelId) {
|
|
413
|
+
markPerfAction('level-switch', level.id)
|
|
414
|
+
}
|
|
415
|
+
useViewer.getState().setSelection({ levelId: level.id })
|
|
416
|
+
})
|
|
412
417
|
}
|
|
413
418
|
/>
|
|
414
419
|
))}
|
|
@@ -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.
|
|
@@ -235,7 +238,7 @@ function LevelRow({
|
|
|
235
238
|
>
|
|
236
239
|
<SliderControl
|
|
237
240
|
label="Level height"
|
|
238
|
-
max={
|
|
241
|
+
max={20}
|
|
239
242
|
min={1}
|
|
240
243
|
onChange={(v) => updateNode(level.id, { height: v })}
|
|
241
244
|
precision={3}
|
|
@@ -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))
|