@pascal-app/editor 0.7.0 → 0.9.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/package.json +12 -13
- package/src/components/editor/alignment-3d-guide-layer.tsx +160 -0
- package/src/components/editor/custom-camera-controls.tsx +755 -25
- package/src/components/editor/editor-layout-mobile.tsx +3 -3
- package/src/components/editor/editor-layout-v2.tsx +62 -54
- package/src/components/editor/export-manager.tsx +32 -3
- package/src/components/editor/first-person/build-collider-world.test.ts +149 -0
- package/src/components/editor/first-person/build-collider-world.ts +203 -87
- package/src/components/editor/first-person/bvh-ecctrl.tsx +127 -62
- package/src/components/editor/first-person-controls.tsx +836 -31
- package/src/components/editor/floating-action-menu.tsx +287 -196
- package/src/components/editor/floating-building-action-menu.tsx +1 -1
- package/src/components/editor/floorplan-panel.tsx +5837 -12006
- package/src/components/editor/grid.tsx +51 -17
- package/src/components/editor/group-move-handle.tsx +316 -0
- package/src/components/editor/group-rotate-handle.tsx +382 -0
- package/src/components/editor/group-transform-shared.test.ts +205 -0
- package/src/components/editor/group-transform-shared.ts +256 -0
- package/src/components/editor/handles/handle-arrow.tsx +523 -0
- package/src/components/editor/handles/use-handle-drag.ts +213 -0
- package/src/components/editor/index.tsx +234 -190
- package/src/components/editor/measurement-pill.tsx +75 -0
- package/src/components/editor/node-arrow-handles.tsx +1453 -0
- package/src/components/editor/selection-manager.tsx +435 -158
- package/src/components/editor/site-edge-labels.tsx +44 -20
- package/src/components/editor/slab-hole-highlights.tsx +466 -0
- package/src/components/editor/snapshot-capture-overlay.tsx +539 -0
- package/src/components/editor/thumbnail-generator.tsx +27 -64
- package/src/components/editor/use-floorplan-background-placement.ts +122 -35
- package/src/components/editor/use-floorplan-hit-testing.ts +24 -0
- package/src/components/editor/use-floorplan-scene-data.ts +14 -2
- package/src/components/editor/wall-measurement-label.tsx +11 -6
- package/src/components/editor/wall-move-side-handles.tsx +855 -0
- package/src/components/editor/wall-opening-highlights.tsx +181 -0
- package/src/components/editor/wall-snap-beacon-layer.tsx +150 -0
- package/src/components/editor-2d/floorplan-action-menu-layer.tsx +6 -1
- package/src/components/editor-2d/floorplan-alignment-guide-layer.tsx +142 -0
- package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +5 -3
- package/src/components/editor-2d/floorplan-registry-action-menu.tsx +227 -0
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +616 -0
- package/src/components/editor-2d/floorplan-render-context.tsx +62 -0
- package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +75 -0
- package/src/components/editor-2d/floorplan-wall-move-ghost-layer.tsx +47 -0
- package/src/components/editor-2d/renderers/floorplan-draft-layer.tsx +6 -1
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +266 -0
- package/src/components/editor-2d/renderers/floorplan-measurements-layer.tsx +108 -61
- package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +59 -0
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +1963 -0
- package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +29 -14
- package/src/components/editor-2d/svg-paths.ts +10 -2
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +30 -9
- package/src/components/systems/ceiling/ceiling-system.tsx +12 -8
- package/src/components/systems/roof/roof-edit-system.tsx +86 -12
- package/src/components/systems/stair/stair-edit-system.tsx +2 -2
- package/src/components/systems/zone/zone-label-editor-system.tsx +0 -0
- package/src/components/systems/zone/zone-system.tsx +0 -0
- package/src/components/tools/elevator/elevator-defaults.ts +8 -0
- package/src/components/tools/elevator/elevator-tool.tsx +349 -0
- package/src/components/tools/elevator/move-elevator-tool.tsx +252 -0
- package/src/components/tools/fence/fence-drafting.ts +64 -22
- package/src/components/tools/item/move-tool.tsx +40 -107
- package/src/components/tools/item/placement-math.test.ts +10 -0
- package/src/components/tools/item/placement-math.ts +33 -9
- package/src/components/tools/item/placement-strategies.ts +201 -25
- package/src/components/tools/item/placement-types.ts +10 -1
- package/src/components/tools/item/use-draft-node.ts +20 -2
- package/src/components/tools/item/use-placement-coordinator.tsx +700 -299
- package/src/components/tools/registry/move-registry-node-tool.tsx +602 -0
- package/src/components/tools/roof/roof-tool.tsx +108 -12
- package/src/components/tools/select/box-select-state.ts +77 -0
- package/src/components/tools/select/box-select-tool.tsx +324 -543
- package/src/components/tools/select/plane-box-select-tool.tsx +577 -0
- package/src/components/tools/select/screen-rectangle-selection.ts +84 -0
- package/src/components/tools/select/select-candidates.test.ts +109 -0
- package/src/components/tools/select/select-candidates.ts +85 -0
- package/src/components/tools/shared/affordance-dispatch.ts +30 -0
- package/src/components/tools/shared/drag-bounding-box.tsx +171 -0
- package/src/components/tools/shared/floor-stack-preview.ts +25 -0
- package/src/components/tools/shared/fresh-placement-visibility.ts +61 -0
- package/src/components/tools/shared/placement-box-geometry.ts +123 -0
- package/src/components/tools/shared/placement-box.tsx +123 -0
- package/src/components/tools/shared/polygon-editor.tsx +617 -167
- package/src/components/tools/shared/segment-angle.ts +57 -1
- package/src/components/tools/site/site-boundary-editor.tsx +464 -12
- package/src/components/tools/site/site-flag-model.ts +18 -0
- package/src/components/tools/stair/stair-defaults.ts +1 -1
- package/src/components/tools/stair/stair-tool.tsx +269 -35
- package/src/components/tools/tool-manager.tsx +172 -56
- package/src/components/tools/wall/wall-drafting.ts +98 -98
- package/src/components/tools/wall/wall-snap-geometry.test.ts +79 -0
- package/src/components/tools/wall/wall-snap-geometry.ts +270 -0
- package/src/components/tools/zone/zone-tool.tsx +36 -11
- package/src/components/ui/action-menu/action-button.tsx +21 -1
- package/src/components/ui/action-menu/camera-actions.tsx +0 -0
- package/src/components/ui/action-menu/control-modes.tsx +5 -76
- package/src/components/ui/action-menu/furnish-tools.tsx +6 -92
- package/src/components/ui/action-menu/index.tsx +33 -196
- package/src/components/ui/action-menu/structure-tools.tsx +13 -88
- package/src/components/ui/action-menu/view-toggles.tsx +262 -37
- package/src/components/ui/command-palette/editor-commands.tsx +24 -17
- package/src/components/ui/command-palette/index.tsx +6 -256
- package/src/components/ui/controls/material-paint-panel.tsx +83 -0
- package/src/components/ui/controls/material-picker.tsx +17 -35
- package/src/components/ui/controls/metric-control.tsx +33 -11
- package/src/components/ui/floating-level-selector.tsx +48 -11
- package/src/components/ui/helpers/helper-manager.tsx +21 -17
- package/src/components/ui/helpers/registered-tool-helper.tsx +25 -0
- package/src/components/ui/item-catalog/catalog-items.tsx +1743 -315
- package/src/components/ui/item-catalog/item-catalog.tsx +91 -46
- package/src/components/ui/level-duplicate-dialog.tsx +5 -6
- package/src/components/ui/panels/node-display.ts +1 -0
- package/src/components/ui/panels/paint-panel.tsx +66 -41
- package/src/components/ui/panels/panel-manager.tsx +33 -74
- package/src/components/ui/panels/panel-wrapper.tsx +217 -12
- package/src/components/ui/panels/parametric-inspector.tsx +469 -0
- package/src/components/ui/panels/reference-panel.tsx +28 -13
- package/src/components/ui/primitives/dropdown-menu.tsx +12 -8
- package/src/components/ui/primitives/number-input.tsx +1 -1
- package/src/components/ui/primitives/sidebar.tsx +0 -0
- package/src/components/ui/scene-loader.tsx +1 -3
- package/src/components/ui/sidebar/app-sidebar.tsx +0 -0
- package/src/components/ui/sidebar/icon-rail.tsx +0 -0
- package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +263 -0
- package/src/components/ui/sidebar/panels/items-panel/index.tsx +404 -0
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +59 -9
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +6 -1
- package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +259 -0
- package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +3 -1
- package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +5 -2
- package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +83 -0
- package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +89 -0
- package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +75 -0
- package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +2 -2
- package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +83 -0
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +8 -12
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +2 -1
- package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +93 -0
- package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +41 -5
- package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +128 -0
- package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +5 -2
- package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +83 -0
- package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +68 -34
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +3 -1
- package/src/components/ui/sidebar/panels/zone-panel/index.tsx +52 -3
- package/src/components/ui/sidebar/tab-bar.tsx +61 -1
- package/src/components/ui/slider.tsx +1 -1
- package/src/components/viewer-overlay.tsx +169 -52
- package/src/components/viewer-zone-system.tsx +1 -1
- package/src/hooks/use-auto-save.ts +24 -3
- package/src/hooks/use-ceiling-events.ts +175 -0
- package/src/hooks/use-drag-action.ts +124 -0
- package/src/hooks/use-keyboard.ts +125 -62
- package/src/hooks/use-selection.ts +64 -0
- package/src/index.tsx +243 -4
- package/src/lib/constants.ts +6 -2
- package/src/lib/editor-api.ts +62 -0
- package/src/lib/elevator-support.ts +96 -0
- package/src/lib/floorplan/apply-alignment.ts +111 -0
- package/src/lib/floorplan/index.ts +7 -0
- package/src/lib/floorplan/selection-tool.ts +40 -0
- package/src/lib/floorplan/stairs.ts +12 -1
- package/src/lib/fresh-planar-placement.test.ts +102 -0
- package/src/lib/fresh-planar-placement.ts +63 -0
- package/src/lib/history.ts +2 -1
- package/src/lib/level-duplication.test.ts +0 -2
- package/src/lib/level-duplication.ts +55 -43
- package/src/lib/level-name.ts +11 -0
- package/src/lib/material-paint.ts +212 -20
- package/src/lib/placement-metadata.ts +29 -0
- package/src/lib/planar-cursor-placement.test.ts +43 -0
- package/src/lib/planar-cursor-placement.ts +42 -0
- package/src/lib/roof-duplication.ts +2 -2
- package/src/lib/scene-bounds.ts +1 -1
- package/src/lib/scene-clipboard.ts +267 -0
- package/src/lib/scene.ts +13 -4
- package/src/lib/sfx-bus.ts +25 -1
- package/src/lib/sfx-player.ts +75 -27
- package/src/lib/site-boundary.ts +1 -0
- package/src/lib/stair-duplication.ts +2 -2
- package/src/lib/stair-levels.test.ts +146 -0
- package/src/lib/stair-levels.ts +177 -0
- package/src/lib/world-grid-snap.ts +199 -0
- package/src/lib/zone-content.ts +130 -0
- package/src/store/use-alignment-guides.ts +21 -0
- package/src/store/use-editor.tsx +342 -91
- package/src/store/use-placement-preview.ts +31 -0
- package/src/store/use-wall-move-ghosts.ts +36 -0
- package/src/store/use-wall-snap-indicator.ts +32 -0
- package/tsconfig.json +2 -1
- package/src/components/editor/preset-thumbnail-generator.tsx +0 -125
- package/src/components/editor-2d/renderers/floorplan-roof-layer.tsx +0 -113
- package/src/components/feedback-dialog.tsx +0 -265
- package/src/components/pascal-radio.tsx +0 -280
- package/src/components/preview-button.tsx +0 -16
- package/src/components/tools/building/move-building-tool.tsx +0 -157
- package/src/components/tools/ceiling/ceiling-boundary-editor.tsx +0 -43
- package/src/components/tools/ceiling/ceiling-hole-editor.tsx +0 -49
- package/src/components/tools/ceiling/ceiling-tool.tsx +0 -465
- package/src/components/tools/ceiling/move-ceiling-tool.tsx +0 -257
- package/src/components/tools/column/column-tool.tsx +0 -97
- package/src/components/tools/column/move-column-tool.tsx +0 -105
- package/src/components/tools/door/door-math.ts +0 -110
- package/src/components/tools/door/door-tool.tsx +0 -324
- package/src/components/tools/door/move-door-tool.tsx +0 -412
- package/src/components/tools/fence/curve-fence-tool.tsx +0 -179
- package/src/components/tools/fence/fence-tool.tsx +0 -346
- package/src/components/tools/fence/move-fence-endpoint-tool.tsx +0 -438
- package/src/components/tools/fence/move-fence-tool.tsx +0 -298
- package/src/components/tools/item/item-tool.tsx +0 -26
- package/src/components/tools/roof/move-roof-tool.tsx +0 -359
- package/src/components/tools/slab/move-slab-tool.tsx +0 -182
- package/src/components/tools/slab/slab-boundary-editor.tsx +0 -43
- package/src/components/tools/slab/slab-hole-editor.tsx +0 -49
- package/src/components/tools/slab/slab-tool.tsx +0 -322
- package/src/components/tools/spawn/move-spawn-tool.tsx +0 -101
- package/src/components/tools/spawn/spawn-tool.tsx +0 -130
- package/src/components/tools/wall/curve-wall-tool.tsx +0 -176
- package/src/components/tools/wall/move-wall-endpoint-tool.tsx +0 -423
- package/src/components/tools/wall/move-wall-tool.tsx +0 -356
- package/src/components/tools/wall/wall-tool.tsx +0 -332
- package/src/components/tools/window/move-window-tool.tsx +0 -447
- package/src/components/tools/window/window-math.ts +0 -117
- package/src/components/tools/window/window-tool.tsx +0 -332
- package/src/components/ui/helpers/ceiling-helper.tsx +0 -20
- package/src/components/ui/helpers/slab-helper.tsx +0 -20
- package/src/components/ui/helpers/wall-helper.tsx +0 -20
- package/src/components/ui/panels/ceiling-panel.tsx +0 -250
- package/src/components/ui/panels/column-panel.tsx +0 -715
- package/src/components/ui/panels/door-panel.tsx +0 -1292
- package/src/components/ui/panels/fence-panel.tsx +0 -227
- package/src/components/ui/panels/item-panel.tsx +0 -306
- package/src/components/ui/panels/presets/presets-popover.tsx +0 -511
- package/src/components/ui/panels/roof-panel.tsx +0 -230
- package/src/components/ui/panels/roof-segment-panel.tsx +0 -311
- package/src/components/ui/panels/slab-panel.tsx +0 -250
- package/src/components/ui/panels/spawn-panel.tsx +0 -155
- package/src/components/ui/panels/stair-panel.tsx +0 -525
- package/src/components/ui/panels/stair-segment-panel.tsx +0 -325
- package/src/components/ui/panels/wall-panel.tsx +0 -177
- package/src/components/ui/panels/window-panel.tsx +0 -960
- package/src/components/ui/viewer-toolbar.tsx +0 -436
- package/src/contexts/presets-context.tsx +0 -121
- package/src/hooks/use-contextual-tools.ts +0 -61
|
@@ -10,6 +10,7 @@ import { useEffect, useState } from 'react'
|
|
|
10
10
|
import { create } from 'zustand'
|
|
11
11
|
import { useShallow } from 'zustand/shallow'
|
|
12
12
|
import { Dialog, DialogContent, DialogTitle } from './../../../components/ui/primitives/dialog'
|
|
13
|
+
import { getLevelDisplayName } from '../../../lib/level-name'
|
|
13
14
|
import { useCommandRegistry } from '../../../store/use-command-registry'
|
|
14
15
|
import { usePaletteViewRegistry } from '../../../store/use-palette-view-registry'
|
|
15
16
|
|
|
@@ -27,15 +28,13 @@ interface CommandPaletteStore {
|
|
|
27
28
|
setInputValue: (value: string) => void
|
|
28
29
|
navigateTo: (page: string) => void
|
|
29
30
|
goBack: () => void
|
|
30
|
-
cameraScope: { nodeId: string; label: string } | null
|
|
31
|
-
setCameraScope: (scope: { nodeId: string; label: string } | null) => void
|
|
32
31
|
}
|
|
33
32
|
|
|
34
33
|
export const useCommandPalette = create<CommandPaletteStore>((set, get) => ({
|
|
35
34
|
open: false,
|
|
36
35
|
setOpen: (open) => {
|
|
37
36
|
set({ open })
|
|
38
|
-
if (!open) set({ pages: [], inputValue: '',
|
|
37
|
+
if (!open) set({ pages: [], inputValue: '', mode: 'command' })
|
|
39
38
|
},
|
|
40
39
|
mode: 'command',
|
|
41
40
|
setMode: (mode) => set({ mode }),
|
|
@@ -44,12 +43,8 @@ export const useCommandPalette = create<CommandPaletteStore>((set, get) => ({
|
|
|
44
43
|
setInputValue: (value) => set({ inputValue: value }),
|
|
45
44
|
navigateTo: (page) => set((s) => ({ pages: [...s.pages, page], inputValue: '' })),
|
|
46
45
|
goBack: () => {
|
|
47
|
-
const { pages } = get()
|
|
48
|
-
if (pages[pages.length - 1] === 'camera-scope') set({ cameraScope: null })
|
|
49
46
|
set((s) => ({ pages: s.pages.slice(0, -1), inputValue: '' }))
|
|
50
47
|
},
|
|
51
|
-
cameraScope: null,
|
|
52
|
-
setCameraScope: (scope) => set({ cameraScope: scope }),
|
|
53
48
|
}))
|
|
54
49
|
|
|
55
50
|
// ---------------------------------------------------------------------------
|
|
@@ -157,8 +152,6 @@ const PAGE_LABEL: Record<string, string> = {
|
|
|
157
152
|
'level-mode': 'Level Mode',
|
|
158
153
|
'rename-level': 'Rename Level',
|
|
159
154
|
'goto-level': 'Go to Level',
|
|
160
|
-
'camera-view': 'Camera Snapshot',
|
|
161
|
-
'camera-scope': '',
|
|
162
155
|
}
|
|
163
156
|
|
|
164
157
|
// ---------------------------------------------------------------------------
|
|
@@ -196,19 +189,8 @@ function EmptyActionItem({ action }: { action: CommandPaletteEmptyAction }) {
|
|
|
196
189
|
// Main component
|
|
197
190
|
// ---------------------------------------------------------------------------
|
|
198
191
|
export function CommandPalette({ emptyAction }: { emptyAction?: CommandPaletteEmptyAction }) {
|
|
199
|
-
const {
|
|
200
|
-
|
|
201
|
-
setOpen,
|
|
202
|
-
mode,
|
|
203
|
-
setMode,
|
|
204
|
-
pages,
|
|
205
|
-
inputValue,
|
|
206
|
-
setInputValue,
|
|
207
|
-
navigateTo,
|
|
208
|
-
goBack,
|
|
209
|
-
cameraScope,
|
|
210
|
-
setCameraScope,
|
|
211
|
-
} = useCommandPalette()
|
|
192
|
+
const { open, setOpen, mode, setMode, pages, inputValue, setInputValue, navigateTo, goBack } =
|
|
193
|
+
useCommandPalette()
|
|
212
194
|
|
|
213
195
|
const [meta, setMeta] = useState('⌘')
|
|
214
196
|
const [isFullscreen, setIsFullscreen] = useState(false)
|
|
@@ -233,11 +215,6 @@ export function CommandPalette({ emptyAction }: { emptyAction?: CommandPaletteEm
|
|
|
233
215
|
),
|
|
234
216
|
)
|
|
235
217
|
|
|
236
|
-
const cameraScopeNode = useScene((s) =>
|
|
237
|
-
cameraScope ? s.nodes[cameraScope.nodeId as AnyNodeId] : null,
|
|
238
|
-
)
|
|
239
|
-
const hasScopeSnapshot = !!(cameraScopeNode as any)?.camera
|
|
240
|
-
|
|
241
218
|
// Platform detection
|
|
242
219
|
useEffect(() => {
|
|
243
220
|
setMeta(/Mac|iPhone|iPad|iPod/.test(navigator.platform) ? '⌘' : 'Ctrl')
|
|
@@ -279,7 +256,6 @@ export function CommandPalette({ emptyAction }: { emptyAction?: CommandPaletteEm
|
|
|
279
256
|
solo: 'Solo',
|
|
280
257
|
}
|
|
281
258
|
|
|
282
|
-
// Camera snapshot helpers (used by sub-pages registered via EditorCommands)
|
|
283
259
|
const confirmRename = () => {
|
|
284
260
|
if (!(activeLevelId && inputValue.trim())) return
|
|
285
261
|
run(() => {
|
|
@@ -287,29 +263,6 @@ export function CommandPalette({ emptyAction }: { emptyAction?: CommandPaletteEm
|
|
|
287
263
|
})
|
|
288
264
|
}
|
|
289
265
|
|
|
290
|
-
const takeSnapshot = () => {
|
|
291
|
-
if (!cameraScope) return
|
|
292
|
-
import('@pascal-app/core').then(({ emitter }) => {
|
|
293
|
-
run(() =>
|
|
294
|
-
emitter.emit('camera-controls:capture', { nodeId: cameraScope.nodeId as AnyNodeId }),
|
|
295
|
-
)
|
|
296
|
-
})
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
const viewSnapshot = () => {
|
|
300
|
-
if (!(cameraScope && hasScopeSnapshot)) return
|
|
301
|
-
import('@pascal-app/core').then(({ emitter }) => {
|
|
302
|
-
run(() => emitter.emit('camera-controls:view', { nodeId: cameraScope.nodeId as AnyNodeId }))
|
|
303
|
-
})
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
const clearSnapshot = () => {
|
|
307
|
-
if (!(cameraScope && hasScopeSnapshot)) return
|
|
308
|
-
run(() => {
|
|
309
|
-
useScene.getState().updateNode(cameraScope.nodeId as AnyNodeId, { camera: undefined } as any)
|
|
310
|
-
})
|
|
311
|
-
}
|
|
312
|
-
|
|
313
266
|
// ---------------------------------------------------------------------------
|
|
314
267
|
// Group registered actions by group (preserving insertion order)
|
|
315
268
|
// ---------------------------------------------------------------------------
|
|
@@ -363,9 +316,7 @@ export function CommandPalette({ emptyAction }: { emptyAction?: CommandPaletteEm
|
|
|
363
316
|
onClick={goBack}
|
|
364
317
|
type="button"
|
|
365
318
|
>
|
|
366
|
-
{page
|
|
367
|
-
? (cameraScope?.label ?? 'Snapshot')
|
|
368
|
-
: (PAGE_LABEL[page] ?? views.get(page)?.label ?? page)}
|
|
319
|
+
{PAGE_LABEL[page] ?? views.get(page)?.label ?? page}
|
|
369
320
|
</button>
|
|
370
321
|
)}
|
|
371
322
|
<Command.Input
|
|
@@ -454,7 +405,7 @@ export function CommandPalette({ emptyAction }: { emptyAction?: CommandPaletteEm
|
|
|
454
405
|
<OptionItem
|
|
455
406
|
isActive={level.id === activeLevelId}
|
|
456
407
|
key={level.id}
|
|
457
|
-
label={level
|
|
408
|
+
label={getLevelDisplayName(level)}
|
|
458
409
|
onSelect={() =>
|
|
459
410
|
run(() => useViewer.getState().setSelection({ levelId: level.id }))
|
|
460
411
|
}
|
|
@@ -500,207 +451,6 @@ export function CommandPalette({ emptyAction }: { emptyAction?: CommandPaletteEm
|
|
|
500
451
|
</Command.Item>
|
|
501
452
|
</Command.Group>
|
|
502
453
|
)}
|
|
503
|
-
|
|
504
|
-
{/* ── Camera Snapshot: scope picker ─────────────────────────── */}
|
|
505
|
-
{page === 'camera-view' && (
|
|
506
|
-
<Command.Group heading="Camera Snapshot — Select Scope">
|
|
507
|
-
<OptionItem
|
|
508
|
-
icon={
|
|
509
|
-
<svg
|
|
510
|
-
className="h-4 w-4"
|
|
511
|
-
fill="none"
|
|
512
|
-
stroke="currentColor"
|
|
513
|
-
strokeWidth={2}
|
|
514
|
-
viewBox="0 0 24 24"
|
|
515
|
-
>
|
|
516
|
-
<path d="M3 3h18v18H3z" strokeLinecap="round" strokeLinejoin="round" />
|
|
517
|
-
<path d="M3 9h18M9 21V9" strokeLinecap="round" strokeLinejoin="round" />
|
|
518
|
-
</svg>
|
|
519
|
-
}
|
|
520
|
-
label="Site"
|
|
521
|
-
onSelect={() => {
|
|
522
|
-
const { rootNodeIds } = useScene.getState()
|
|
523
|
-
const siteId = rootNodeIds[0]
|
|
524
|
-
if (siteId) {
|
|
525
|
-
setCameraScope({ nodeId: siteId, label: 'Site' })
|
|
526
|
-
navigateTo('camera-scope')
|
|
527
|
-
}
|
|
528
|
-
}}
|
|
529
|
-
/>
|
|
530
|
-
<OptionItem
|
|
531
|
-
icon={
|
|
532
|
-
<svg
|
|
533
|
-
className="h-4 w-4"
|
|
534
|
-
fill="none"
|
|
535
|
-
stroke="currentColor"
|
|
536
|
-
strokeWidth={2}
|
|
537
|
-
viewBox="0 0 24 24"
|
|
538
|
-
>
|
|
539
|
-
<path
|
|
540
|
-
d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"
|
|
541
|
-
strokeLinecap="round"
|
|
542
|
-
strokeLinejoin="round"
|
|
543
|
-
/>
|
|
544
|
-
<polyline
|
|
545
|
-
points="9 22 9 12 15 12 15 22"
|
|
546
|
-
strokeLinecap="round"
|
|
547
|
-
strokeLinejoin="round"
|
|
548
|
-
/>
|
|
549
|
-
</svg>
|
|
550
|
-
}
|
|
551
|
-
label="Building"
|
|
552
|
-
onSelect={() => {
|
|
553
|
-
const building = Object.values(useScene.getState().nodes).find(
|
|
554
|
-
(n) => n.type === 'building',
|
|
555
|
-
)
|
|
556
|
-
if (building) {
|
|
557
|
-
setCameraScope({ nodeId: building.id, label: 'Building' })
|
|
558
|
-
navigateTo('camera-scope')
|
|
559
|
-
}
|
|
560
|
-
}}
|
|
561
|
-
/>
|
|
562
|
-
<OptionItem
|
|
563
|
-
disabled={!activeLevelId}
|
|
564
|
-
icon={
|
|
565
|
-
<svg
|
|
566
|
-
className="h-4 w-4"
|
|
567
|
-
fill="none"
|
|
568
|
-
stroke="currentColor"
|
|
569
|
-
strokeWidth={2}
|
|
570
|
-
viewBox="0 0 24 24"
|
|
571
|
-
>
|
|
572
|
-
<path
|
|
573
|
-
d="M12 2L2 7l10 5 10-5-10-5z"
|
|
574
|
-
strokeLinecap="round"
|
|
575
|
-
strokeLinejoin="round"
|
|
576
|
-
/>
|
|
577
|
-
<path
|
|
578
|
-
d="M2 17l10 5 10-5M2 12l10 5 10-5"
|
|
579
|
-
strokeLinecap="round"
|
|
580
|
-
strokeLinejoin="round"
|
|
581
|
-
/>
|
|
582
|
-
</svg>
|
|
583
|
-
}
|
|
584
|
-
label="Level"
|
|
585
|
-
onSelect={() => {
|
|
586
|
-
if (activeLevelId) {
|
|
587
|
-
setCameraScope({ nodeId: activeLevelId, label: 'Level' })
|
|
588
|
-
navigateTo('camera-scope')
|
|
589
|
-
}
|
|
590
|
-
}}
|
|
591
|
-
/>
|
|
592
|
-
<OptionItem
|
|
593
|
-
disabled={!useViewer.getState().selection.selectedIds.length}
|
|
594
|
-
icon={
|
|
595
|
-
<svg
|
|
596
|
-
className="h-4 w-4"
|
|
597
|
-
fill="none"
|
|
598
|
-
stroke="currentColor"
|
|
599
|
-
strokeWidth={2}
|
|
600
|
-
viewBox="0 0 24 24"
|
|
601
|
-
>
|
|
602
|
-
<path d="M5 3l14 9-14 9V3z" strokeLinecap="round" strokeLinejoin="round" />
|
|
603
|
-
</svg>
|
|
604
|
-
}
|
|
605
|
-
label="Selection"
|
|
606
|
-
onSelect={() => {
|
|
607
|
-
const firstId = useViewer.getState().selection.selectedIds[0]
|
|
608
|
-
if (firstId) {
|
|
609
|
-
setCameraScope({ nodeId: firstId, label: 'Selection' })
|
|
610
|
-
navigateTo('camera-scope')
|
|
611
|
-
}
|
|
612
|
-
}}
|
|
613
|
-
/>
|
|
614
|
-
</Command.Group>
|
|
615
|
-
)}
|
|
616
|
-
|
|
617
|
-
{/* ── Camera Snapshot: actions for selected scope ───────────── */}
|
|
618
|
-
{page === 'camera-scope' && cameraScope && (
|
|
619
|
-
<Command.Group heading={`${cameraScope.label} Snapshot`}>
|
|
620
|
-
<OptionItem
|
|
621
|
-
icon={
|
|
622
|
-
<svg
|
|
623
|
-
className="h-4 w-4"
|
|
624
|
-
fill="none"
|
|
625
|
-
stroke="currentColor"
|
|
626
|
-
strokeWidth={2}
|
|
627
|
-
viewBox="0 0 24 24"
|
|
628
|
-
>
|
|
629
|
-
<path
|
|
630
|
-
d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"
|
|
631
|
-
strokeLinecap="round"
|
|
632
|
-
strokeLinejoin="round"
|
|
633
|
-
/>
|
|
634
|
-
<circle
|
|
635
|
-
cx="12"
|
|
636
|
-
cy="13"
|
|
637
|
-
r="4"
|
|
638
|
-
strokeLinecap="round"
|
|
639
|
-
strokeLinejoin="round"
|
|
640
|
-
/>
|
|
641
|
-
</svg>
|
|
642
|
-
}
|
|
643
|
-
label={hasScopeSnapshot ? 'Update Snapshot' : 'Take Snapshot'}
|
|
644
|
-
onSelect={takeSnapshot}
|
|
645
|
-
/>
|
|
646
|
-
{hasScopeSnapshot && (
|
|
647
|
-
<OptionItem
|
|
648
|
-
icon={
|
|
649
|
-
<svg
|
|
650
|
-
className="h-4 w-4"
|
|
651
|
-
fill="none"
|
|
652
|
-
stroke="currentColor"
|
|
653
|
-
strokeWidth={2}
|
|
654
|
-
viewBox="0 0 24 24"
|
|
655
|
-
>
|
|
656
|
-
<path
|
|
657
|
-
d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"
|
|
658
|
-
strokeLinecap="round"
|
|
659
|
-
strokeLinejoin="round"
|
|
660
|
-
/>
|
|
661
|
-
<circle
|
|
662
|
-
cx="12"
|
|
663
|
-
cy="12"
|
|
664
|
-
r="3"
|
|
665
|
-
strokeLinecap="round"
|
|
666
|
-
strokeLinejoin="round"
|
|
667
|
-
/>
|
|
668
|
-
</svg>
|
|
669
|
-
}
|
|
670
|
-
label="View Snapshot"
|
|
671
|
-
onSelect={viewSnapshot}
|
|
672
|
-
/>
|
|
673
|
-
)}
|
|
674
|
-
{hasScopeSnapshot && (
|
|
675
|
-
<OptionItem
|
|
676
|
-
icon={
|
|
677
|
-
<svg
|
|
678
|
-
className="h-4 w-4"
|
|
679
|
-
fill="none"
|
|
680
|
-
stroke="currentColor"
|
|
681
|
-
strokeWidth={2}
|
|
682
|
-
viewBox="0 0 24 24"
|
|
683
|
-
>
|
|
684
|
-
<polyline
|
|
685
|
-
points="3 6 5 6 21 6"
|
|
686
|
-
strokeLinecap="round"
|
|
687
|
-
strokeLinejoin="round"
|
|
688
|
-
/>
|
|
689
|
-
<path
|
|
690
|
-
d="M19 6l-1 14H6L5 6"
|
|
691
|
-
strokeLinecap="round"
|
|
692
|
-
strokeLinejoin="round"
|
|
693
|
-
/>
|
|
694
|
-
<path d="M10 11v6M14 11v6" strokeLinecap="round" strokeLinejoin="round" />
|
|
695
|
-
<path d="M9 6V4h6v2" strokeLinecap="round" strokeLinejoin="round" />
|
|
696
|
-
</svg>
|
|
697
|
-
}
|
|
698
|
-
label="Clear Snapshot"
|
|
699
|
-
onSelect={clearSnapshot}
|
|
700
|
-
/>
|
|
701
|
-
)}
|
|
702
|
-
</Command.Group>
|
|
703
|
-
)}
|
|
704
454
|
</Command.List>
|
|
705
455
|
|
|
706
456
|
{/* Footer hint */}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { type AnyNodeId, useScene } from '@pascal-app/core'
|
|
4
|
+
import { useViewer } from '@pascal-app/viewer'
|
|
5
|
+
import { Eraser, RotateCcw } from 'lucide-react'
|
|
6
|
+
import { useEffect } from 'react'
|
|
7
|
+
import {
|
|
8
|
+
buildResetSurfaceMaterialUpdates,
|
|
9
|
+
resolvePaintTargetFromSelection,
|
|
10
|
+
} from './../../../lib/material-paint'
|
|
11
|
+
import useEditor from './../../../store/use-editor'
|
|
12
|
+
import { Button } from '../primitives/button'
|
|
13
|
+
import { MaterialPicker } from './material-picker'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Material picker for paint mode. Embedders render this wherever paint controls
|
|
17
|
+
* belong (the community editor places it in the Build sidebar while paint mode
|
|
18
|
+
* is active). It owns the paint-target/material wiring so the host only needs
|
|
19
|
+
* to mount it; it fills its container's width.
|
|
20
|
+
*/
|
|
21
|
+
export function MaterialPaintPanel() {
|
|
22
|
+
const activePaintMaterial = useEditor((state) => state.activePaintMaterial)
|
|
23
|
+
const activePaintTarget = useEditor((state) => state.activePaintTarget)
|
|
24
|
+
const setActivePaintMaterial = useEditor((state) => state.setActivePaintMaterial)
|
|
25
|
+
const setActivePaintTarget = useEditor((state) => state.setActivePaintTarget)
|
|
26
|
+
const paintEraser = useEditor((state) => state.paintEraser)
|
|
27
|
+
const setPaintEraser = useEditor((state) => state.setPaintEraser)
|
|
28
|
+
const selectedIds = useViewer((state) => state.selection.selectedIds)
|
|
29
|
+
const nodes = useScene((state) => state.nodes)
|
|
30
|
+
const selectedId = selectedIds.length === 1 ? (selectedIds[0] ?? null) : null
|
|
31
|
+
const selectedNode = selectedId ? nodes[selectedId as AnyNodeId] : null
|
|
32
|
+
const canResetSelection =
|
|
33
|
+
selectedNode != null && resolvePaintTargetFromSelection({ nodes, selectedId }) != null
|
|
34
|
+
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
const selectedPaintTarget = resolvePaintTargetFromSelection({ nodes, selectedId })
|
|
37
|
+
if (selectedPaintTarget) {
|
|
38
|
+
setActivePaintTarget(selectedPaintTarget)
|
|
39
|
+
}
|
|
40
|
+
}, [nodes, selectedId, setActivePaintTarget])
|
|
41
|
+
|
|
42
|
+
const resetSelection = () => {
|
|
43
|
+
if (!selectedNode) return
|
|
44
|
+
useScene.getState().updateNodes(buildResetSurfaceMaterialUpdates(nodes, selectedNode))
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<div className="w-full space-y-2">
|
|
49
|
+
<div className="flex items-center gap-2">
|
|
50
|
+
<Button
|
|
51
|
+
aria-pressed={paintEraser}
|
|
52
|
+
className="flex-1"
|
|
53
|
+
onClick={() => setPaintEraser(!paintEraser)}
|
|
54
|
+
size="sm"
|
|
55
|
+
variant={paintEraser ? 'default' : 'outline'}
|
|
56
|
+
>
|
|
57
|
+
<Eraser />
|
|
58
|
+
Erase
|
|
59
|
+
</Button>
|
|
60
|
+
<Button
|
|
61
|
+
className="flex-1"
|
|
62
|
+
disabled={!canResetSelection}
|
|
63
|
+
onClick={resetSelection}
|
|
64
|
+
size="sm"
|
|
65
|
+
variant="outline"
|
|
66
|
+
>
|
|
67
|
+
<RotateCcw />
|
|
68
|
+
Reset all
|
|
69
|
+
</Button>
|
|
70
|
+
</div>
|
|
71
|
+
<MaterialPicker
|
|
72
|
+
onChange={(material) => {
|
|
73
|
+
setActivePaintMaterial({ material, sourceTarget: activePaintTarget })
|
|
74
|
+
}}
|
|
75
|
+
onSelectMaterialPreset={(materialPreset) => {
|
|
76
|
+
setActivePaintMaterial({ materialPreset, sourceTarget: activePaintTarget })
|
|
77
|
+
}}
|
|
78
|
+
selectedMaterialPreset={activePaintMaterial?.materialPreset}
|
|
79
|
+
value={activePaintMaterial?.material}
|
|
80
|
+
/>
|
|
81
|
+
</div>
|
|
82
|
+
)
|
|
83
|
+
}
|
|
@@ -5,8 +5,9 @@ import {
|
|
|
5
5
|
getLibraryMaterialIdFromRef,
|
|
6
6
|
getMaterialsForCategory,
|
|
7
7
|
MATERIAL_CATEGORIES,
|
|
8
|
-
toLibraryMaterialRef,
|
|
9
8
|
type MaterialSchema,
|
|
9
|
+
type MaterialTarget,
|
|
10
|
+
toLibraryMaterialRef,
|
|
10
11
|
} from '@pascal-app/core'
|
|
11
12
|
import { useEffect, useRef, useState } from 'react'
|
|
12
13
|
import useEditor from '../../../store/use-editor'
|
|
@@ -17,6 +18,13 @@ type MaterialPickerProps = {
|
|
|
17
18
|
onChange?: (material: MaterialSchema) => void
|
|
18
19
|
onSelectMaterialPreset?: (materialPreset: string) => void
|
|
19
20
|
disabled?: boolean
|
|
21
|
+
nodeType?: MaterialTarget
|
|
22
|
+
hideSideControl?: boolean
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function getCategoryLabel(category: (typeof MATERIAL_CATEGORIES)[number]) {
|
|
26
|
+
if (category === 'roof') return 'Roofing'
|
|
27
|
+
return category.charAt(0).toUpperCase() + category.slice(1)
|
|
20
28
|
}
|
|
21
29
|
|
|
22
30
|
export function MaterialPicker({
|
|
@@ -31,7 +39,6 @@ export function MaterialPicker({
|
|
|
31
39
|
const [selectedCategory, setSelectedCategory] = useState<(typeof MATERIAL_CATEGORIES)[number]>(
|
|
32
40
|
MATERIAL_CATEGORIES[0],
|
|
33
41
|
)
|
|
34
|
-
const catalogScrollRef = useRef<HTMLDivElement>(null)
|
|
35
42
|
const categoryScrollRef = useRef<HTMLDivElement>(null)
|
|
36
43
|
const catalogItems =
|
|
37
44
|
selectedCategory === 'other'
|
|
@@ -48,8 +55,7 @@ export function MaterialPicker({
|
|
|
48
55
|
return
|
|
49
56
|
}
|
|
50
57
|
|
|
51
|
-
const catalogId =
|
|
52
|
-
getLibraryMaterialIdFromRef(selectedMaterialPreset) ?? value?.id ?? undefined
|
|
58
|
+
const catalogId = getLibraryMaterialIdFromRef(selectedMaterialPreset) ?? value?.id ?? undefined
|
|
53
59
|
const selectedCatalogEntry = getCatalogMaterialById(catalogId)
|
|
54
60
|
if (selectedCatalogEntry?.category) {
|
|
55
61
|
setSelectedCategory(selectedCatalogEntry.category)
|
|
@@ -66,27 +72,6 @@ export function MaterialPicker({
|
|
|
66
72
|
onSelectMaterialPreset?.(toLibraryMaterialRef(materialId))
|
|
67
73
|
}
|
|
68
74
|
|
|
69
|
-
useEffect(() => {
|
|
70
|
-
const container = catalogScrollRef.current
|
|
71
|
-
if (!container) return
|
|
72
|
-
|
|
73
|
-
const handleWheel = (event: WheelEvent) => {
|
|
74
|
-
const deltaX = event.deltaX
|
|
75
|
-
const deltaY = event.deltaY
|
|
76
|
-
const nextScrollLeft = container.scrollLeft + deltaX + deltaY
|
|
77
|
-
|
|
78
|
-
if (nextScrollLeft === container.scrollLeft) return
|
|
79
|
-
|
|
80
|
-
event.preventDefault()
|
|
81
|
-
container.scrollLeft = nextScrollLeft
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
container.addEventListener('wheel', handleWheel, { passive: false })
|
|
85
|
-
return () => {
|
|
86
|
-
container.removeEventListener('wheel', handleWheel)
|
|
87
|
-
}
|
|
88
|
-
}, [catalogItems.length, onChange, showCustom])
|
|
89
|
-
|
|
90
75
|
useEffect(() => {
|
|
91
76
|
const container = categoryScrollRef.current
|
|
92
77
|
if (!container) return
|
|
@@ -154,20 +139,18 @@ export function MaterialPicker({
|
|
|
154
139
|
}}
|
|
155
140
|
type="button"
|
|
156
141
|
>
|
|
157
|
-
{
|
|
142
|
+
{getCategoryLabel(category)}
|
|
158
143
|
</button>
|
|
159
144
|
))}
|
|
160
145
|
</div>
|
|
161
146
|
</div>
|
|
162
147
|
<div
|
|
163
|
-
className="
|
|
164
|
-
|
|
165
|
-
style={{ msOverflowStyle: 'none', scrollbarWidth: 'none' }}
|
|
148
|
+
className="grid gap-1.5 pb-1"
|
|
149
|
+
style={{ gridTemplateColumns: 'repeat(auto-fill, minmax(72px, 1fr))' }}
|
|
166
150
|
>
|
|
167
|
-
|
|
168
|
-
{catalogItems.map((item) => (
|
|
151
|
+
{catalogItems.map((item) => (
|
|
169
152
|
<button
|
|
170
|
-
className={`relative
|
|
153
|
+
className={`relative aspect-square w-full overflow-hidden rounded-lg border transition-all ${
|
|
171
154
|
selectedCatalogId === toLibraryMaterialRef(item.id)
|
|
172
155
|
? 'border-blue-500 ring-2 ring-blue-500/30'
|
|
173
156
|
: 'border-gray-300 hover:border-gray-400'
|
|
@@ -177,7 +160,7 @@ export function MaterialPicker({
|
|
|
177
160
|
title={item.label}
|
|
178
161
|
type="button"
|
|
179
162
|
>
|
|
180
|
-
<div className="pointer-events-none absolute inset-0 rounded-[inherit] ring-1 ring-
|
|
163
|
+
<div className="pointer-events-none absolute inset-0 rounded-[inherit] ring-1 ring-white/12 ring-inset" />
|
|
181
164
|
{item.previewThumbnailUrl ? (
|
|
182
165
|
<img
|
|
183
166
|
alt={item.label}
|
|
@@ -193,7 +176,7 @@ export function MaterialPicker({
|
|
|
193
176
|
))}
|
|
194
177
|
{selectedCategory === 'other' && onChange ? (
|
|
195
178
|
<button
|
|
196
|
-
className={`flex
|
|
179
|
+
className={`flex aspect-square w-full items-center justify-center rounded-lg border font-medium text-[10px] transition-all ${
|
|
197
180
|
showCustom
|
|
198
181
|
? 'border-blue-500 ring-2 ring-blue-500/30'
|
|
199
182
|
: 'border-gray-300 hover:border-gray-400'
|
|
@@ -205,7 +188,6 @@ export function MaterialPicker({
|
|
|
205
188
|
Custom
|
|
206
189
|
</button>
|
|
207
190
|
) : null}
|
|
208
|
-
</div>
|
|
209
191
|
</div>
|
|
210
192
|
</div>
|
|
211
193
|
)}
|
|
@@ -9,24 +9,28 @@ interface MetricControlProps {
|
|
|
9
9
|
label: React.ReactNode
|
|
10
10
|
value: number
|
|
11
11
|
onChange: (value: number) => void
|
|
12
|
+
onCommit?: (value: number) => void
|
|
12
13
|
min?: number
|
|
13
14
|
max?: number
|
|
14
15
|
precision?: number
|
|
15
16
|
step?: number
|
|
16
17
|
className?: string
|
|
17
18
|
unit?: string
|
|
19
|
+
restoreOnCommit?: boolean
|
|
18
20
|
}
|
|
19
21
|
|
|
20
22
|
export function MetricControl({
|
|
21
23
|
label,
|
|
22
24
|
value,
|
|
23
25
|
onChange,
|
|
26
|
+
onCommit,
|
|
24
27
|
min = Number.NEGATIVE_INFINITY,
|
|
25
28
|
max = Number.POSITIVE_INFINITY,
|
|
26
29
|
precision = 2,
|
|
27
30
|
step = 1,
|
|
28
31
|
className,
|
|
29
32
|
unit = '',
|
|
33
|
+
restoreOnCommit = true,
|
|
30
34
|
}: MetricControlProps) {
|
|
31
35
|
const viewerUnit = useViewer((state) => state.unit)
|
|
32
36
|
const isImperial = viewerUnit === 'imperial' && unit === 'm'
|
|
@@ -53,6 +57,17 @@ export function MetricControl({
|
|
|
53
57
|
[min, max],
|
|
54
58
|
)
|
|
55
59
|
|
|
60
|
+
const applyCommittedValue = useCallback(
|
|
61
|
+
(nextValue: number) => {
|
|
62
|
+
if (onCommit) {
|
|
63
|
+
onCommit(nextValue)
|
|
64
|
+
} else {
|
|
65
|
+
onChange(nextValue)
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
[onChange, onCommit],
|
|
69
|
+
)
|
|
70
|
+
|
|
56
71
|
useEffect(() => {
|
|
57
72
|
if (!isEditing) {
|
|
58
73
|
setInputValue(displayValue.toFixed(precision))
|
|
@@ -77,13 +92,13 @@ export function MetricControl({
|
|
|
77
92
|
const finalValue = Number.parseFloat((newValue * multiplier).toFixed(precision)) / multiplier
|
|
78
93
|
|
|
79
94
|
if (Math.abs(finalValue - valueRef.current) > 1e-6) {
|
|
80
|
-
|
|
95
|
+
applyCommittedValue(finalValue)
|
|
81
96
|
}
|
|
82
97
|
}
|
|
83
98
|
|
|
84
99
|
container.addEventListener('wheel', handleWheel, { passive: false })
|
|
85
100
|
return () => container.removeEventListener('wheel', handleWheel)
|
|
86
|
-
}, [isEditing, step, clamp,
|
|
101
|
+
}, [isEditing, step, clamp, applyCommittedValue, precision, multiplier])
|
|
87
102
|
|
|
88
103
|
useEffect(() => {
|
|
89
104
|
if (!isHovered || isEditing) return
|
|
@@ -104,14 +119,14 @@ export function MetricControl({
|
|
|
104
119
|
Number.parseFloat((newValue * multiplier).toFixed(precision)) / multiplier
|
|
105
120
|
|
|
106
121
|
if (Math.abs(finalValue - valueRef.current) > 1e-6) {
|
|
107
|
-
|
|
122
|
+
applyCommittedValue(finalValue)
|
|
108
123
|
}
|
|
109
124
|
}
|
|
110
125
|
}
|
|
111
126
|
|
|
112
127
|
window.addEventListener('keydown', handleKeyDown)
|
|
113
128
|
return () => window.removeEventListener('keydown', handleKeyDown)
|
|
114
|
-
}, [isHovered, isEditing, step, clamp,
|
|
129
|
+
}, [isHovered, isEditing, step, clamp, applyCommittedValue, precision, multiplier])
|
|
115
130
|
|
|
116
131
|
const handlePointerDown = useCallback(
|
|
117
132
|
(e: React.PointerEvent) => {
|
|
@@ -148,7 +163,14 @@ export function MetricControl({
|
|
|
148
163
|
document.removeEventListener('pointermove', handlePointerMove)
|
|
149
164
|
document.removeEventListener('pointerup', handlePointerUp)
|
|
150
165
|
|
|
151
|
-
|
|
166
|
+
const changed = Math.abs(finalValue - startValueRef.current) > 1e-6
|
|
167
|
+
if (onCommit) {
|
|
168
|
+
if (changed && restoreOnCommit) {
|
|
169
|
+
onChange(startValueRef.current)
|
|
170
|
+
}
|
|
171
|
+
useScene.temporal.getState().resume()
|
|
172
|
+
onCommit(finalValue)
|
|
173
|
+
} else if (changed) {
|
|
152
174
|
onChange(startValueRef.current)
|
|
153
175
|
useScene.temporal.getState().resume()
|
|
154
176
|
onChange(finalValue)
|
|
@@ -160,7 +182,7 @@ export function MetricControl({
|
|
|
160
182
|
document.addEventListener('pointermove', handlePointerMove)
|
|
161
183
|
document.addEventListener('pointerup', handlePointerUp)
|
|
162
184
|
},
|
|
163
|
-
[isEditing, value, onChange, clamp, precision, step, multiplier],
|
|
185
|
+
[isEditing, value, onChange, onCommit, restoreOnCommit, clamp, precision, step, multiplier],
|
|
164
186
|
)
|
|
165
187
|
|
|
166
188
|
const handleValueClick = useCallback(() => {
|
|
@@ -177,10 +199,10 @@ export function MetricControl({
|
|
|
177
199
|
if (Number.isNaN(numValue)) {
|
|
178
200
|
setInputValue((value * multiplier).toFixed(precision))
|
|
179
201
|
} else {
|
|
180
|
-
|
|
202
|
+
applyCommittedValue(clamp(numValue / multiplier))
|
|
181
203
|
}
|
|
182
204
|
setIsEditing(false)
|
|
183
|
-
}, [inputValue,
|
|
205
|
+
}, [inputValue, applyCommittedValue, clamp, multiplier, value, precision])
|
|
184
206
|
|
|
185
207
|
const handleInputBlur = useCallback(() => {
|
|
186
208
|
submitValue()
|
|
@@ -196,16 +218,16 @@ export function MetricControl({
|
|
|
196
218
|
} else if (e.key === 'ArrowUp') {
|
|
197
219
|
e.preventDefault()
|
|
198
220
|
const newV = clamp(value + step / multiplier)
|
|
199
|
-
|
|
221
|
+
applyCommittedValue(newV)
|
|
200
222
|
setInputValue((newV * multiplier).toFixed(precision))
|
|
201
223
|
} else if (e.key === 'ArrowDown') {
|
|
202
224
|
e.preventDefault()
|
|
203
225
|
const newV = clamp(value - step / multiplier)
|
|
204
|
-
|
|
226
|
+
applyCommittedValue(newV)
|
|
205
227
|
setInputValue((newV * multiplier).toFixed(precision))
|
|
206
228
|
}
|
|
207
229
|
},
|
|
208
|
-
[submitValue, value, multiplier, precision, step, clamp,
|
|
230
|
+
[submitValue, value, multiplier, precision, step, clamp, applyCommittedValue],
|
|
209
231
|
)
|
|
210
232
|
|
|
211
233
|
return (
|