@pascal-app/editor 0.8.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 +8 -9
- package/src/components/editor/alignment-3d-guide-layer.tsx +160 -0
- package/src/components/editor/custom-camera-controls.tsx +754 -25
- package/src/components/editor/editor-layout-mobile.tsx +3 -3
- package/src/components/editor/editor-layout-v2.tsx +58 -51
- 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 +201 -83
- package/src/components/editor/first-person/bvh-ecctrl.tsx +8 -8
- package/src/components/editor/first-person-controls.tsx +825 -20
- 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 +5829 -12005
- 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 +178 -149
- 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 +433 -156
- 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 +136 -62
- package/src/components/editor/thumbnail-generator.tsx +9 -3
- package/src/components/editor/use-floorplan-background-placement.ts +119 -32
- 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-geometry-renderer.tsx +266 -0
- package/src/components/editor-2d/renderers/floorplan-measurements-layer.tsx +108 -66
- 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 +24 -9
- package/src/components/editor-2d/svg-paths.ts +10 -2
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +26 -3
- 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 +1 -1
- 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 +41 -105
- package/src/components/tools/item/placement-math.test.ts +10 -0
- package/src/components/tools/item/placement-math.ts +31 -5
- package/src/components/tools/item/placement-strategies.ts +190 -15
- package/src/components/tools/item/placement-types.ts +10 -1
- package/src/components/tools/item/use-draft-node.ts +20 -1
- package/src/components/tools/item/use-placement-coordinator.tsx +694 -191
- 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 -538
- 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 +56 -0
- 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 +16 -6
- package/src/components/ui/action-menu/action-button.tsx +21 -1
- package/src/components/ui/action-menu/control-modes.tsx +5 -82
- package/src/components/ui/action-menu/index.tsx +1 -113
- package/src/components/ui/action-menu/structure-tools.tsx +13 -88
- package/src/components/ui/action-menu/view-toggles.tsx +243 -6
- package/src/components/ui/command-palette/editor-commands.tsx +18 -13
- package/src/components/ui/command-palette/index.tsx +2 -1
- package/src/components/ui/controls/material-paint-panel.tsx +83 -0
- package/src/components/ui/controls/material-picker.tsx +11 -32
- package/src/components/ui/controls/metric-control.tsx +33 -11
- package/src/components/ui/floating-level-selector.tsx +47 -10
- package/src/components/ui/helpers/helper-manager.tsx +16 -17
- package/src/components/ui/helpers/registered-tool-helper.tsx +25 -0
- package/src/components/ui/item-catalog/catalog-items.tsx +1 -0
- package/src/components/ui/item-catalog/item-catalog.tsx +3 -0
- package/src/components/ui/level-duplicate-dialog.tsx +2 -1
- package/src/components/ui/panels/node-display.ts +1 -0
- package/src/components/ui/panels/panel-manager.tsx +35 -47
- package/src/components/ui/panels/panel-wrapper.tsx +217 -12
- package/src/components/ui/panels/parametric-inspector.tsx +469 -0
- package/src/components/ui/primitives/dropdown-menu.tsx +12 -8
- package/src/components/ui/scene-loader.tsx +1 -3
- package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +263 -0
- package/src/components/ui/sidebar/panels/items-panel/index.tsx +82 -8
- 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 +4 -6
- 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 +7 -1
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +68 -36
- package/src/components/ui/sidebar/panels/zone-panel/index.tsx +51 -2
- package/src/components/ui/sidebar/tab-bar.tsx +61 -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 +10 -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 +115 -62
- package/src/hooks/use-selection.ts +64 -0
- package/src/index.tsx +237 -5
- 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.ts +54 -42
- package/src/lib/level-name.ts +11 -0
- package/src/lib/material-paint.ts +211 -19
- 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 +1 -1
- package/src/lib/scene-clipboard.ts +267 -0
- package/src/lib/scene.ts +13 -4
- package/src/lib/sfx-bus.ts +23 -1
- package/src/lib/sfx-player.ts +70 -22
- package/src/lib/site-boundary.ts +1 -0
- 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 +320 -37
- 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/src/components/editor/preset-thumbnail-generator.tsx +0 -125
- package/src/components/editor-2d/renderers/floorplan-roof-layer.tsx +0 -113
- 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 -264
- 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 -178
- package/src/components/tools/fence/fence-tool.tsx +0 -346
- package/src/components/tools/fence/move-fence-endpoint-tool.tsx +0 -441
- package/src/components/tools/fence/move-fence-tool.tsx +0 -302
- package/src/components/tools/item/item-tool.tsx +0 -26
- package/src/components/tools/roof/move-roof-tool.tsx +0 -364
- 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 -178
- package/src/components/tools/wall/move-wall-endpoint-tool.tsx +0 -426
- package/src/components/tools/wall/move-wall-tool.tsx +0 -358
- 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 -249
- package/src/components/ui/panels/column-panel.tsx +0 -759
- package/src/components/ui/panels/door-panel.tsx +0 -1299
- package/src/components/ui/panels/fence-panel.tsx +0 -222
- 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 -280
- 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 -161
- package/src/components/ui/panels/stair-panel.tsx +0 -577
- package/src/components/ui/panels/stair-segment-panel.tsx +0 -325
- package/src/components/ui/panels/wall-panel.tsx +0 -273
- package/src/components/ui/panels/window-panel.tsx +0 -968
- package/src/contexts/presets-context.tsx +0 -121
- package/src/hooks/use-contextual-tools.ts +0 -61
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import { resolvePlanarCursorPosition } from './planar-cursor-placement'
|
|
3
|
+
|
|
4
|
+
const snapHalf = (value: number) => Math.round(value / 0.5) * 0.5
|
|
5
|
+
|
|
6
|
+
describe('resolvePlanarCursorPosition', () => {
|
|
7
|
+
test('absolute mode places the point directly at the snapped cursor', () => {
|
|
8
|
+
const result = resolvePlanarCursorPosition({
|
|
9
|
+
cursor: [1.24, -2.26],
|
|
10
|
+
original: [10, 10],
|
|
11
|
+
anchor: null,
|
|
12
|
+
mode: 'absolute',
|
|
13
|
+
snap: snapHalf,
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
expect(result.point).toEqual([1, -2.5])
|
|
17
|
+
expect(result.anchor).toBeNull()
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
test('relative mode preserves the original grab offset from the first cursor sample', () => {
|
|
21
|
+
const start = resolvePlanarCursorPosition({
|
|
22
|
+
cursor: [4.1, 6.1],
|
|
23
|
+
original: [10, 20],
|
|
24
|
+
anchor: null,
|
|
25
|
+
mode: 'relative',
|
|
26
|
+
snap: snapHalf,
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
expect(start.point).toEqual([10, 20])
|
|
30
|
+
expect(start.anchor).toEqual([4.1, 6.1])
|
|
31
|
+
|
|
32
|
+
const moved = resolvePlanarCursorPosition({
|
|
33
|
+
cursor: [4.9, 5.2],
|
|
34
|
+
original: [10, 20],
|
|
35
|
+
anchor: start.anchor,
|
|
36
|
+
mode: 'relative',
|
|
37
|
+
snap: snapHalf,
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
expect(moved.point).toEqual([11, 19])
|
|
41
|
+
expect(moved.anchor).toEqual([4.1, 6.1])
|
|
42
|
+
})
|
|
43
|
+
})
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export type PlanarPoint = [number, number]
|
|
2
|
+
|
|
3
|
+
export type PlanarCursorPlacementMode = 'absolute' | 'relative'
|
|
4
|
+
|
|
5
|
+
type ResolvePlanarCursorPositionArgs = {
|
|
6
|
+
cursor: PlanarPoint
|
|
7
|
+
original: PlanarPoint
|
|
8
|
+
anchor: PlanarPoint | null
|
|
9
|
+
mode: PlanarCursorPlacementMode
|
|
10
|
+
snap?: (value: number) => number
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
type ResolvePlanarCursorPositionResult = {
|
|
14
|
+
point: PlanarPoint
|
|
15
|
+
anchor: PlanarPoint | null
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const identity = (value: number) => value
|
|
19
|
+
|
|
20
|
+
export function resolvePlanarCursorPosition({
|
|
21
|
+
cursor,
|
|
22
|
+
original,
|
|
23
|
+
anchor,
|
|
24
|
+
mode,
|
|
25
|
+
snap = identity,
|
|
26
|
+
}: ResolvePlanarCursorPositionArgs): ResolvePlanarCursorPositionResult {
|
|
27
|
+
if (mode === 'absolute') {
|
|
28
|
+
return {
|
|
29
|
+
point: [snap(cursor[0]), snap(cursor[1])],
|
|
30
|
+
anchor,
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const resolvedAnchor = anchor ?? cursor
|
|
35
|
+
return {
|
|
36
|
+
point: [
|
|
37
|
+
original[0] + snap(cursor[0] - resolvedAnchor[0]),
|
|
38
|
+
original[1] + snap(cursor[1] - resolvedAnchor[1]),
|
|
39
|
+
],
|
|
40
|
+
anchor: resolvedAnchor,
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -175,7 +175,7 @@ export function duplicateRoofSubtree(
|
|
|
175
175
|
|
|
176
176
|
export function clearRoofDuplicateMetadata(
|
|
177
177
|
roofId: AnyNodeId,
|
|
178
|
-
updates: Partial<Pick<RoofNode, 'position' | 'rotation' | 'metadata'>> = {},
|
|
178
|
+
updates: Partial<Pick<RoofNode, 'position' | 'rotation' | 'metadata' | 'visible'>> = {},
|
|
179
179
|
) {
|
|
180
180
|
const scene = useScene.getState()
|
|
181
181
|
const roofNode = scene.nodes[roofId]
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AnyNode,
|
|
3
|
+
type AnyNodeId,
|
|
4
|
+
generateId,
|
|
5
|
+
type LevelNode,
|
|
6
|
+
type StairNode,
|
|
7
|
+
useScene,
|
|
8
|
+
} from '@pascal-app/core'
|
|
9
|
+
import { useViewer } from '@pascal-app/viewer'
|
|
10
|
+
|
|
11
|
+
type ClipboardPayload = {
|
|
12
|
+
copiedAt: number
|
|
13
|
+
nodes: AnyNode[]
|
|
14
|
+
rootIds: AnyNodeId[]
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
type PasteResult = {
|
|
18
|
+
pastedIds: AnyNodeId[]
|
|
19
|
+
skippedIds: AnyNodeId[]
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const COPYABLE_ROOT_TYPES = new Set<AnyNode['type']>([
|
|
23
|
+
'wall',
|
|
24
|
+
'fence',
|
|
25
|
+
'column',
|
|
26
|
+
'item',
|
|
27
|
+
'slab',
|
|
28
|
+
'ceiling',
|
|
29
|
+
'roof',
|
|
30
|
+
'stair',
|
|
31
|
+
'spawn',
|
|
32
|
+
'zone',
|
|
33
|
+
])
|
|
34
|
+
|
|
35
|
+
let clipboardPayload: ClipboardPayload | null = null
|
|
36
|
+
const subscribers = new Set<() => void>()
|
|
37
|
+
|
|
38
|
+
function notifySubscribers() {
|
|
39
|
+
for (const subscriber of subscribers) {
|
|
40
|
+
subscriber()
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function subscribeEditorClipboard(subscriber: () => void) {
|
|
45
|
+
subscribers.add(subscriber)
|
|
46
|
+
return () => {
|
|
47
|
+
subscribers.delete(subscriber)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function getEditorClipboardSnapshot() {
|
|
52
|
+
return clipboardPayload
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function hasEditorClipboard() {
|
|
56
|
+
return !!clipboardPayload && clipboardPayload.rootIds.length > 0
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function extractIdPrefix(id: string) {
|
|
60
|
+
const underscoreIndex = id.indexOf('_')
|
|
61
|
+
return underscoreIndex === -1 ? 'node' : id.slice(0, underscoreIndex)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function collectSubtreeIds(
|
|
65
|
+
nodes: Record<AnyNodeId, AnyNode>,
|
|
66
|
+
rootId: AnyNodeId,
|
|
67
|
+
ids: Set<AnyNodeId>,
|
|
68
|
+
) {
|
|
69
|
+
if (ids.has(rootId)) return
|
|
70
|
+
const node = nodes[rootId]
|
|
71
|
+
if (!node) return
|
|
72
|
+
ids.add(rootId)
|
|
73
|
+
|
|
74
|
+
if ('children' in node && Array.isArray(node.children)) {
|
|
75
|
+
for (const childId of node.children as AnyNodeId[]) {
|
|
76
|
+
collectSubtreeIds(nodes, childId, ids)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function hasSelectedAncestor(
|
|
82
|
+
nodes: Record<AnyNodeId, AnyNode>,
|
|
83
|
+
id: AnyNodeId,
|
|
84
|
+
selectedIds: Set<AnyNodeId>,
|
|
85
|
+
) {
|
|
86
|
+
let parentId = nodes[id]?.parentId as AnyNodeId | null
|
|
87
|
+
|
|
88
|
+
while (parentId) {
|
|
89
|
+
if (selectedIds.has(parentId)) return true
|
|
90
|
+
parentId = nodes[parentId]?.parentId as AnyNodeId | null
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return false
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function isLevelChildRoot(nodes: Record<AnyNodeId, AnyNode>, node: AnyNode) {
|
|
97
|
+
const parentId = node.parentId as AnyNodeId | null
|
|
98
|
+
if (!parentId) return true
|
|
99
|
+
return nodes[parentId]?.type === 'level'
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function getPasteTargetLevel(targetLevelId?: AnyNodeId) {
|
|
103
|
+
const scene = useScene.getState()
|
|
104
|
+
const resolvedLevelId =
|
|
105
|
+
targetLevelId ?? (useViewer.getState().selection.levelId as AnyNodeId | null)
|
|
106
|
+
if (!resolvedLevelId) return null
|
|
107
|
+
|
|
108
|
+
const level = scene.nodes[resolvedLevelId]
|
|
109
|
+
return level?.type === 'level' ? level : null
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function getNextLevelId(level: LevelNode, nodes: Record<AnyNodeId, AnyNode>) {
|
|
113
|
+
const parentId = level.parentId as AnyNodeId | null
|
|
114
|
+
if (!parentId) return null
|
|
115
|
+
|
|
116
|
+
const building = nodes[parentId]
|
|
117
|
+
if (!building || building.type !== 'building') return null
|
|
118
|
+
|
|
119
|
+
const siblingLevels = building.children
|
|
120
|
+
.map((childId) => nodes[childId as AnyNodeId])
|
|
121
|
+
.filter((node): node is LevelNode => node?.type === 'level')
|
|
122
|
+
|
|
123
|
+
return (
|
|
124
|
+
siblingLevels
|
|
125
|
+
.filter((candidate) => candidate.level > level.level)
|
|
126
|
+
.sort((a, b) => a.level - b.level)[0]?.id ?? null
|
|
127
|
+
)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function remapNodeReferences(
|
|
131
|
+
node: AnyNode,
|
|
132
|
+
oldId: AnyNodeId,
|
|
133
|
+
targetLevel: LevelNode,
|
|
134
|
+
idMap: Map<AnyNodeId, AnyNodeId>,
|
|
135
|
+
rootIds: Set<AnyNodeId>,
|
|
136
|
+
nodes: Record<AnyNodeId, AnyNode>,
|
|
137
|
+
) {
|
|
138
|
+
const clone = JSON.parse(JSON.stringify(node)) as AnyNode
|
|
139
|
+
;(clone as Record<string, unknown>).id = idMap.get(oldId)
|
|
140
|
+
|
|
141
|
+
if (rootIds.has(oldId)) {
|
|
142
|
+
clone.parentId = targetLevel.id
|
|
143
|
+
} else if (clone.parentId && typeof clone.parentId === 'string') {
|
|
144
|
+
clone.parentId = idMap.get(clone.parentId as AnyNodeId) ?? clone.parentId
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if ('children' in clone && Array.isArray(clone.children)) {
|
|
148
|
+
;(clone as Record<string, unknown>).children = (clone.children as AnyNodeId[])
|
|
149
|
+
.map((childId) => idMap.get(childId))
|
|
150
|
+
.filter((childId): childId is AnyNodeId => !!childId)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if ('wallId' in clone && typeof clone.wallId === 'string') {
|
|
154
|
+
const nextWallId = idMap.get(clone.wallId as AnyNodeId)
|
|
155
|
+
if (nextWallId) {
|
|
156
|
+
;(clone as Record<string, unknown>).wallId = nextWallId
|
|
157
|
+
} else {
|
|
158
|
+
delete (clone as Record<string, unknown>).wallId
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (clone.type === 'stair') {
|
|
163
|
+
const nextLevelId = getNextLevelId(targetLevel, nodes)
|
|
164
|
+
;(clone as StairNode).fromLevelId = targetLevel.id
|
|
165
|
+
;(clone as StairNode).toLevelId = nextLevelId
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const metadata =
|
|
169
|
+
clone.metadata && typeof clone.metadata === 'object' && !Array.isArray(clone.metadata)
|
|
170
|
+
? { ...(clone.metadata as Record<string, unknown>) }
|
|
171
|
+
: {}
|
|
172
|
+
delete metadata.isNew
|
|
173
|
+
delete metadata.isTransient
|
|
174
|
+
;(clone as Record<string, unknown>).metadata = metadata
|
|
175
|
+
|
|
176
|
+
return AnyNode.parse(clone)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function copySelectedNodesToEditorClipboard(selectedIds?: AnyNodeId[]) {
|
|
180
|
+
const scene = useScene.getState()
|
|
181
|
+
const ids = selectedIds ?? (useViewer.getState().selection.selectedIds as AnyNodeId[])
|
|
182
|
+
const selectedIdSet = new Set(ids)
|
|
183
|
+
const rootIds = ids.filter((id) => {
|
|
184
|
+
const node = scene.nodes[id]
|
|
185
|
+
return (
|
|
186
|
+
node &&
|
|
187
|
+
COPYABLE_ROOT_TYPES.has(node.type) &&
|
|
188
|
+
isLevelChildRoot(scene.nodes, node) &&
|
|
189
|
+
!hasSelectedAncestor(scene.nodes, id, selectedIdSet)
|
|
190
|
+
)
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
if (rootIds.length === 0) {
|
|
194
|
+
return false
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const subtreeIds = new Set<AnyNodeId>()
|
|
198
|
+
for (const rootId of rootIds) {
|
|
199
|
+
collectSubtreeIds(scene.nodes, rootId, subtreeIds)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
clipboardPayload = {
|
|
203
|
+
copiedAt: Date.now(),
|
|
204
|
+
nodes: [...subtreeIds]
|
|
205
|
+
.map((id) => scene.nodes[id])
|
|
206
|
+
.filter((node): node is AnyNode => !!node)
|
|
207
|
+
.map((node) => JSON.parse(JSON.stringify(node)) as AnyNode),
|
|
208
|
+
rootIds,
|
|
209
|
+
}
|
|
210
|
+
notifySubscribers()
|
|
211
|
+
|
|
212
|
+
return true
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export function pasteEditorClipboardToLevel(targetLevelId?: AnyNodeId): PasteResult | null {
|
|
216
|
+
const payload = clipboardPayload
|
|
217
|
+
const targetLevel = getPasteTargetLevel(targetLevelId)
|
|
218
|
+
if (!payload || !targetLevel) return null
|
|
219
|
+
|
|
220
|
+
const scene = useScene.getState()
|
|
221
|
+
const idMap = new Map<AnyNodeId, AnyNodeId>()
|
|
222
|
+
|
|
223
|
+
for (const node of payload.nodes) {
|
|
224
|
+
idMap.set(node.id as AnyNodeId, generateId(extractIdPrefix(node.id)) as AnyNodeId)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const rootIdSet = new Set(payload.rootIds)
|
|
228
|
+
const pastedNodes: AnyNode[] = []
|
|
229
|
+
const skippedIds: AnyNodeId[] = []
|
|
230
|
+
|
|
231
|
+
for (const node of payload.nodes) {
|
|
232
|
+
try {
|
|
233
|
+
pastedNodes.push(
|
|
234
|
+
remapNodeReferences(node, node.id as AnyNodeId, targetLevel, idMap, rootIdSet, scene.nodes),
|
|
235
|
+
)
|
|
236
|
+
} catch (error) {
|
|
237
|
+
console.error('Failed to paste copied node', node.id, error)
|
|
238
|
+
skippedIds.push(node.id as AnyNodeId)
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (pastedNodes.length === 0) {
|
|
243
|
+
return { pastedIds: [], skippedIds }
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
scene.createNodes(
|
|
247
|
+
pastedNodes.map((node) => ({
|
|
248
|
+
node,
|
|
249
|
+
parentId: (node.parentId as AnyNodeId | null) ?? undefined,
|
|
250
|
+
})),
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
const pastedNodeIds = new Set(pastedNodes.map((node) => node.id as AnyNodeId))
|
|
254
|
+
const pastedRootIds = payload.rootIds
|
|
255
|
+
.map((rootId) => idMap.get(rootId))
|
|
256
|
+
.filter((id): id is AnyNodeId => !!id && pastedNodeIds.has(id))
|
|
257
|
+
|
|
258
|
+
useViewer.getState().setSelection({
|
|
259
|
+
levelId: targetLevel.id,
|
|
260
|
+
selectedIds: pastedRootIds,
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
return {
|
|
264
|
+
pastedIds: pastedRootIds,
|
|
265
|
+
skippedIds,
|
|
266
|
+
}
|
|
267
|
+
}
|
package/src/lib/scene.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import { resolveLevelId, sceneRegistry, useScene } from '@pascal-app/core'
|
|
3
|
+
import { nodeRegistry, resolveLevelId, sceneRegistry, useScene } from '@pascal-app/core'
|
|
4
4
|
import { useViewer } from '@pascal-app/viewer'
|
|
5
5
|
import useEditor, {
|
|
6
6
|
hasCustomPersistedEditorUiState,
|
|
@@ -220,7 +220,11 @@ function getValidatedSelectionForScene(
|
|
|
220
220
|
|
|
221
221
|
const selectedIds = selection.selectedIds.filter((id) => {
|
|
222
222
|
const node = sceneNodes[id]
|
|
223
|
-
|
|
223
|
+
if (!node) return false
|
|
224
|
+
if (resolveLevelId(node, sceneNodes) === levelId) return true
|
|
225
|
+
|
|
226
|
+
const def = nodeRegistry.get(node.type)
|
|
227
|
+
return def?.floorplanScope === 'building' && node.parentId === buildingId
|
|
224
228
|
})
|
|
225
229
|
|
|
226
230
|
return {
|
|
@@ -283,7 +287,11 @@ export function syncEditorSelectionFromCurrentScene() {
|
|
|
283
287
|
|
|
284
288
|
if (shouldRestoreEditorUiState) {
|
|
285
289
|
if (restoredSelection) {
|
|
286
|
-
|
|
290
|
+
// PersistedSelectionPath carries plain `string` ids (read from
|
|
291
|
+
// localStorage, no branded-template-literal guarantee). The viewer's
|
|
292
|
+
// SelectionPath expects branded ids. The runtime values match the
|
|
293
|
+
// brand; the cast bridges the static gap.
|
|
294
|
+
useViewer.getState().setSelection(restoredSelection as never)
|
|
287
295
|
useEditor.setState(
|
|
288
296
|
restoredEditorUiState.phase === 'site'
|
|
289
297
|
? (selectionDrivenEditorUiState ?? restoredEditorUiState)
|
|
@@ -305,7 +313,7 @@ export function syncEditorSelectionFromCurrentScene() {
|
|
|
305
313
|
}
|
|
306
314
|
|
|
307
315
|
if (restoredSelection) {
|
|
308
|
-
useViewer.getState().setSelection(restoredSelection)
|
|
316
|
+
useViewer.getState().setSelection(restoredSelection as never)
|
|
309
317
|
if (selectionDrivenEditorUiState) {
|
|
310
318
|
useEditor.setState(selectionDrivenEditorUiState)
|
|
311
319
|
}
|
|
@@ -351,6 +359,7 @@ function resetEditorInteractionState() {
|
|
|
351
359
|
selectedReferenceId: null,
|
|
352
360
|
spaces: {},
|
|
353
361
|
editingHole: null,
|
|
362
|
+
hoveredHole: null,
|
|
354
363
|
isPreviewMode: false,
|
|
355
364
|
})
|
|
356
365
|
}
|
package/src/lib/sfx-bus.ts
CHANGED
|
@@ -10,9 +10,12 @@ type SFXEvents = {
|
|
|
10
10
|
'sfx:item-pick': undefined
|
|
11
11
|
'sfx:item-place': undefined
|
|
12
12
|
'sfx:item-rotate': undefined
|
|
13
|
+
'sfx:structure-build-start': undefined
|
|
13
14
|
'sfx:structure-build': undefined
|
|
14
15
|
'sfx:structure-delete': undefined
|
|
15
16
|
'sfx:snapshot-capture': undefined
|
|
17
|
+
'sfx:menu-hover': undefined
|
|
18
|
+
'sfx:menu-click': undefined
|
|
16
19
|
}
|
|
17
20
|
|
|
18
21
|
/**
|
|
@@ -36,9 +39,12 @@ export function initSFXBus() {
|
|
|
36
39
|
sfxEmitter.on('sfx:item-pick', () => playSFX('itemPick'))
|
|
37
40
|
sfxEmitter.on('sfx:item-place', () => playSFX('itemPlace'))
|
|
38
41
|
sfxEmitter.on('sfx:item-rotate', () => playSFX('itemRotate'))
|
|
39
|
-
sfxEmitter.on('sfx:structure-build', () => playSFX('
|
|
42
|
+
sfxEmitter.on('sfx:structure-build-start', () => playSFX('structureBuildStart'))
|
|
43
|
+
sfxEmitter.on('sfx:structure-build', () => playSFX('structureBuildEnd'))
|
|
40
44
|
sfxEmitter.on('sfx:structure-delete', () => playSFX('structureDelete'))
|
|
41
45
|
sfxEmitter.on('sfx:snapshot-capture', () => playSFX('snapshotCapture'))
|
|
46
|
+
sfxEmitter.on('sfx:menu-hover', () => playSFX('menuHover'))
|
|
47
|
+
sfxEmitter.on('sfx:menu-click', () => playSFX('menuClick'))
|
|
42
48
|
}
|
|
43
49
|
|
|
44
50
|
/**
|
|
@@ -49,3 +55,19 @@ export function initSFXBus() {
|
|
|
49
55
|
export function triggerSFX(event: keyof SFXEvents) {
|
|
50
56
|
sfxEmitter.emit(event)
|
|
51
57
|
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Node types whose deletion should use the lighter item-delete cue rather
|
|
61
|
+
* than the heavier structure-delete one. Shelves are furniture-like placeable
|
|
62
|
+
* objects, so they sound like items being removed, not structures demolished.
|
|
63
|
+
*/
|
|
64
|
+
const ITEM_DELETE_NODE_TYPES = new Set(['item', 'shelf'])
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Emit the delete SFX appropriate for a deleted node's type.
|
|
68
|
+
*/
|
|
69
|
+
export function emitDeleteSFX(nodeType: string | undefined) {
|
|
70
|
+
sfxEmitter.emit(
|
|
71
|
+
nodeType && ITEM_DELETE_NODE_TYPES.has(nodeType) ? 'sfx:item-delete' : 'sfx:structure-delete',
|
|
72
|
+
)
|
|
73
|
+
}
|
package/src/lib/sfx-player.ts
CHANGED
|
@@ -5,7 +5,8 @@ import useAudio from '../store/use-audio'
|
|
|
5
5
|
// so a rate range of ~0.88–1.12 reads as a subtle ±2 semitones — enough to kill the
|
|
6
6
|
// machine-gun feeling when the same SFX fires in rapid succession.
|
|
7
7
|
type SFXConfig = {
|
|
8
|
-
|
|
8
|
+
// One file, or several pre-rendered variations cycled round-robin per play.
|
|
9
|
+
src: string | string[]
|
|
9
10
|
// Random playback-rate range applied per play (1 = unchanged).
|
|
10
11
|
rateRange?: [number, number]
|
|
11
12
|
// Random volume multiplier range applied per play (1 = unchanged).
|
|
@@ -24,26 +25,31 @@ const DEFAULT_MIN_INTERVAL_MS = 30
|
|
|
24
25
|
// SFX sound definitions
|
|
25
26
|
export const SFX: Record<string, SFXConfig> = {
|
|
26
27
|
gridSnap: {
|
|
27
|
-
src:
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
src: [
|
|
29
|
+
'/audios/sfx/grid_snap_0.mp3',
|
|
30
|
+
'/audios/sfx/grid_snap_1.mp3',
|
|
31
|
+
'/audios/sfx/grid_snap_2.mp3',
|
|
32
|
+
],
|
|
33
|
+
rateRange: [0.98, 1.02],
|
|
34
|
+
volumeRange: [0.5, 0.6],
|
|
30
35
|
panJitter: 0.15,
|
|
36
|
+
minIntervalMs: 50,
|
|
31
37
|
},
|
|
32
38
|
itemDelete: {
|
|
33
39
|
src: '/audios/sfx/item_delete.mp3',
|
|
34
40
|
rateRange: [0.9, 1.1],
|
|
35
41
|
volumeRange: [0.9, 1.0],
|
|
36
|
-
panJitter: 0.
|
|
42
|
+
panJitter: 0.05,
|
|
37
43
|
},
|
|
38
44
|
itemPick: {
|
|
39
45
|
src: '/audios/sfx/item_pick.mp3',
|
|
40
|
-
rateRange: [0.
|
|
46
|
+
rateRange: [0.95, 1.05],
|
|
41
47
|
volumeRange: [0.92, 1.0],
|
|
42
48
|
panJitter: 0.15,
|
|
43
49
|
},
|
|
44
50
|
itemPlace: {
|
|
45
51
|
src: '/audios/sfx/item_place.mp3',
|
|
46
|
-
rateRange: [0.98, 1.
|
|
52
|
+
rateRange: [0.98, 1.02],
|
|
47
53
|
volumeRange: [0.9, 1.0],
|
|
48
54
|
panJitter: 0.15,
|
|
49
55
|
},
|
|
@@ -53,8 +59,16 @@ export const SFX: Record<string, SFXConfig> = {
|
|
|
53
59
|
volumeRange: [0.92, 1.0],
|
|
54
60
|
panJitter: 0.15,
|
|
55
61
|
},
|
|
56
|
-
|
|
57
|
-
|
|
62
|
+
// Fired when a structure draft begins (first click of a wall/slab/etc).
|
|
63
|
+
structureBuildStart: {
|
|
64
|
+
src: '/audios/sfx/structure_build_start.mp3',
|
|
65
|
+
rateRange: [0.95, 1.05],
|
|
66
|
+
volumeRange: [0.88, 1.0],
|
|
67
|
+
panJitter: 0.15,
|
|
68
|
+
},
|
|
69
|
+
// Fired when a structure is committed (segment placed / polygon closed).
|
|
70
|
+
structureBuildEnd: {
|
|
71
|
+
src: '/audios/sfx/structure_build_end.mp3',
|
|
58
72
|
rateRange: [0.95, 1.05],
|
|
59
73
|
volumeRange: [0.88, 1.0],
|
|
60
74
|
panJitter: 0.15,
|
|
@@ -63,12 +77,29 @@ export const SFX: Record<string, SFXConfig> = {
|
|
|
63
77
|
src: '/audios/sfx/structure_delete.mp3',
|
|
64
78
|
rateRange: [0.9, 1.1],
|
|
65
79
|
volumeRange: [0.9, 1.0],
|
|
66
|
-
panJitter: 0.
|
|
80
|
+
panJitter: 0.08,
|
|
67
81
|
},
|
|
68
82
|
snapshotCapture: {
|
|
69
83
|
// Shutter should sound consistent — no variation.
|
|
70
84
|
src: '/audios/sfx/snapshot_capture.mp3',
|
|
71
85
|
},
|
|
86
|
+
// Soft tick when hovering a main category in the Build / Items panels.
|
|
87
|
+
// Kept quiet and rate-locked so sweeping across the grid reads as texture,
|
|
88
|
+
// not a melody.
|
|
89
|
+
menuHover: {
|
|
90
|
+
src: '/audios/sfx/menu_hover.mp3',
|
|
91
|
+
rateRange: [0.98, 1.02],
|
|
92
|
+
volumeRange: [0.2, 0.3],
|
|
93
|
+
panJitter: 0.1,
|
|
94
|
+
minIntervalMs: 0,
|
|
95
|
+
},
|
|
96
|
+
// Fired when a main category in the Build / Items panels is clicked.
|
|
97
|
+
menuClick: {
|
|
98
|
+
src: '/audios/sfx/menu_click.mp3',
|
|
99
|
+
rateRange: [0.98, 1.02],
|
|
100
|
+
volumeRange: [0.5, 0.6],
|
|
101
|
+
panJitter: 0.1,
|
|
102
|
+
},
|
|
72
103
|
} as const
|
|
73
104
|
|
|
74
105
|
export type SFXName = keyof typeof SFX
|
|
@@ -77,26 +108,32 @@ function randomInRange([min, max]: [number, number]): number {
|
|
|
77
108
|
return min + Math.random() * (max - min)
|
|
78
109
|
}
|
|
79
110
|
|
|
80
|
-
// Preload all SFX sounds
|
|
81
|
-
|
|
111
|
+
// Preload all SFX sounds. Each variation gets its own Howl so they can overlap
|
|
112
|
+
// and be cycled round-robin.
|
|
113
|
+
const sfxCache = new Map<SFXName, Howl[]>()
|
|
82
114
|
const lastPlayedAt = new Map<SFXName, number>()
|
|
115
|
+
const lastVariation = new Map<SFXName, number>()
|
|
83
116
|
|
|
84
117
|
// Initialize all sounds
|
|
85
118
|
Object.entries(SFX).forEach(([name, config]) => {
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
119
|
+
const sources = Array.isArray(config.src) ? config.src : [config.src]
|
|
120
|
+
const sounds = sources.map(
|
|
121
|
+
(src) =>
|
|
122
|
+
new Howl({
|
|
123
|
+
src: [src],
|
|
124
|
+
preload: true,
|
|
125
|
+
volume: 0.5, // Will be adjusted by the bus
|
|
126
|
+
}),
|
|
127
|
+
)
|
|
128
|
+
sfxCache.set(name as SFXName, sounds)
|
|
92
129
|
})
|
|
93
130
|
|
|
94
131
|
/**
|
|
95
132
|
* Play a sound effect with volume based on audio settings
|
|
96
133
|
*/
|
|
97
134
|
export function playSFX(name: SFXName) {
|
|
98
|
-
const
|
|
99
|
-
if (!
|
|
135
|
+
const sounds = sfxCache.get(name)
|
|
136
|
+
if (!sounds || sounds.length === 0) {
|
|
100
137
|
console.warn(`SFX not found: ${name}`)
|
|
101
138
|
return
|
|
102
139
|
}
|
|
@@ -110,6 +147,15 @@ export function playSFX(name: SFXName) {
|
|
|
110
147
|
if (last !== undefined && now - last < minInterval) return
|
|
111
148
|
lastPlayedAt.set(name, now)
|
|
112
149
|
|
|
150
|
+
// Pick a random variation, avoiding an immediate repeat of the last one so
|
|
151
|
+
// consecutive plays don't land on the same file.
|
|
152
|
+
let index = Math.floor(Math.random() * sounds.length)
|
|
153
|
+
if (sounds.length > 1 && index === lastVariation.get(name)) {
|
|
154
|
+
index = (index + 1) % sounds.length
|
|
155
|
+
}
|
|
156
|
+
lastVariation.set(name, index)
|
|
157
|
+
const sound = sounds[index]!
|
|
158
|
+
|
|
113
159
|
const { masterVolume, sfxVolume, muted } = useAudio.getState()
|
|
114
160
|
|
|
115
161
|
if (muted) return
|
|
@@ -137,7 +183,9 @@ export function updateSFXVolumes() {
|
|
|
137
183
|
const { masterVolume, sfxVolume } = useAudio.getState()
|
|
138
184
|
const finalVolume = (masterVolume / 100) * (sfxVolume / 100)
|
|
139
185
|
|
|
140
|
-
sfxCache.forEach((
|
|
141
|
-
|
|
186
|
+
sfxCache.forEach((sounds) => {
|
|
187
|
+
sounds.forEach((sound) => {
|
|
188
|
+
sound.volume(finalVolume)
|
|
189
|
+
})
|
|
142
190
|
})
|
|
143
191
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const SITE_BOUNDARY_DRAG_LABEL = 'site-boundary'
|