@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
|
@@ -1,280 +0,0 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
|
|
3
|
-
import { Howl } from 'howler'
|
|
4
|
-
import { Disc3, Settings2, SkipBack, SkipForward, Volume2, VolumeX } from 'lucide-react'
|
|
5
|
-
import { AnimatePresence, motion } from 'motion/react'
|
|
6
|
-
import { useCallback, useEffect, useRef, useState } from 'react'
|
|
7
|
-
import { Slider } from '../components/ui/slider'
|
|
8
|
-
import { cn } from '../lib/utils'
|
|
9
|
-
import useAudio from '../store/use-audio'
|
|
10
|
-
|
|
11
|
-
const PLAYLIST = [
|
|
12
|
-
{
|
|
13
|
-
title: 'Ballroom in Miniature',
|
|
14
|
-
file: '/audios/radios/classic/Ballroom in Miniature.mp3',
|
|
15
|
-
},
|
|
16
|
-
{
|
|
17
|
-
title: 'Blueprints in Springtime',
|
|
18
|
-
file: '/audios/radios/classic/Blueprints in Springtime.mp3',
|
|
19
|
-
},
|
|
20
|
-
{
|
|
21
|
-
title: 'Clockwork Tea Party',
|
|
22
|
-
file: '/audios/radios/classic/Clockwork Tea Party.mp3',
|
|
23
|
-
},
|
|
24
|
-
{
|
|
25
|
-
title: 'Clockwork Tea Party (Alternate)',
|
|
26
|
-
file: '/audios/radios/classic/Clockwork Tea Party (Alternate).mp3',
|
|
27
|
-
},
|
|
28
|
-
{
|
|
29
|
-
title: 'Clockwork Teacups',
|
|
30
|
-
file: '/audios/radios/classic/Clockwork Teacups.mp3',
|
|
31
|
-
},
|
|
32
|
-
{
|
|
33
|
-
title: 'Evening in the Parlor',
|
|
34
|
-
file: '/audios/radios/classic/Evening in the Parlor.mp3',
|
|
35
|
-
},
|
|
36
|
-
{
|
|
37
|
-
title: 'Glass Atrium',
|
|
38
|
-
file: '/audios/radios/classic/Glass Atrium.mp3',
|
|
39
|
-
},
|
|
40
|
-
{
|
|
41
|
-
title: 'Moonlight On The Drafting Table',
|
|
42
|
-
file: '/audios/radios/classic/Moonlight On The Drafting Table.mp3',
|
|
43
|
-
},
|
|
44
|
-
{
|
|
45
|
-
title: 'Sunlit Garden Reverie',
|
|
46
|
-
file: '/audios/radios/classic/Sunlit Garden Reverie.mp3',
|
|
47
|
-
},
|
|
48
|
-
{
|
|
49
|
-
title: 'Sunlit Waltz in Pastel Hues',
|
|
50
|
-
file: '/audios/radios/classic/Sunlit Waltz in Pastel Hues.mp3',
|
|
51
|
-
},
|
|
52
|
-
]
|
|
53
|
-
|
|
54
|
-
// Shuffle array helper
|
|
55
|
-
function shuffleArray<T>(array: T[]): T[] {
|
|
56
|
-
const shuffled = [...array]
|
|
57
|
-
for (let i = shuffled.length - 1; i > 0; i--) {
|
|
58
|
-
const j = Math.floor(Math.random() * (i + 1))
|
|
59
|
-
;[shuffled[i], shuffled[j]] = [shuffled[j]!, shuffled[i]!]
|
|
60
|
-
}
|
|
61
|
-
return shuffled
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export function PascalRadio() {
|
|
65
|
-
const [shuffledPlaylist] = useState(() => shuffleArray(PLAYLIST))
|
|
66
|
-
const [currentTrackIndex, setCurrentTrackIndex] = useState(0)
|
|
67
|
-
const { masterVolume, radioVolume, muted, isRadioPlaying, setRadioPlaying } = useAudio()
|
|
68
|
-
const soundRef = useRef<Howl | null>(null)
|
|
69
|
-
const [isOpen, setIsOpen] = useState(false)
|
|
70
|
-
const containerRef = useRef<HTMLDivElement>(null)
|
|
71
|
-
|
|
72
|
-
const currentTrack = shuffledPlaylist[currentTrackIndex]!
|
|
73
|
-
|
|
74
|
-
// Calculate effective volume (masterVolume * radioVolume, both are 0-100)
|
|
75
|
-
const effectiveVolume = (masterVolume / 100) * (radioVolume / 100)
|
|
76
|
-
|
|
77
|
-
// Keep a ref so the track-init effect can read current volume/muted/isRadioPlaying
|
|
78
|
-
// without those values being part of its dependency array (which would restart the song).
|
|
79
|
-
const effectiveVolumeRef = useRef(effectiveVolume)
|
|
80
|
-
const mutedRef = useRef(muted)
|
|
81
|
-
const isPlayingRef = useRef(isRadioPlaying)
|
|
82
|
-
effectiveVolumeRef.current = effectiveVolume
|
|
83
|
-
mutedRef.current = muted
|
|
84
|
-
isPlayingRef.current = isRadioPlaying
|
|
85
|
-
|
|
86
|
-
const handleNext = useCallback(() => {
|
|
87
|
-
setCurrentTrackIndex((prev) => (prev + 1) % shuffledPlaylist.length)
|
|
88
|
-
}, [shuffledPlaylist.length])
|
|
89
|
-
|
|
90
|
-
const handlePrevious = useCallback(() => {
|
|
91
|
-
setCurrentTrackIndex((prev) => (prev - 1 + shuffledPlaylist.length) % shuffledPlaylist.length)
|
|
92
|
-
}, [shuffledPlaylist.length])
|
|
93
|
-
|
|
94
|
-
// Initialize Howler only when the track changes — not on volume/mute/play-state changes.
|
|
95
|
-
// Volume and mute are handled by the separate effect below.
|
|
96
|
-
useEffect(() => {
|
|
97
|
-
if (soundRef.current) {
|
|
98
|
-
soundRef.current.unload()
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
const wasPlaying = isPlayingRef.current
|
|
102
|
-
|
|
103
|
-
soundRef.current = new Howl({
|
|
104
|
-
src: [currentTrack.file],
|
|
105
|
-
volume: mutedRef.current ? 0 : effectiveVolumeRef.current,
|
|
106
|
-
onend: handleNext,
|
|
107
|
-
})
|
|
108
|
-
|
|
109
|
-
if (wasPlaying && !mutedRef.current) {
|
|
110
|
-
soundRef.current?.play()
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
return () => {
|
|
114
|
-
soundRef.current?.unload()
|
|
115
|
-
}
|
|
116
|
-
}, [handleNext, currentTrack.file])
|
|
117
|
-
|
|
118
|
-
// Update volume when settings change
|
|
119
|
-
useEffect(() => {
|
|
120
|
-
if (soundRef.current) {
|
|
121
|
-
soundRef.current.volume(muted ? 0 : effectiveVolume)
|
|
122
|
-
|
|
123
|
-
// Pause if muted, resume if unmuted and was playing
|
|
124
|
-
if (muted && isRadioPlaying) {
|
|
125
|
-
soundRef.current.pause()
|
|
126
|
-
} else if (!muted && isRadioPlaying && !soundRef.current.playing()) {
|
|
127
|
-
soundRef.current.play()
|
|
128
|
-
} else if (!isRadioPlaying && soundRef.current.playing()) {
|
|
129
|
-
soundRef.current.pause()
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
}, [effectiveVolume, muted, isRadioPlaying])
|
|
133
|
-
|
|
134
|
-
const handlePlayPause = () => {
|
|
135
|
-
if (!soundRef.current || muted) return
|
|
136
|
-
|
|
137
|
-
if (isRadioPlaying) {
|
|
138
|
-
soundRef.current.pause()
|
|
139
|
-
} else {
|
|
140
|
-
soundRef.current.play()
|
|
141
|
-
}
|
|
142
|
-
setRadioPlaying(!isRadioPlaying)
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
const handleVolumeChange = (value: number[]) => {
|
|
146
|
-
useAudio.setState({ radioVolume: value[0] })
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// Handle click outside to close
|
|
150
|
-
useEffect(() => {
|
|
151
|
-
function handleClickOutside(event: MouseEvent) {
|
|
152
|
-
if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
|
|
153
|
-
setIsOpen(false)
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
if (isOpen) {
|
|
157
|
-
document.addEventListener('mousedown', handleClickOutside)
|
|
158
|
-
}
|
|
159
|
-
return () => {
|
|
160
|
-
document.removeEventListener('mousedown', handleClickOutside)
|
|
161
|
-
}
|
|
162
|
-
}, [isOpen])
|
|
163
|
-
|
|
164
|
-
return (
|
|
165
|
-
<motion.div
|
|
166
|
-
className={cn(
|
|
167
|
-
'flex flex-col overflow-hidden rounded-lg border border-border bg-background/95 shadow-lg backdrop-blur-md',
|
|
168
|
-
!isOpen && 'cursor-pointer transition-colors hover:bg-accent/90',
|
|
169
|
-
)}
|
|
170
|
-
layout
|
|
171
|
-
onClick={() => {
|
|
172
|
-
if (!isOpen) setIsOpen(true)
|
|
173
|
-
}}
|
|
174
|
-
ref={containerRef}
|
|
175
|
-
transition={{ type: 'spring', bounce: 0.2, duration: 0.6 }}
|
|
176
|
-
>
|
|
177
|
-
<div className="flex items-center justify-between gap-2 px-3 py-2 font-medium text-sm">
|
|
178
|
-
<div className="flex items-center gap-2">
|
|
179
|
-
<Disc3 className={cn('h-4 w-4 shrink-0', isRadioPlaying && 'animate-spin')} />
|
|
180
|
-
<span className="hidden whitespace-nowrap sm:inline">Radio Pascal</span>
|
|
181
|
-
</div>
|
|
182
|
-
<div className="flex items-center gap-2">
|
|
183
|
-
<div
|
|
184
|
-
aria-label={isRadioPlaying ? 'Pause' : 'Play'}
|
|
185
|
-
className="cursor-pointer rounded-sm bg-accent/30 p-1 transition-all hover:bg-accent hover:text-accent-foreground hover:shadow-sm"
|
|
186
|
-
onClick={(e) => {
|
|
187
|
-
e.stopPropagation()
|
|
188
|
-
handlePlayPause()
|
|
189
|
-
}}
|
|
190
|
-
onKeyDown={(e) => {
|
|
191
|
-
if (e.key === 'Enter' || e.key === ' ') {
|
|
192
|
-
e.preventDefault()
|
|
193
|
-
e.stopPropagation()
|
|
194
|
-
handlePlayPause()
|
|
195
|
-
}
|
|
196
|
-
}}
|
|
197
|
-
role="button"
|
|
198
|
-
tabIndex={0}
|
|
199
|
-
>
|
|
200
|
-
{isRadioPlaying ? (
|
|
201
|
-
<Volume2 className="h-3.5 w-3.5" />
|
|
202
|
-
) : (
|
|
203
|
-
<VolumeX className="h-3.5 w-3.5" />
|
|
204
|
-
)}
|
|
205
|
-
</div>
|
|
206
|
-
<button
|
|
207
|
-
aria-label="Radio Settings"
|
|
208
|
-
className={cn(
|
|
209
|
-
'cursor-pointer rounded-sm p-1 transition-all hover:bg-accent hover:text-accent-foreground',
|
|
210
|
-
isOpen && 'bg-accent text-accent-foreground',
|
|
211
|
-
)}
|
|
212
|
-
onClick={(e) => {
|
|
213
|
-
e.stopPropagation()
|
|
214
|
-
setIsOpen(!isOpen)
|
|
215
|
-
}}
|
|
216
|
-
>
|
|
217
|
-
<Settings2 className="h-3.5 w-3.5" />
|
|
218
|
-
</button>
|
|
219
|
-
</div>
|
|
220
|
-
</div>
|
|
221
|
-
|
|
222
|
-
<AnimatePresence>
|
|
223
|
-
{isOpen && (
|
|
224
|
-
<motion.div
|
|
225
|
-
animate={{ opacity: 1, height: 'auto' }}
|
|
226
|
-
exit={{ opacity: 0, height: 0 }}
|
|
227
|
-
initial={{ opacity: 0, height: 0 }}
|
|
228
|
-
transition={{ type: 'spring', bounce: 0.2, duration: 0.6 }}
|
|
229
|
-
>
|
|
230
|
-
<div className="w-[16rem] space-y-3 px-3 pb-3">
|
|
231
|
-
<div className="mb-3 h-px w-full bg-border/50" />
|
|
232
|
-
{/* Current song info with prev/next */}
|
|
233
|
-
<div>
|
|
234
|
-
<p className="mb-2 text-muted-foreground text-xs">Now Playing</p>
|
|
235
|
-
<div className="flex items-center justify-between gap-2">
|
|
236
|
-
<button
|
|
237
|
-
aria-label="Previous"
|
|
238
|
-
className="shrink-0 rounded-full p-1.5 transition-colors hover:bg-accent"
|
|
239
|
-
onClick={handlePrevious}
|
|
240
|
-
>
|
|
241
|
-
<SkipBack className="h-4 w-4" />
|
|
242
|
-
</button>
|
|
243
|
-
<p
|
|
244
|
-
className="flex-1 truncate text-center font-medium text-sm"
|
|
245
|
-
title={currentTrack.title}
|
|
246
|
-
>
|
|
247
|
-
{currentTrack.title}
|
|
248
|
-
</p>
|
|
249
|
-
<button
|
|
250
|
-
aria-label="Next"
|
|
251
|
-
className="shrink-0 rounded-full p-1.5 transition-colors hover:bg-accent"
|
|
252
|
-
onClick={handleNext}
|
|
253
|
-
>
|
|
254
|
-
<SkipForward className="h-4 w-4" />
|
|
255
|
-
</button>
|
|
256
|
-
</div>
|
|
257
|
-
</div>
|
|
258
|
-
|
|
259
|
-
{/* Volume control */}
|
|
260
|
-
<div className="flex items-center gap-2">
|
|
261
|
-
<Volume2 className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
|
|
262
|
-
<Slider
|
|
263
|
-
aria-label="Radio Volume"
|
|
264
|
-
className="flex-1"
|
|
265
|
-
max={100}
|
|
266
|
-
onValueChange={handleVolumeChange}
|
|
267
|
-
step={1}
|
|
268
|
-
value={[radioVolume]}
|
|
269
|
-
/>
|
|
270
|
-
<span className="w-8 shrink-0 text-right text-muted-foreground text-xs">
|
|
271
|
-
{radioVolume}%
|
|
272
|
-
</span>
|
|
273
|
-
</div>
|
|
274
|
-
</div>
|
|
275
|
-
</motion.div>
|
|
276
|
-
)}
|
|
277
|
-
</AnimatePresence>
|
|
278
|
-
</motion.div>
|
|
279
|
-
)
|
|
280
|
-
}
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
|
|
3
|
-
import { Eye } from 'lucide-react'
|
|
4
|
-
import useEditor from '../store/use-editor'
|
|
5
|
-
|
|
6
|
-
export function PreviewButton() {
|
|
7
|
-
return (
|
|
8
|
-
<button
|
|
9
|
-
className="flex cursor-pointer items-center gap-2 rounded-lg border border-border bg-background/95 px-3 py-2 font-medium text-sm shadow-lg backdrop-blur-md transition-colors hover:bg-accent/90"
|
|
10
|
-
onClick={() => useEditor.getState().setPreviewMode(true)}
|
|
11
|
-
>
|
|
12
|
-
<Eye className="h-4 w-4 shrink-0" />
|
|
13
|
-
<span className="hidden whitespace-nowrap sm:inline">Preview</span>
|
|
14
|
-
</button>
|
|
15
|
-
)
|
|
16
|
-
}
|
|
@@ -1,157 +0,0 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
type BuildingNode,
|
|
5
|
-
emitter,
|
|
6
|
-
type GridEvent,
|
|
7
|
-
sceneRegistry,
|
|
8
|
-
useScene,
|
|
9
|
-
} from '@pascal-app/core'
|
|
10
|
-
import { useViewer } from '@pascal-app/viewer'
|
|
11
|
-
import { useFrame } from '@react-three/fiber'
|
|
12
|
-
import { useCallback, useEffect, useRef, useState } from 'react'
|
|
13
|
-
import * as THREE from 'three'
|
|
14
|
-
import { markToolCancelConsumed } from '../../../hooks/use-keyboard'
|
|
15
|
-
import { sfxEmitter } from '../../../lib/sfx-bus'
|
|
16
|
-
import useEditor from '../../../store/use-editor'
|
|
17
|
-
import { CursorSphere } from '../shared/cursor-sphere'
|
|
18
|
-
|
|
19
|
-
export function MoveBuildingContent({ node }: { node: BuildingNode }) {
|
|
20
|
-
const previousGridPosRef = useRef<[number, number] | null>(null)
|
|
21
|
-
|
|
22
|
-
// Stable refs so the effect never needs node in its dependency array
|
|
23
|
-
const nodeIdRef = useRef(node.id)
|
|
24
|
-
const originalPositionRef = useRef<[number, number, number]>([...node.position] as [
|
|
25
|
-
number,
|
|
26
|
-
number,
|
|
27
|
-
number,
|
|
28
|
-
])
|
|
29
|
-
const originalRotationRef = useRef<number>(node.rotation[1] ?? 0)
|
|
30
|
-
const pendingRotationRef = useRef<number>(node.rotation[1] ?? 0)
|
|
31
|
-
|
|
32
|
-
const [cursorWorldPos, setCursorWorldPos] = useState<[number, number, number]>(() => {
|
|
33
|
-
const obj = sceneRegistry.nodes.get(node.id)
|
|
34
|
-
if (obj) {
|
|
35
|
-
const pos = new THREE.Vector3()
|
|
36
|
-
obj.getWorldPosition(pos)
|
|
37
|
-
return [pos.x, pos.y, pos.z]
|
|
38
|
-
}
|
|
39
|
-
return [node.position[0], node.position[1], node.position[2]]
|
|
40
|
-
})
|
|
41
|
-
|
|
42
|
-
const exitMoveMode = useCallback(() => {
|
|
43
|
-
useEditor.getState().setMovingNode(null)
|
|
44
|
-
}, [])
|
|
45
|
-
|
|
46
|
-
useEffect(() => {
|
|
47
|
-
const nodeId = nodeIdRef.current
|
|
48
|
-
const originalPosition = originalPositionRef.current
|
|
49
|
-
|
|
50
|
-
useScene.temporal.getState().pause()
|
|
51
|
-
|
|
52
|
-
let wasCommitted = false
|
|
53
|
-
|
|
54
|
-
const onKeyDown = (event: KeyboardEvent) => {
|
|
55
|
-
if (event.target instanceof HTMLInputElement || event.target instanceof HTMLTextAreaElement) {
|
|
56
|
-
return
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const ROTATION_STEP = Math.PI / 2
|
|
60
|
-
let rotationDelta = 0
|
|
61
|
-
if (event.key === 'r' || event.key === 'R') rotationDelta = ROTATION_STEP
|
|
62
|
-
else if (event.key === 't' || event.key === 'T') rotationDelta = -ROTATION_STEP
|
|
63
|
-
|
|
64
|
-
if (rotationDelta !== 0) {
|
|
65
|
-
event.preventDefault()
|
|
66
|
-
sfxEmitter.emit('sfx:item-rotate')
|
|
67
|
-
pendingRotationRef.current += rotationDelta
|
|
68
|
-
|
|
69
|
-
const mesh = sceneRegistry.nodes.get(nodeId)
|
|
70
|
-
if (mesh) mesh.rotation.y = pendingRotationRef.current
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
const onGridMove = (event: GridEvent) => {
|
|
75
|
-
const gridX = Math.round(event.position[0] * 2) / 2
|
|
76
|
-
const gridZ = Math.round(event.position[2] * 2) / 2
|
|
77
|
-
|
|
78
|
-
if (
|
|
79
|
-
previousGridPosRef.current &&
|
|
80
|
-
(gridX !== previousGridPosRef.current[0] || gridZ !== previousGridPosRef.current[1])
|
|
81
|
-
) {
|
|
82
|
-
sfxEmitter.emit('sfx:grid-snap')
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
previousGridPosRef.current = [gridX, gridZ]
|
|
86
|
-
setCursorWorldPos([gridX, 0, gridZ])
|
|
87
|
-
|
|
88
|
-
// Directly update the Three.js group — no store update during drag
|
|
89
|
-
const mesh = sceneRegistry.nodes.get(nodeId)
|
|
90
|
-
if (mesh) {
|
|
91
|
-
mesh.position.x = gridX
|
|
92
|
-
mesh.position.z = gridZ
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
const onGridClick = (event: GridEvent) => {
|
|
97
|
-
const gridX = Math.round(event.position[0] * 2) / 2
|
|
98
|
-
const gridZ = Math.round(event.position[2] * 2) / 2
|
|
99
|
-
|
|
100
|
-
wasCommitted = true
|
|
101
|
-
|
|
102
|
-
useScene.temporal.getState().resume()
|
|
103
|
-
useScene.getState().updateNode(nodeId, {
|
|
104
|
-
position: [gridX, originalPosition[1], gridZ],
|
|
105
|
-
rotation: [0, pendingRotationRef.current, 0],
|
|
106
|
-
})
|
|
107
|
-
useScene.temporal.getState().pause()
|
|
108
|
-
|
|
109
|
-
sfxEmitter.emit('sfx:item-place')
|
|
110
|
-
useViewer.getState().setSelection({ buildingId: nodeId as BuildingNode['id'] })
|
|
111
|
-
exitMoveMode()
|
|
112
|
-
event.nativeEvent?.stopPropagation?.()
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
const onCancel = () => {
|
|
116
|
-
// Revert mesh position and rotation immediately
|
|
117
|
-
const mesh = sceneRegistry.nodes.get(nodeId)
|
|
118
|
-
if (mesh) {
|
|
119
|
-
mesh.position.x = originalPosition[0]
|
|
120
|
-
mesh.position.z = originalPosition[2]
|
|
121
|
-
mesh.rotation.y = originalRotationRef.current
|
|
122
|
-
}
|
|
123
|
-
pendingRotationRef.current = originalRotationRef.current
|
|
124
|
-
// Restore building selection
|
|
125
|
-
useViewer.getState().setSelection({ buildingId: nodeId as BuildingNode['id'] })
|
|
126
|
-
useScene.temporal.getState().resume()
|
|
127
|
-
// Tell the keyboard handler we handled this, so it doesn't also clear the selection
|
|
128
|
-
markToolCancelConsumed()
|
|
129
|
-
exitMoveMode()
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
emitter.on('grid:move', onGridMove)
|
|
133
|
-
emitter.on('grid:click', onGridClick)
|
|
134
|
-
emitter.on('tool:cancel', onCancel)
|
|
135
|
-
window.addEventListener('keydown', onKeyDown)
|
|
136
|
-
|
|
137
|
-
return () => {
|
|
138
|
-
if (!wasCommitted) {
|
|
139
|
-
useScene.getState().updateNode(nodeId, {
|
|
140
|
-
position: originalPosition,
|
|
141
|
-
rotation: [0, originalRotationRef.current, 0],
|
|
142
|
-
})
|
|
143
|
-
}
|
|
144
|
-
useScene.temporal.getState().resume()
|
|
145
|
-
emitter.off('grid:move', onGridMove)
|
|
146
|
-
emitter.off('grid:click', onGridClick)
|
|
147
|
-
emitter.off('tool:cancel', onCancel)
|
|
148
|
-
window.removeEventListener('keydown', onKeyDown)
|
|
149
|
-
}
|
|
150
|
-
}, [exitMoveMode]) // stable — node values captured via refs at mount
|
|
151
|
-
|
|
152
|
-
return (
|
|
153
|
-
<group>
|
|
154
|
-
<CursorSphere position={cursorWorldPos} showTooltip={false} />
|
|
155
|
-
</group>
|
|
156
|
-
)
|
|
157
|
-
}
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import { type CeilingNode, resolveLevelId, useScene } from '@pascal-app/core'
|
|
2
|
-
import { useViewer } from '@pascal-app/viewer'
|
|
3
|
-
import { useCallback } from 'react'
|
|
4
|
-
import { PolygonEditor } from '../shared/polygon-editor'
|
|
5
|
-
|
|
6
|
-
interface CeilingBoundaryEditorProps {
|
|
7
|
-
ceilingId: CeilingNode['id']
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Ceiling boundary editor - allows editing ceiling polygon vertices for a specific ceiling
|
|
12
|
-
* Uses the generic PolygonEditor component
|
|
13
|
-
*/
|
|
14
|
-
export const CeilingBoundaryEditor: React.FC<CeilingBoundaryEditorProps> = ({ ceilingId }) => {
|
|
15
|
-
const ceilingNode = useScene((state) => state.nodes[ceilingId])
|
|
16
|
-
const updateNode = useScene((state) => state.updateNode)
|
|
17
|
-
const setSelection = useViewer((state) => state.setSelection)
|
|
18
|
-
|
|
19
|
-
const ceiling = ceilingNode?.type === 'ceiling' ? (ceilingNode as CeilingNode) : null
|
|
20
|
-
|
|
21
|
-
const handlePolygonChange = useCallback(
|
|
22
|
-
(newPolygon: Array<[number, number]>) => {
|
|
23
|
-
updateNode(ceilingId, { polygon: newPolygon })
|
|
24
|
-
// Re-assert selection so the ceiling stays selected after the edit
|
|
25
|
-
setSelection({ selectedIds: [ceilingId] })
|
|
26
|
-
},
|
|
27
|
-
[ceilingId, updateNode, setSelection],
|
|
28
|
-
)
|
|
29
|
-
|
|
30
|
-
if (!ceiling?.polygon || ceiling.polygon.length < 3) return null
|
|
31
|
-
|
|
32
|
-
return (
|
|
33
|
-
<PolygonEditor
|
|
34
|
-
allowEdgeMove
|
|
35
|
-
color="#d4d4d4"
|
|
36
|
-
levelId={resolveLevelId(ceiling, useScene.getState().nodes)}
|
|
37
|
-
minVertices={3}
|
|
38
|
-
onPolygonChange={handlePolygonChange}
|
|
39
|
-
polygon={ceiling.polygon}
|
|
40
|
-
surfaceHeight={ceiling.height ?? 2.5}
|
|
41
|
-
/>
|
|
42
|
-
)
|
|
43
|
-
}
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import { type CeilingNode, resolveLevelId, useScene } from '@pascal-app/core'
|
|
2
|
-
import { useViewer } from '@pascal-app/viewer'
|
|
3
|
-
import { useCallback } from 'react'
|
|
4
|
-
import { PolygonEditor } from '../shared/polygon-editor'
|
|
5
|
-
|
|
6
|
-
interface CeilingHoleEditorProps {
|
|
7
|
-
ceilingId: CeilingNode['id']
|
|
8
|
-
holeIndex: number
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Ceiling hole editor - allows editing a specific hole polygon within a ceiling
|
|
13
|
-
* Uses the generic PolygonEditor component
|
|
14
|
-
*/
|
|
15
|
-
export const CeilingHoleEditor: React.FC<CeilingHoleEditorProps> = ({ ceilingId, holeIndex }) => {
|
|
16
|
-
const ceilingNode = useScene((state) => state.nodes[ceilingId])
|
|
17
|
-
const updateNode = useScene((state) => state.updateNode)
|
|
18
|
-
const setSelection = useViewer((state) => state.setSelection)
|
|
19
|
-
|
|
20
|
-
const ceiling = ceilingNode?.type === 'ceiling' ? (ceilingNode as CeilingNode) : null
|
|
21
|
-
const holes = ceiling?.holes || []
|
|
22
|
-
const hole = holes[holeIndex]
|
|
23
|
-
|
|
24
|
-
const handlePolygonChange = useCallback(
|
|
25
|
-
(newPolygon: Array<[number, number]>) => {
|
|
26
|
-
const updatedHoles = [...holes]
|
|
27
|
-
updatedHoles[holeIndex] = newPolygon
|
|
28
|
-
updateNode(ceilingId, { holes: updatedHoles })
|
|
29
|
-
// Re-assert selection so the ceiling stays selected after the edit
|
|
30
|
-
setSelection({ selectedIds: [ceilingId] })
|
|
31
|
-
},
|
|
32
|
-
[ceilingId, holeIndex, holes, updateNode, setSelection],
|
|
33
|
-
)
|
|
34
|
-
|
|
35
|
-
if (!(ceiling && hole) || hole.length < 3) return null
|
|
36
|
-
|
|
37
|
-
return (
|
|
38
|
-
<PolygonEditor
|
|
39
|
-
allowEdgeMove
|
|
40
|
-
allowPolygonMove
|
|
41
|
-
color="#ef4444"
|
|
42
|
-
levelId={resolveLevelId(ceiling, useScene.getState().nodes)} // red for holes
|
|
43
|
-
minVertices={3}
|
|
44
|
-
onPolygonChange={handlePolygonChange}
|
|
45
|
-
polygon={hole}
|
|
46
|
-
surfaceHeight={ceiling.height ?? 2.5}
|
|
47
|
-
/>
|
|
48
|
-
)
|
|
49
|
-
}
|