@pascal-app/editor 1.0.0-beta.5 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/bunfig.toml +4 -0
- package/package.json +15 -8
- package/scripts/generate-print-golden-house.ts +89 -0
- package/src/components/editor/bake-exporter.tsx +5 -0
- package/src/components/editor/bake-thumbnail.tsx +4 -1
- package/src/components/editor/camera-dragging-lifecycle.test.ts +41 -0
- package/src/components/editor/camera-dragging-lifecycle.ts +10 -2
- package/src/components/editor/capture-camera-rig.tsx +59 -0
- package/src/components/editor/custom-camera-controls.tsx +20 -11
- package/src/components/editor/editor-layout-mobile.tsx +6 -6
- package/src/components/editor/editor-layout-v2.tsx +9 -2
- package/src/components/editor/export-manager.tsx +175 -33
- package/src/components/editor/first-person/build-collider-world.test.ts +30 -0
- package/src/components/editor/first-person-controls.tsx +180 -12
- package/src/components/editor/floating-action-menu.tsx +55 -51
- package/src/components/editor/floorplan-panel.tsx +99 -15
- package/src/components/editor/grid.tsx +12 -7
- package/src/components/editor/group-actions.ts +34 -50
- package/src/components/editor/group-move-3d.ts +51 -21
- package/src/components/editor/group-transform-shared.test.ts +78 -0
- package/src/components/editor/group-transform-shared.ts +74 -7
- package/src/components/editor/handles/handle-arrow-raycast.test.ts +86 -0
- package/src/components/editor/handles/handle-arrow.tsx +109 -48
- package/src/components/editor/handles/handle-placement.test.ts +111 -0
- package/src/components/editor/handles/handle-placement.ts +69 -0
- package/src/components/editor/handles/linear-resize-drag.ts +78 -0
- package/src/components/editor/handles/resize-snap.test.ts +46 -0
- package/src/components/editor/handles/resize-snap.ts +10 -1
- package/src/components/editor/handles/use-handle-drag.ts +23 -3
- package/src/components/editor/index.tsx +151 -24
- package/src/components/editor/node-action-menu.tsx +14 -1
- package/src/components/editor/node-arrow-handles.tsx +67 -119
- package/src/components/editor/selection-manager.tsx +153 -42
- package/src/components/editor/snapshot-capture-overlay.tsx +298 -39
- package/src/components/editor/snapshot-capture.test.ts +422 -0
- package/src/components/editor/snapshot-capture.ts +145 -0
- package/src/components/editor/thumbnail-generator.tsx +295 -239
- package/src/components/editor/use-floorplan-background-placement.ts +53 -5
- package/src/components/editor/wall-move-side-handles.tsx +299 -7
- package/src/components/editor-2d/floorplan-group-move.tsx +67 -33
- package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +3 -1
- package/src/components/editor-2d/floorplan-registry-action-menu.tsx +12 -1
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +129 -38
- package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +14 -1
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.test.tsx +16 -0
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +2 -0
- package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +52 -3
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +196 -1
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +226 -35
- package/src/components/procedural-items/inspector.tsx +223 -0
- package/src/components/systems/ceiling/ceiling-bracket-batch.test.ts +402 -0
- package/src/components/systems/ceiling/ceiling-bracket-batch.ts +371 -0
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.test.ts +347 -0
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +281 -305
- package/src/components/systems/ceiling/ceiling-system.tsx +3 -1
- package/src/components/systems/roof/roof-edit-system.test.tsx +120 -0
- package/src/components/systems/roof/roof-edit-system.tsx +10 -19
- package/src/components/systems/selection-affordance-manager.tsx +59 -9
- package/src/components/systems/selection-affordance-services.ts +19 -0
- package/src/components/systems/stair/stair-edit-system.tsx +5 -2
- package/src/components/tools/fence/fence-drafting.test.ts +100 -0
- package/src/components/tools/fence/fence-drafting.ts +6 -25
- package/src/components/tools/item/draft-creation.test.ts +12 -0
- package/src/components/tools/item/draft-creation.ts +10 -0
- package/src/components/tools/item/face-host-commit.test.ts +65 -0
- package/src/components/tools/item/face-host-commit.ts +56 -0
- package/src/components/tools/item/face-host-preview.test.ts +77 -0
- package/src/components/tools/item/face-host-preview.ts +82 -0
- package/src/components/tools/item/item-surface-placement.test.ts +175 -0
- package/src/components/tools/item/item-surface-strategy-resolver.test.ts +210 -0
- package/src/components/tools/item/move-tool.tsx +4 -3
- package/src/components/tools/item/placement-math.test.ts +11 -1
- package/src/components/tools/item/placement-math.ts +0 -20
- package/src/components/tools/item/placement-strategies.test.ts +331 -4
- package/src/components/tools/item/placement-strategies.ts +304 -171
- package/src/components/tools/item/placement-surface.test.ts +61 -0
- package/src/components/tools/item/placement-surface.ts +24 -0
- package/src/components/tools/item/placement-types.ts +10 -0
- package/src/components/tools/item/shelf-surface-strategy-resolver.test.ts +257 -0
- package/src/components/tools/item/test-face-host.ts +121 -0
- package/src/components/tools/item/use-draft-node.test.tsx +336 -0
- package/src/components/tools/item/use-draft-node.ts +116 -44
- package/src/components/tools/item/use-placement-coordinator.tsx +672 -256
- package/src/components/tools/registered-tool-install-lifecycle.test.tsx +256 -0
- package/src/components/tools/registry/item-surface-child-frame.test.ts +90 -0
- package/src/components/tools/registry/item-surface-move.test.ts +501 -0
- package/src/components/tools/registry/item-surface-move.ts +441 -0
- package/src/components/tools/registry/item-surface-resolver.test.ts +257 -0
- package/src/components/tools/registry/move-registry-node-tool.test.ts +10 -0
- package/src/components/tools/registry/move-registry-node-tool.tsx +505 -164
- package/src/components/tools/registry-tool-context.tsx +30 -0
- package/src/components/tools/select/box-select-tool.tsx +3 -1
- package/src/components/tools/select/marquee-footprint.test.ts +53 -0
- package/src/components/tools/select/marquee-footprint.ts +29 -0
- package/src/components/tools/shared/placement-box.tsx +57 -7
- package/src/components/tools/shared/placement-dimension-guides.tsx +121 -0
- package/src/components/tools/shared/pointer-support-cap.test.ts +319 -17
- package/src/components/tools/shared/pointer-support-cap.ts +56 -17
- package/src/components/tools/shared/shelf-stickiness.ts +54 -0
- package/src/components/tools/shared/surface-hit.test.ts +56 -0
- package/src/components/tools/shared/surface-hit.ts +23 -0
- package/src/components/tools/shared/surface-rejection.test.tsx +106 -0
- package/src/components/tools/shared/surface-rejection.tsx +29 -0
- package/src/components/tools/site/terrain-sculpt-grid.tsx +9 -5
- package/src/components/tools/site/terrain-sculpt-tool.tsx +1 -1
- package/src/components/tools/stair/stair-tool.tsx +127 -74
- package/src/components/tools/tool-manager.tsx +25 -8
- package/src/components/tools/wall/wall-drafting.test.ts +320 -2
- package/src/components/tools/wall/wall-drafting.ts +23 -114
- package/src/components/tools/zone/zone-tool.tsx +13 -4
- package/src/components/ui/action-menu/control-modes.tsx +9 -6
- package/src/components/ui/action-menu/index.tsx +11 -1
- package/src/components/ui/command-palette/editor-commands.tsx +63 -13
- package/src/components/ui/command-palette/index.tsx +7 -2
- package/src/components/ui/controls/material-paint-panel.tsx +6 -4
- package/src/components/ui/controls/material-picker.tsx +4 -4
- package/src/components/ui/controls/metric-control.tsx +29 -13
- package/src/components/ui/controls/scene-material-list.tsx +6 -5
- package/src/components/ui/controls/segmented-control.tsx +8 -1
- package/src/components/ui/controls/slider-control.tsx +44 -21
- package/src/components/ui/controls/toggle-control.tsx +18 -5
- package/src/components/ui/controls/tool-options-panel.tsx +126 -0
- package/src/components/ui/floating-level-selector.tsx +103 -10
- package/src/components/ui/helpers/contextual-helper-panel.tsx +9 -0
- package/src/components/ui/helpers/helper-manager.tsx +47 -11
- package/src/components/ui/helpers/item-helper.tsx +1 -1
- package/src/components/ui/helpers/registered-tool-helper.tsx +21 -0
- package/src/components/ui/panels/homogeneous-selection.test.ts +80 -0
- package/src/components/ui/panels/homogeneous-selection.ts +47 -0
- package/src/components/ui/panels/mobile-selection-bar.tsx +11 -5
- package/src/components/ui/panels/multi-field-value.test.ts +209 -0
- package/src/components/ui/panels/multi-field-value.ts +163 -0
- package/src/components/ui/panels/multi-height-mode.tsx +185 -0
- package/src/components/ui/panels/multi-parametric-inspector.tsx +266 -0
- package/src/components/ui/panels/multi-selection-panel.tsx +48 -37
- package/src/components/ui/panels/node-display.ts +4 -0
- package/src/components/ui/panels/panel-manager.tsx +77 -3
- package/src/components/ui/panels/parametric-field-control.tsx +173 -0
- package/src/components/ui/panels/parametric-field-utils.ts +18 -0
- package/src/components/ui/panels/parametric-inspector.tsx +35 -164
- package/src/components/ui/panels/reference-panel.tsx +34 -4
- package/src/components/ui/primitives/checkbox.tsx +34 -0
- package/src/components/ui/primitives/shortcut-token.tsx +23 -1
- package/src/components/ui/scene-loader.tsx +35 -0
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +502 -31
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +20 -23
- package/src/components/ui/sidebar/panels/settings-panel/print-export-button.test.ts +167 -0
- package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +128 -0
- package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +8 -1
- package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +19 -5
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +222 -50
- package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +2 -7
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +5 -2
- package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +28 -11
- package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +3 -8
- package/src/components/ui/sidebar/panels/site-panel/tree-node.test.ts +8 -0
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +20 -7
- package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +224 -0
- package/src/components/ui/sidebar/panels/site-panel/zone-membership-checkbox.tsx +22 -0
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +24 -7
- package/src/components/ui/sidebar/tab-bar.tsx +2 -0
- package/src/components/ui/sidebar/use-plugin-panels.tsx +17 -2
- package/src/components/ui/snap-target-badge.tsx +2 -0
- package/src/components/viewer/viewer-scene-header.tsx +5 -1
- package/src/components/viewer/viewer-stage.tsx +4 -1
- package/src/components/viewer/viewer-units-panel.tsx +116 -0
- package/src/components/viewer-overlay.tsx +1 -22
- package/src/components/viewer-zone-system.tsx +6 -2
- package/src/components/walkthrough-hud.tsx +19 -10
- package/src/hooks/use-auto-save.test.ts +73 -1
- package/src/hooks/use-auto-save.ts +76 -5
- package/src/hooks/use-ceiling-events.test.ts +144 -0
- package/src/hooks/use-ceiling-events.ts +5 -1
- package/src/hooks/use-drag-action.ts +17 -3
- package/src/hooks/use-grid-events.test.ts +240 -0
- package/src/hooks/use-grid-events.ts +190 -11
- package/src/hooks/use-keyboard.test.ts +244 -0
- package/src/hooks/use-keyboard.ts +132 -63
- package/src/hooks/use-registered-tool-enabled.ts +20 -0
- package/src/hooks/use-save-shortcut.ts +33 -0
- package/src/index.tsx +87 -4
- package/src/lib/active-placement-surface.test.ts +16 -0
- package/src/lib/active-placement-surface.ts +14 -0
- package/src/lib/ceiling-surface-raycast.test.ts +64 -0
- package/src/lib/ceiling-surface-raycast.ts +12 -0
- package/src/lib/contextual-help-extension.ts +17 -0
- package/src/lib/continuation.test.ts +11 -0
- package/src/lib/continuation.ts +8 -1
- package/src/lib/direct-manipulation.test.ts +132 -0
- package/src/lib/direct-manipulation.ts +38 -1
- package/src/lib/editor-api.ts +6 -3
- package/src/lib/elevation-guides.test.ts +28 -1
- package/src/lib/elevation-guides.ts +22 -7
- package/src/lib/export-texture-utils.test.ts +167 -0
- package/src/lib/export-texture-utils.ts +121 -0
- package/src/lib/export-user-data.test.ts +48 -0
- package/src/lib/export-user-data.ts +69 -0
- package/src/lib/floating-menu-scale.test.ts +29 -0
- package/src/lib/floating-menu-scale.ts +14 -0
- package/src/lib/floorplan/apply-alignment.test.ts +47 -2
- package/src/lib/floorplan/apply-alignment.ts +10 -1
- package/src/lib/floorplan/floorplan-export.test.ts +536 -2
- package/src/lib/floorplan/floorplan-export.tsx +120 -19
- package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +26 -0
- package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +32 -24
- package/src/lib/floorplan/floorplan-readonly.ts +5 -0
- package/src/lib/floorplan-grid-event-point.test.ts +29 -0
- package/src/lib/floorplan-grid-event-point.ts +13 -0
- package/src/lib/fresh-planar-placement.ts +13 -5
- package/src/lib/glb-export-serialize.test.ts +280 -0
- package/src/lib/glb-export.test.ts +1375 -21
- package/src/lib/glb-export.ts +781 -106
- package/src/lib/grid-event-presentation.ts +16 -0
- package/src/lib/history.test.ts +528 -147
- package/src/lib/history.ts +81 -7
- package/src/lib/host-tree-children.test.ts +27 -0
- package/src/lib/host-tree-children.ts +79 -0
- package/src/lib/interaction/hot-set.test.ts +10 -1
- package/src/lib/interaction/hot-set.ts +3 -1
- package/src/lib/interaction/overlay-policy.test.ts +33 -1
- package/src/lib/interaction/overlay-policy.ts +16 -0
- package/src/lib/interaction/registered-drafting.ts +36 -0
- package/src/lib/interaction/scope.ts +31 -0
- package/src/lib/level-duplication.test.ts +73 -0
- package/src/lib/level-duplication.ts +32 -2
- package/src/lib/level-print-export.test.ts +484 -0
- package/src/lib/level-print-export.ts +526 -0
- package/src/lib/linear-display.test.ts +59 -0
- package/src/lib/linear-display.ts +39 -0
- package/src/lib/local-guide-image.test.ts +24 -0
- package/src/lib/local-guide-image.ts +34 -1
- package/src/lib/local-project-presentation-persistence.test.ts +302 -0
- package/src/lib/local-project-presentation-persistence.ts +335 -0
- package/src/lib/material-paint.ts +2 -0
- package/src/lib/model-export.ts +29 -0
- package/src/lib/node-action-movement.ts +21 -0
- package/src/lib/paint-preview-owner.ts +71 -0
- package/src/lib/planar-cursor-placement.test.ts +178 -1
- package/src/lib/planar-cursor-placement.ts +61 -5
- package/src/lib/plugin-panels.test.ts +92 -1
- package/src/lib/plugin-panels.ts +48 -0
- package/src/lib/portable-export.test.ts +455 -0
- package/src/lib/portable-export.ts +1042 -0
- package/src/lib/print-3mf.test.ts +86 -0
- package/src/lib/print-3mf.ts +161 -0
- package/src/lib/print-content-scope.test.ts +107 -0
- package/src/lib/print-content-scope.ts +90 -0
- package/src/lib/print-export.test.ts +294 -0
- package/src/lib/print-export.ts +735 -0
- package/src/lib/print-feature-thickness.test.ts +147 -0
- package/src/lib/print-feature-thickness.ts +180 -0
- package/src/lib/print-golden-house.test-fixture.ts +352 -0
- package/src/lib/print-golden-house.test.ts +249 -0
- package/src/lib/print-roof-solids.test.ts +133 -0
- package/src/lib/print-roof-solids.ts +630 -0
- package/src/lib/print-shell-compiler-baseline.test.ts +419 -0
- package/src/lib/print-shell-compiler-baseline.ts +225 -0
- package/src/lib/print-shell-compiler-manifold-core.ts +379 -0
- package/src/lib/print-shell-compiler-manifold-worker.ts +200 -0
- package/src/lib/print-shell-compiler-manifold.worker.ts +20 -0
- package/src/lib/print-shell-compiler-mesh-data.ts +32 -0
- package/src/lib/print-shell-compiler-protocol.ts +38 -0
- package/src/lib/print-shell-compiler.ts +273 -0
- package/src/lib/print-wall-solids.test.ts +194 -0
- package/src/lib/print-wall-solids.ts +328 -0
- package/src/lib/rigid-plan-svg-transform.test.ts +35 -0
- package/src/lib/rigid-plan-svg-transform.ts +24 -0
- package/src/lib/scene-clipboard.ts +1 -1
- package/src/lib/scene.ts +22 -8
- package/src/lib/selection-routing.test.ts +105 -1
- package/src/lib/selection-routing.ts +19 -0
- package/src/lib/send-to-app.test.ts +108 -0
- package/src/lib/send-to-app.ts +121 -0
- package/src/lib/sfx/movement-tick.ts +37 -0
- package/src/lib/sfx-bus.ts +4 -0
- package/src/lib/sfx-player.ts +8 -0
- package/src/lib/snapping-mode.test.ts +32 -4
- package/src/lib/snapping-mode.ts +11 -7
- package/src/lib/surface-attachment.ts +77 -0
- package/src/lib/units.ts +254 -0
- package/src/lib/usdz-export.ts +65 -0
- package/src/lib/use-linear-display.ts +8 -32
- package/src/lib/walkthrough-pointer-lock.ts +47 -0
- package/src/store/live-draft-preview-stores.test.ts +3 -0
- package/src/store/terrain-sculpt-mode.test.ts +3 -6
- package/src/store/tool-mode.test.ts +87 -0
- package/src/store/use-camera-hint-focus.ts +48 -0
- package/src/store/use-editor.tsx +299 -188
- package/src/store/use-floorplan-draft-preview.ts +9 -0
- package/src/store/use-handle-group.ts +4 -0
- package/src/store/use-interaction-scope.test.ts +23 -1
- package/src/store/use-interaction-scope.ts +63 -2
- package/src/store/use-placement-preview.ts +56 -3
- package/src/store/use-send-to-app.ts +21 -0
- package/src/components/tools/roof/roof-tool.tsx +0 -656
- package/src/components/ui/helpers/roof-helper.tsx +0 -14
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
3
|
import type { AnyNodeId } from '@pascal-app/core'
|
|
4
|
-
import { DEFAULT_LEVEL_HEIGHT, LevelNode, useScene } from '@pascal-app/core'
|
|
4
|
+
import { DEFAULT_LEVEL_HEIGHT, LevelNode, type UnitNode, useScene } from '@pascal-app/core'
|
|
5
5
|
import { useViewer } from '@pascal-app/viewer'
|
|
6
6
|
import {
|
|
7
7
|
AppWindow,
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
EyeOff,
|
|
16
16
|
FileJson,
|
|
17
17
|
Grid3X3,
|
|
18
|
+
Group,
|
|
18
19
|
Hexagon,
|
|
19
20
|
Layers,
|
|
20
21
|
Map,
|
|
@@ -39,6 +40,7 @@ import {
|
|
|
39
40
|
import { useEffect } from 'react'
|
|
40
41
|
import { getHistoryCommandState, runRedo, runUndo } from '../../../lib/history'
|
|
41
42
|
import { deleteLevelWithFallbackSelection } from '../../../lib/level-selection'
|
|
43
|
+
import { createUnitInBuilding, enterUnitFocus, leaveUnitFocus } from '../../../lib/units'
|
|
42
44
|
import { useCommandRegistry } from '../../../store/use-command-registry'
|
|
43
45
|
import type { StructureTool } from '../../../store/use-editor'
|
|
44
46
|
import useEditor from '../../../store/use-editor'
|
|
@@ -49,28 +51,39 @@ export function EditorCommands() {
|
|
|
49
51
|
const { navigateTo, setInputValue, setOpen } = useCommandPalette()
|
|
50
52
|
|
|
51
53
|
const setPhase = useEditor((s) => s.setPhase)
|
|
52
|
-
const
|
|
53
|
-
const
|
|
54
|
+
const armToolMode = useEditor((s) => s.armToolMode)
|
|
55
|
+
const armMaterialPaint = useEditor((s) => s.armMaterialPaint)
|
|
54
56
|
const setStructureLayer = useEditor((s) => s.setStructureLayer)
|
|
55
|
-
const primeMaterialPaintFromSelection = useEditor((s) => s.primeMaterialPaintFromSelection)
|
|
56
57
|
const isPreviewMode = useEditor((s) => s.isPreviewMode)
|
|
57
58
|
const setPreviewMode = useEditor((s) => s.setPreviewMode)
|
|
58
59
|
|
|
59
60
|
const exportScene = useViewer((s) => s.exportScene)
|
|
61
|
+
// Focusable units are listed one command each; the key changes only when a
|
|
62
|
+
// unit is added, removed or renamed.
|
|
63
|
+
const unitListKey = useScene((s) =>
|
|
64
|
+
Object.values(s.nodes)
|
|
65
|
+
.filter((n): n is UnitNode => n.type === 'unit')
|
|
66
|
+
.map((n) => `${n.id}:${n.name ?? ''}`)
|
|
67
|
+
.join('|'),
|
|
68
|
+
)
|
|
60
69
|
|
|
61
70
|
// Re-register when exportScene availability changes (it's a conditional action)
|
|
62
71
|
useEffect(() => {
|
|
72
|
+
void unitListKey
|
|
63
73
|
const run = (fn: () => void) => {
|
|
64
74
|
fn()
|
|
65
75
|
setOpen(false)
|
|
66
76
|
}
|
|
77
|
+
const focusableUnits = Object.values(useScene.getState().nodes).filter(
|
|
78
|
+
(n): n is UnitNode => n.type === 'unit',
|
|
79
|
+
)
|
|
67
80
|
|
|
68
81
|
const activateTool = (tool: StructureTool) => {
|
|
69
82
|
run(() => {
|
|
70
83
|
setPhase('structure')
|
|
71
|
-
setMode('build')
|
|
72
84
|
if (tool === 'zone') setStructureLayer('zones')
|
|
73
|
-
|
|
85
|
+
else setStructureLayer('elements')
|
|
86
|
+
armToolMode({ mode: 'build', tool })
|
|
74
87
|
})
|
|
75
88
|
}
|
|
76
89
|
|
|
@@ -163,10 +176,9 @@ export function EditorCommands() {
|
|
|
163
176
|
shortcut: ['P'],
|
|
164
177
|
execute: () =>
|
|
165
178
|
run(() => {
|
|
166
|
-
primeMaterialPaintFromSelection()
|
|
167
179
|
setPhase('structure')
|
|
168
180
|
setStructureLayer('elements')
|
|
169
|
-
|
|
181
|
+
armMaterialPaint()
|
|
170
182
|
}),
|
|
171
183
|
},
|
|
172
184
|
{
|
|
@@ -176,9 +188,8 @@ export function EditorCommands() {
|
|
|
176
188
|
icon: <Mountain className="h-4 w-4" />,
|
|
177
189
|
keywords: ['terrain', 'ground', 'elevation', 'sculpt', 'hill', 'slope', 'grade', 'dig'],
|
|
178
190
|
shortcut: ['G'],
|
|
179
|
-
//
|
|
180
|
-
|
|
181
|
-
execute: () => run(() => setMode('terrain-sculpt')),
|
|
191
|
+
// The ToolMode transition moves to the site phase itself.
|
|
192
|
+
execute: () => run(() => armToolMode({ mode: 'terrain-sculpt' })),
|
|
182
193
|
},
|
|
183
194
|
|
|
184
195
|
// ── Levels ───────────────────────────────────────────────────────────
|
|
@@ -252,6 +263,44 @@ export function EditorCommands() {
|
|
|
252
263
|
}),
|
|
253
264
|
},
|
|
254
265
|
|
|
266
|
+
// ── Units ────────────────────────────────────────────────────────────
|
|
267
|
+
{
|
|
268
|
+
id: 'editor.unit.new',
|
|
269
|
+
label: 'New unit',
|
|
270
|
+
group: 'Units',
|
|
271
|
+
icon: <Group className="h-4 w-4" />,
|
|
272
|
+
keywords: ['unit', 'apartment', 'hotel', 'room', 'add', 'create', 'new'],
|
|
273
|
+
when: () => Object.values(useScene.getState().nodes).some((n) => n.type === 'building'),
|
|
274
|
+
execute: () =>
|
|
275
|
+
run(() => {
|
|
276
|
+
const { nodes } = useScene.getState()
|
|
277
|
+
const selectedBuildingId = useViewer.getState().selection.buildingId
|
|
278
|
+
const building =
|
|
279
|
+
(selectedBuildingId ? nodes[selectedBuildingId] : undefined) ??
|
|
280
|
+
Object.values(nodes).find((n) => n.type === 'building')
|
|
281
|
+
if (building?.type !== 'building') return
|
|
282
|
+
createUnitInBuilding(building.id)
|
|
283
|
+
}),
|
|
284
|
+
},
|
|
285
|
+
...focusableUnits.map((unit) => ({
|
|
286
|
+
id: `editor.unit.focus.${unit.id}`,
|
|
287
|
+
label: `Focus unit: ${unit.name || 'Unit'}`,
|
|
288
|
+
group: 'Units',
|
|
289
|
+
icon: <Group className="h-4 w-4" />,
|
|
290
|
+
keywords: ['unit', 'focus', 'apartment', unit.name || 'Unit'],
|
|
291
|
+
when: () => useViewer.getState().focusedUnitId !== unit.id,
|
|
292
|
+
execute: () => run(() => enterUnitFocus(unit.id)),
|
|
293
|
+
})),
|
|
294
|
+
{
|
|
295
|
+
id: 'editor.unit.exit-focus',
|
|
296
|
+
label: 'Exit unit focus',
|
|
297
|
+
group: 'Units',
|
|
298
|
+
icon: <Group className="h-4 w-4" />,
|
|
299
|
+
keywords: ['unit', 'focus', 'exit', 'leave', 'apartment'],
|
|
300
|
+
when: () => !!useViewer.getState().focusedUnitId,
|
|
301
|
+
execute: () => run(() => leaveUnitFocus()),
|
|
302
|
+
},
|
|
303
|
+
|
|
255
304
|
// ── Viewer Controls ──────────────────────────────────────────────────
|
|
256
305
|
{
|
|
257
306
|
id: 'editor.viewer.wall-mode',
|
|
@@ -428,12 +477,13 @@ export function EditorCommands() {
|
|
|
428
477
|
setInputValue,
|
|
429
478
|
setOpen,
|
|
430
479
|
setPhase,
|
|
431
|
-
|
|
432
|
-
|
|
480
|
+
armToolMode,
|
|
481
|
+
armMaterialPaint,
|
|
433
482
|
setStructureLayer,
|
|
434
483
|
isPreviewMode,
|
|
435
484
|
setPreviewMode,
|
|
436
485
|
exportScene,
|
|
486
|
+
unitListKey,
|
|
437
487
|
])
|
|
438
488
|
|
|
439
489
|
return null
|
|
@@ -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,
|
|
@@ -2,11 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { useScene } from '@pascal-app/core'
|
|
4
4
|
import { useCallback, useEffect, useRef, useState } from 'react'
|
|
5
|
-
import {
|
|
6
|
-
lingoUnitSpec,
|
|
7
|
-
measurementHint,
|
|
8
|
-
parseMeasurement,
|
|
9
|
-
} from '../../../lib/measurement-parser'
|
|
5
|
+
import { lingoUnitSpec, measurementHint, parseMeasurement } from '../../../lib/measurement-parser'
|
|
10
6
|
import { useLinearDisplay } from '../../../lib/use-linear-display'
|
|
11
7
|
import { cn } from '../../../lib/utils'
|
|
12
8
|
|
|
@@ -31,8 +27,8 @@ export function MetricControl({
|
|
|
31
27
|
onCommit,
|
|
32
28
|
min = Number.NEGATIVE_INFINITY,
|
|
33
29
|
max = Number.POSITIVE_INFINITY,
|
|
34
|
-
precision = 2,
|
|
35
|
-
step = 1,
|
|
30
|
+
precision: storedPrecision = 2,
|
|
31
|
+
step: storedStep = 1,
|
|
36
32
|
className,
|
|
37
33
|
unit = '',
|
|
38
34
|
restoreOnCommit = true,
|
|
@@ -40,9 +36,12 @@ export function MetricControl({
|
|
|
40
36
|
const {
|
|
41
37
|
isImperial,
|
|
42
38
|
displayUnit,
|
|
39
|
+
parseUnit,
|
|
40
|
+
precision,
|
|
41
|
+
step,
|
|
43
42
|
toDisplay: toDisplayValue,
|
|
44
43
|
toStored: toStoredValue,
|
|
45
|
-
} = useLinearDisplay(unit,
|
|
44
|
+
} = useLinearDisplay(unit, storedPrecision, storedStep)
|
|
46
45
|
|
|
47
46
|
const clamp = useCallback(
|
|
48
47
|
(val: number) => {
|
|
@@ -110,7 +109,14 @@ export function MetricControl({
|
|
|
110
109
|
|
|
111
110
|
container.addEventListener('wheel', handleWheel, { passive: false })
|
|
112
111
|
return () => container.removeEventListener('wheel', handleWheel)
|
|
113
|
-
}, [
|
|
112
|
+
}, [
|
|
113
|
+
isEditing,
|
|
114
|
+
step,
|
|
115
|
+
clamp,
|
|
116
|
+
applyCommittedValue,
|
|
117
|
+
toStoredValue,
|
|
118
|
+
roundStoredValueForDisplayPrecision,
|
|
119
|
+
])
|
|
114
120
|
|
|
115
121
|
useEffect(() => {
|
|
116
122
|
if (!isHovered || isEditing) return
|
|
@@ -226,7 +232,7 @@ export function MetricControl({
|
|
|
226
232
|
const spec = lingoUnitSpec(unit)
|
|
227
233
|
let stored = spec
|
|
228
234
|
? parseMeasurement(inputValue, spec, {
|
|
229
|
-
bareUnit:
|
|
235
|
+
bareUnit: parseUnit ?? spec.unitId,
|
|
230
236
|
system: isImperial ? 'us' : 'metric',
|
|
231
237
|
})
|
|
232
238
|
: null
|
|
@@ -244,6 +250,7 @@ export function MetricControl({
|
|
|
244
250
|
inputValue,
|
|
245
251
|
unit,
|
|
246
252
|
isImperial,
|
|
253
|
+
parseUnit,
|
|
247
254
|
applyCommittedValue,
|
|
248
255
|
clamp,
|
|
249
256
|
toStoredValue,
|
|
@@ -256,9 +263,9 @@ export function MetricControl({
|
|
|
256
263
|
const hint =
|
|
257
264
|
isEditing && spec
|
|
258
265
|
? measurementHint(inputValue, spec, {
|
|
259
|
-
bareUnit:
|
|
266
|
+
bareUnit: parseUnit ?? spec.unitId,
|
|
260
267
|
system: isImperial ? 'us' : 'metric',
|
|
261
|
-
displayUnit:
|
|
268
|
+
displayUnit: parseUnit ?? spec.unitId,
|
|
262
269
|
precision,
|
|
263
270
|
clamp,
|
|
264
271
|
})
|
|
@@ -287,7 +294,16 @@ export function MetricControl({
|
|
|
287
294
|
setInputValue(toDisplayValue(newV).toFixed(precision))
|
|
288
295
|
}
|
|
289
296
|
},
|
|
290
|
-
[
|
|
297
|
+
[
|
|
298
|
+
submitValue,
|
|
299
|
+
value,
|
|
300
|
+
toDisplayValue,
|
|
301
|
+
precision,
|
|
302
|
+
step,
|
|
303
|
+
clamp,
|
|
304
|
+
applyCommittedValue,
|
|
305
|
+
toStoredValue,
|
|
306
|
+
],
|
|
291
307
|
)
|
|
292
308
|
|
|
293
309
|
return (
|
|
@@ -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"
|
|
@@ -2,11 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { useScene } from '@pascal-app/core'
|
|
4
4
|
import { useCallback, useEffect, useRef, useState } from 'react'
|
|
5
|
-
import {
|
|
6
|
-
lingoUnitSpec,
|
|
7
|
-
measurementHint,
|
|
8
|
-
parseMeasurement,
|
|
9
|
-
} from '../../../lib/measurement-parser'
|
|
5
|
+
import { lingoUnitSpec, measurementHint, parseMeasurement } from '../../../lib/measurement-parser'
|
|
10
6
|
import { useLinearDisplay } from '../../../lib/use-linear-display'
|
|
11
7
|
import { cn } from '../../../lib/utils'
|
|
12
8
|
|
|
@@ -22,6 +18,7 @@ interface SliderControlProps {
|
|
|
22
18
|
className?: string
|
|
23
19
|
unit?: string
|
|
24
20
|
restoreOnCommit?: boolean
|
|
21
|
+
mixed?: boolean
|
|
25
22
|
}
|
|
26
23
|
|
|
27
24
|
function stepPrecision(s: number): number {
|
|
@@ -59,23 +56,25 @@ export function SliderControl({
|
|
|
59
56
|
onCommit,
|
|
60
57
|
min = Number.NEGATIVE_INFINITY,
|
|
61
58
|
max = Number.POSITIVE_INFINITY,
|
|
62
|
-
precision = 0,
|
|
63
|
-
step = 1,
|
|
59
|
+
precision: storedPrecision = 0,
|
|
60
|
+
step: storedStep = 1,
|
|
64
61
|
className,
|
|
65
62
|
unit = '',
|
|
66
63
|
restoreOnCommit = true,
|
|
64
|
+
mixed = false,
|
|
67
65
|
}: SliderControlProps) {
|
|
68
|
-
//
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
// and rendered number are in the DISPLAY unit (feet when imperial). For
|
|
72
|
-
// metric and non-length units these conversions are the identity.
|
|
73
|
-
const { isImperial, displayUnit, toDisplay, toStored } = useLinearDisplay(unit, precision)
|
|
66
|
+
// Values and bounds stay in meters; gestures and input use the displayed unit.
|
|
67
|
+
const { isImperial, displayUnit, parseUnit, precision, step, toDisplay, toStored } =
|
|
68
|
+
useLinearDisplay(unit, storedPrecision, storedStep)
|
|
74
69
|
|
|
75
70
|
const [isEditing, setIsEditing] = useState(false)
|
|
76
71
|
const [isDragging, setIsDragging] = useState(false)
|
|
77
72
|
const [isHovered, setIsHovered] = useState(false)
|
|
78
73
|
const [inputValue, setInputValue] = useState(toDisplay(value).toFixed(precision))
|
|
74
|
+
// Live readout while dragging. Multi-edit previews write live overrides
|
|
75
|
+
// instead of the scene, so `value` would otherwise stay frozen even though
|
|
76
|
+
// the meshes are moving.
|
|
77
|
+
const [dragDisplay, setDragDisplay] = useState<number | null>(null)
|
|
79
78
|
|
|
80
79
|
const dragRef = useRef<{
|
|
81
80
|
// Original value at drag start — preserved across modifier re-anchors so
|
|
@@ -89,8 +88,9 @@ export function SliderControl({
|
|
|
89
88
|
stepMultiplier: number
|
|
90
89
|
} | null>(null)
|
|
91
90
|
const labelRef = useRef<HTMLDivElement>(null)
|
|
92
|
-
const
|
|
93
|
-
valueRef
|
|
91
|
+
const shown = dragDisplay ?? value
|
|
92
|
+
const valueRef = useRef(shown)
|
|
93
|
+
valueRef.current = shown
|
|
94
94
|
|
|
95
95
|
const clamp = useCallback((val: number) => Math.min(Math.max(val, min), max), [min, max])
|
|
96
96
|
// Apply a signed display-unit delta to a stored value, rounding in the
|
|
@@ -99,7 +99,9 @@ export function SliderControl({
|
|
|
99
99
|
(storedValue: number, displayDelta: number, displayStep: number) =>
|
|
100
100
|
clamp(
|
|
101
101
|
toStored(
|
|
102
|
-
Number.parseFloat(
|
|
102
|
+
Number.parseFloat(
|
|
103
|
+
(toDisplay(storedValue) + displayDelta).toFixed(stepPrecision(displayStep)),
|
|
104
|
+
),
|
|
103
105
|
),
|
|
104
106
|
),
|
|
105
107
|
[clamp, toDisplay, toStored],
|
|
@@ -185,6 +187,7 @@ export function SliderControl({
|
|
|
185
187
|
const newValue = applyDisplayDelta(anchorValue, (dx / 4) * s, s)
|
|
186
188
|
if (newValue !== valueRef.current) {
|
|
187
189
|
valueRef.current = newValue
|
|
190
|
+
setDragDisplay(newValue)
|
|
188
191
|
onChange(newValue)
|
|
189
192
|
}
|
|
190
193
|
},
|
|
@@ -209,6 +212,7 @@ export function SliderControl({
|
|
|
209
212
|
useScene.temporal.getState().resume()
|
|
210
213
|
onCommit?.(finalVal)
|
|
211
214
|
}
|
|
215
|
+
setDragDisplay(null)
|
|
212
216
|
},
|
|
213
217
|
[onChange, onCommit, restoreOnCommit],
|
|
214
218
|
)
|
|
@@ -222,7 +226,7 @@ export function SliderControl({
|
|
|
222
226
|
const spec = lingoUnitSpec(unit)
|
|
223
227
|
let stored = spec
|
|
224
228
|
? parseMeasurement(inputValue, spec, {
|
|
225
|
-
bareUnit:
|
|
229
|
+
bareUnit: parseUnit ?? spec.unitId,
|
|
226
230
|
system: isImperial ? 'us' : 'metric',
|
|
227
231
|
})
|
|
228
232
|
: null
|
|
@@ -239,15 +243,27 @@ export function SliderControl({
|
|
|
239
243
|
onCommit?.(nextValue)
|
|
240
244
|
}
|
|
241
245
|
setIsEditing(false)
|
|
242
|
-
}, [
|
|
246
|
+
}, [
|
|
247
|
+
inputValue,
|
|
248
|
+
unit,
|
|
249
|
+
isImperial,
|
|
250
|
+
parseUnit,
|
|
251
|
+
onChange,
|
|
252
|
+
onCommit,
|
|
253
|
+
clamp,
|
|
254
|
+
precision,
|
|
255
|
+
value,
|
|
256
|
+
toDisplay,
|
|
257
|
+
toStored,
|
|
258
|
+
])
|
|
243
259
|
|
|
244
260
|
const spec = lingoUnitSpec(unit)
|
|
245
261
|
const hint =
|
|
246
262
|
isEditing && spec
|
|
247
263
|
? measurementHint(inputValue, spec, {
|
|
248
|
-
bareUnit:
|
|
264
|
+
bareUnit: parseUnit ?? spec.unitId,
|
|
249
265
|
system: isImperial ? 'us' : 'metric',
|
|
250
|
-
displayUnit:
|
|
266
|
+
displayUnit: parseUnit ?? spec.unitId,
|
|
251
267
|
precision,
|
|
252
268
|
clamp,
|
|
253
269
|
})
|
|
@@ -272,7 +288,7 @@ export function SliderControl({
|
|
|
272
288
|
[submitValue, value, precision, step, applyDisplayDelta, onChange, toDisplay],
|
|
273
289
|
)
|
|
274
290
|
|
|
275
|
-
const displayValue = toDisplay(
|
|
291
|
+
const displayValue = toDisplay(shown)
|
|
276
292
|
|
|
277
293
|
return (
|
|
278
294
|
<div
|
|
@@ -340,6 +356,13 @@ export function SliderControl({
|
|
|
340
356
|
</span>
|
|
341
357
|
)}
|
|
342
358
|
</>
|
|
359
|
+
) : mixed && !isDragging ? (
|
|
360
|
+
<div
|
|
361
|
+
className="flex cursor-text items-center text-muted-foreground transition-colors hover:text-foreground"
|
|
362
|
+
onClick={handleValueClick}
|
|
363
|
+
>
|
|
364
|
+
<span className="font-mono tracking-tight">Mixed</span>
|
|
365
|
+
</div>
|
|
343
366
|
) : (
|
|
344
367
|
<div
|
|
345
368
|
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
|
)
|