@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.
Files changed (244) hide show
  1. package/package.json +12 -13
  2. package/src/components/editor/alignment-3d-guide-layer.tsx +160 -0
  3. package/src/components/editor/custom-camera-controls.tsx +755 -25
  4. package/src/components/editor/editor-layout-mobile.tsx +3 -3
  5. package/src/components/editor/editor-layout-v2.tsx +62 -54
  6. package/src/components/editor/export-manager.tsx +32 -3
  7. package/src/components/editor/first-person/build-collider-world.test.ts +149 -0
  8. package/src/components/editor/first-person/build-collider-world.ts +203 -87
  9. package/src/components/editor/first-person/bvh-ecctrl.tsx +127 -62
  10. package/src/components/editor/first-person-controls.tsx +836 -31
  11. package/src/components/editor/floating-action-menu.tsx +287 -196
  12. package/src/components/editor/floating-building-action-menu.tsx +1 -1
  13. package/src/components/editor/floorplan-panel.tsx +5837 -12006
  14. package/src/components/editor/grid.tsx +51 -17
  15. package/src/components/editor/group-move-handle.tsx +316 -0
  16. package/src/components/editor/group-rotate-handle.tsx +382 -0
  17. package/src/components/editor/group-transform-shared.test.ts +205 -0
  18. package/src/components/editor/group-transform-shared.ts +256 -0
  19. package/src/components/editor/handles/handle-arrow.tsx +523 -0
  20. package/src/components/editor/handles/use-handle-drag.ts +213 -0
  21. package/src/components/editor/index.tsx +234 -190
  22. package/src/components/editor/measurement-pill.tsx +75 -0
  23. package/src/components/editor/node-arrow-handles.tsx +1453 -0
  24. package/src/components/editor/selection-manager.tsx +435 -158
  25. package/src/components/editor/site-edge-labels.tsx +44 -20
  26. package/src/components/editor/slab-hole-highlights.tsx +466 -0
  27. package/src/components/editor/snapshot-capture-overlay.tsx +539 -0
  28. package/src/components/editor/thumbnail-generator.tsx +27 -64
  29. package/src/components/editor/use-floorplan-background-placement.ts +122 -35
  30. package/src/components/editor/use-floorplan-hit-testing.ts +24 -0
  31. package/src/components/editor/use-floorplan-scene-data.ts +14 -2
  32. package/src/components/editor/wall-measurement-label.tsx +11 -6
  33. package/src/components/editor/wall-move-side-handles.tsx +855 -0
  34. package/src/components/editor/wall-opening-highlights.tsx +181 -0
  35. package/src/components/editor/wall-snap-beacon-layer.tsx +150 -0
  36. package/src/components/editor-2d/floorplan-action-menu-layer.tsx +6 -1
  37. package/src/components/editor-2d/floorplan-alignment-guide-layer.tsx +142 -0
  38. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +5 -3
  39. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +227 -0
  40. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +616 -0
  41. package/src/components/editor-2d/floorplan-render-context.tsx +62 -0
  42. package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +75 -0
  43. package/src/components/editor-2d/floorplan-wall-move-ghost-layer.tsx +47 -0
  44. package/src/components/editor-2d/renderers/floorplan-draft-layer.tsx +6 -1
  45. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +266 -0
  46. package/src/components/editor-2d/renderers/floorplan-measurements-layer.tsx +108 -61
  47. package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +59 -0
  48. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +1963 -0
  49. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +29 -14
  50. package/src/components/editor-2d/svg-paths.ts +10 -2
  51. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +30 -9
  52. package/src/components/systems/ceiling/ceiling-system.tsx +12 -8
  53. package/src/components/systems/roof/roof-edit-system.tsx +86 -12
  54. package/src/components/systems/stair/stair-edit-system.tsx +2 -2
  55. package/src/components/systems/zone/zone-label-editor-system.tsx +0 -0
  56. package/src/components/systems/zone/zone-system.tsx +0 -0
  57. package/src/components/tools/elevator/elevator-defaults.ts +8 -0
  58. package/src/components/tools/elevator/elevator-tool.tsx +349 -0
  59. package/src/components/tools/elevator/move-elevator-tool.tsx +252 -0
  60. package/src/components/tools/fence/fence-drafting.ts +64 -22
  61. package/src/components/tools/item/move-tool.tsx +40 -107
  62. package/src/components/tools/item/placement-math.test.ts +10 -0
  63. package/src/components/tools/item/placement-math.ts +33 -9
  64. package/src/components/tools/item/placement-strategies.ts +201 -25
  65. package/src/components/tools/item/placement-types.ts +10 -1
  66. package/src/components/tools/item/use-draft-node.ts +20 -2
  67. package/src/components/tools/item/use-placement-coordinator.tsx +700 -299
  68. package/src/components/tools/registry/move-registry-node-tool.tsx +602 -0
  69. package/src/components/tools/roof/roof-tool.tsx +108 -12
  70. package/src/components/tools/select/box-select-state.ts +77 -0
  71. package/src/components/tools/select/box-select-tool.tsx +324 -543
  72. package/src/components/tools/select/plane-box-select-tool.tsx +577 -0
  73. package/src/components/tools/select/screen-rectangle-selection.ts +84 -0
  74. package/src/components/tools/select/select-candidates.test.ts +109 -0
  75. package/src/components/tools/select/select-candidates.ts +85 -0
  76. package/src/components/tools/shared/affordance-dispatch.ts +30 -0
  77. package/src/components/tools/shared/drag-bounding-box.tsx +171 -0
  78. package/src/components/tools/shared/floor-stack-preview.ts +25 -0
  79. package/src/components/tools/shared/fresh-placement-visibility.ts +61 -0
  80. package/src/components/tools/shared/placement-box-geometry.ts +123 -0
  81. package/src/components/tools/shared/placement-box.tsx +123 -0
  82. package/src/components/tools/shared/polygon-editor.tsx +617 -167
  83. package/src/components/tools/shared/segment-angle.ts +57 -1
  84. package/src/components/tools/site/site-boundary-editor.tsx +464 -12
  85. package/src/components/tools/site/site-flag-model.ts +18 -0
  86. package/src/components/tools/stair/stair-defaults.ts +1 -1
  87. package/src/components/tools/stair/stair-tool.tsx +269 -35
  88. package/src/components/tools/tool-manager.tsx +172 -56
  89. package/src/components/tools/wall/wall-drafting.ts +98 -98
  90. package/src/components/tools/wall/wall-snap-geometry.test.ts +79 -0
  91. package/src/components/tools/wall/wall-snap-geometry.ts +270 -0
  92. package/src/components/tools/zone/zone-tool.tsx +36 -11
  93. package/src/components/ui/action-menu/action-button.tsx +21 -1
  94. package/src/components/ui/action-menu/camera-actions.tsx +0 -0
  95. package/src/components/ui/action-menu/control-modes.tsx +5 -76
  96. package/src/components/ui/action-menu/furnish-tools.tsx +6 -92
  97. package/src/components/ui/action-menu/index.tsx +33 -196
  98. package/src/components/ui/action-menu/structure-tools.tsx +13 -88
  99. package/src/components/ui/action-menu/view-toggles.tsx +262 -37
  100. package/src/components/ui/command-palette/editor-commands.tsx +24 -17
  101. package/src/components/ui/command-palette/index.tsx +6 -256
  102. package/src/components/ui/controls/material-paint-panel.tsx +83 -0
  103. package/src/components/ui/controls/material-picker.tsx +17 -35
  104. package/src/components/ui/controls/metric-control.tsx +33 -11
  105. package/src/components/ui/floating-level-selector.tsx +48 -11
  106. package/src/components/ui/helpers/helper-manager.tsx +21 -17
  107. package/src/components/ui/helpers/registered-tool-helper.tsx +25 -0
  108. package/src/components/ui/item-catalog/catalog-items.tsx +1743 -315
  109. package/src/components/ui/item-catalog/item-catalog.tsx +91 -46
  110. package/src/components/ui/level-duplicate-dialog.tsx +5 -6
  111. package/src/components/ui/panels/node-display.ts +1 -0
  112. package/src/components/ui/panels/paint-panel.tsx +66 -41
  113. package/src/components/ui/panels/panel-manager.tsx +33 -74
  114. package/src/components/ui/panels/panel-wrapper.tsx +217 -12
  115. package/src/components/ui/panels/parametric-inspector.tsx +469 -0
  116. package/src/components/ui/panels/reference-panel.tsx +28 -13
  117. package/src/components/ui/primitives/dropdown-menu.tsx +12 -8
  118. package/src/components/ui/primitives/number-input.tsx +1 -1
  119. package/src/components/ui/primitives/sidebar.tsx +0 -0
  120. package/src/components/ui/scene-loader.tsx +1 -3
  121. package/src/components/ui/sidebar/app-sidebar.tsx +0 -0
  122. package/src/components/ui/sidebar/icon-rail.tsx +0 -0
  123. package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +263 -0
  124. package/src/components/ui/sidebar/panels/items-panel/index.tsx +404 -0
  125. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +59 -9
  126. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +6 -1
  127. package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +259 -0
  128. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +3 -1
  129. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +5 -2
  130. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +83 -0
  131. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +89 -0
  132. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +75 -0
  133. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +2 -2
  134. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +83 -0
  135. package/src/components/ui/sidebar/panels/site-panel/index.tsx +8 -12
  136. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +2 -1
  137. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +93 -0
  138. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +41 -5
  139. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +128 -0
  140. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +5 -2
  141. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +83 -0
  142. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -1
  143. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +68 -34
  144. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +3 -1
  145. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +52 -3
  146. package/src/components/ui/sidebar/tab-bar.tsx +61 -1
  147. package/src/components/ui/slider.tsx +1 -1
  148. package/src/components/viewer-overlay.tsx +169 -52
  149. package/src/components/viewer-zone-system.tsx +1 -1
  150. package/src/hooks/use-auto-save.ts +24 -3
  151. package/src/hooks/use-ceiling-events.ts +175 -0
  152. package/src/hooks/use-drag-action.ts +124 -0
  153. package/src/hooks/use-keyboard.ts +125 -62
  154. package/src/hooks/use-selection.ts +64 -0
  155. package/src/index.tsx +243 -4
  156. package/src/lib/constants.ts +6 -2
  157. package/src/lib/editor-api.ts +62 -0
  158. package/src/lib/elevator-support.ts +96 -0
  159. package/src/lib/floorplan/apply-alignment.ts +111 -0
  160. package/src/lib/floorplan/index.ts +7 -0
  161. package/src/lib/floorplan/selection-tool.ts +40 -0
  162. package/src/lib/floorplan/stairs.ts +12 -1
  163. package/src/lib/fresh-planar-placement.test.ts +102 -0
  164. package/src/lib/fresh-planar-placement.ts +63 -0
  165. package/src/lib/history.ts +2 -1
  166. package/src/lib/level-duplication.test.ts +0 -2
  167. package/src/lib/level-duplication.ts +55 -43
  168. package/src/lib/level-name.ts +11 -0
  169. package/src/lib/material-paint.ts +212 -20
  170. package/src/lib/placement-metadata.ts +29 -0
  171. package/src/lib/planar-cursor-placement.test.ts +43 -0
  172. package/src/lib/planar-cursor-placement.ts +42 -0
  173. package/src/lib/roof-duplication.ts +2 -2
  174. package/src/lib/scene-bounds.ts +1 -1
  175. package/src/lib/scene-clipboard.ts +267 -0
  176. package/src/lib/scene.ts +13 -4
  177. package/src/lib/sfx-bus.ts +25 -1
  178. package/src/lib/sfx-player.ts +75 -27
  179. package/src/lib/site-boundary.ts +1 -0
  180. package/src/lib/stair-duplication.ts +2 -2
  181. package/src/lib/stair-levels.test.ts +146 -0
  182. package/src/lib/stair-levels.ts +177 -0
  183. package/src/lib/world-grid-snap.ts +199 -0
  184. package/src/lib/zone-content.ts +130 -0
  185. package/src/store/use-alignment-guides.ts +21 -0
  186. package/src/store/use-editor.tsx +342 -91
  187. package/src/store/use-placement-preview.ts +31 -0
  188. package/src/store/use-wall-move-ghosts.ts +36 -0
  189. package/src/store/use-wall-snap-indicator.ts +32 -0
  190. package/tsconfig.json +2 -1
  191. package/src/components/editor/preset-thumbnail-generator.tsx +0 -125
  192. package/src/components/editor-2d/renderers/floorplan-roof-layer.tsx +0 -113
  193. package/src/components/feedback-dialog.tsx +0 -265
  194. package/src/components/pascal-radio.tsx +0 -280
  195. package/src/components/preview-button.tsx +0 -16
  196. package/src/components/tools/building/move-building-tool.tsx +0 -157
  197. package/src/components/tools/ceiling/ceiling-boundary-editor.tsx +0 -43
  198. package/src/components/tools/ceiling/ceiling-hole-editor.tsx +0 -49
  199. package/src/components/tools/ceiling/ceiling-tool.tsx +0 -465
  200. package/src/components/tools/ceiling/move-ceiling-tool.tsx +0 -257
  201. package/src/components/tools/column/column-tool.tsx +0 -97
  202. package/src/components/tools/column/move-column-tool.tsx +0 -105
  203. package/src/components/tools/door/door-math.ts +0 -110
  204. package/src/components/tools/door/door-tool.tsx +0 -324
  205. package/src/components/tools/door/move-door-tool.tsx +0 -412
  206. package/src/components/tools/fence/curve-fence-tool.tsx +0 -179
  207. package/src/components/tools/fence/fence-tool.tsx +0 -346
  208. package/src/components/tools/fence/move-fence-endpoint-tool.tsx +0 -438
  209. package/src/components/tools/fence/move-fence-tool.tsx +0 -298
  210. package/src/components/tools/item/item-tool.tsx +0 -26
  211. package/src/components/tools/roof/move-roof-tool.tsx +0 -359
  212. package/src/components/tools/slab/move-slab-tool.tsx +0 -182
  213. package/src/components/tools/slab/slab-boundary-editor.tsx +0 -43
  214. package/src/components/tools/slab/slab-hole-editor.tsx +0 -49
  215. package/src/components/tools/slab/slab-tool.tsx +0 -322
  216. package/src/components/tools/spawn/move-spawn-tool.tsx +0 -101
  217. package/src/components/tools/spawn/spawn-tool.tsx +0 -130
  218. package/src/components/tools/wall/curve-wall-tool.tsx +0 -176
  219. package/src/components/tools/wall/move-wall-endpoint-tool.tsx +0 -423
  220. package/src/components/tools/wall/move-wall-tool.tsx +0 -356
  221. package/src/components/tools/wall/wall-tool.tsx +0 -332
  222. package/src/components/tools/window/move-window-tool.tsx +0 -447
  223. package/src/components/tools/window/window-math.ts +0 -117
  224. package/src/components/tools/window/window-tool.tsx +0 -332
  225. package/src/components/ui/helpers/ceiling-helper.tsx +0 -20
  226. package/src/components/ui/helpers/slab-helper.tsx +0 -20
  227. package/src/components/ui/helpers/wall-helper.tsx +0 -20
  228. package/src/components/ui/panels/ceiling-panel.tsx +0 -250
  229. package/src/components/ui/panels/column-panel.tsx +0 -715
  230. package/src/components/ui/panels/door-panel.tsx +0 -1292
  231. package/src/components/ui/panels/fence-panel.tsx +0 -227
  232. package/src/components/ui/panels/item-panel.tsx +0 -306
  233. package/src/components/ui/panels/presets/presets-popover.tsx +0 -511
  234. package/src/components/ui/panels/roof-panel.tsx +0 -230
  235. package/src/components/ui/panels/roof-segment-panel.tsx +0 -311
  236. package/src/components/ui/panels/slab-panel.tsx +0 -250
  237. package/src/components/ui/panels/spawn-panel.tsx +0 -155
  238. package/src/components/ui/panels/stair-panel.tsx +0 -525
  239. package/src/components/ui/panels/stair-segment-panel.tsx +0 -325
  240. package/src/components/ui/panels/wall-panel.tsx +0 -177
  241. package/src/components/ui/panels/window-panel.tsx +0 -960
  242. package/src/components/ui/viewer-toolbar.tsx +0 -436
  243. package/src/contexts/presets-context.tsx +0 -121
  244. 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
- }