@pascal-app/editor 0.4.0

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 (165) hide show
  1. package/package.json +62 -0
  2. package/src/components/editor/custom-camera-controls.tsx +387 -0
  3. package/src/components/editor/editor-layout-v2.tsx +220 -0
  4. package/src/components/editor/export-manager.tsx +78 -0
  5. package/src/components/editor/first-person-controls.tsx +249 -0
  6. package/src/components/editor/floating-action-menu.tsx +231 -0
  7. package/src/components/editor/floorplan-panel.tsx +9609 -0
  8. package/src/components/editor/grid.tsx +161 -0
  9. package/src/components/editor/index.tsx +928 -0
  10. package/src/components/editor/node-action-menu.tsx +66 -0
  11. package/src/components/editor/preset-thumbnail-generator.tsx +125 -0
  12. package/src/components/editor/selection-manager.tsx +897 -0
  13. package/src/components/editor/site-edge-labels.tsx +90 -0
  14. package/src/components/editor/thumbnail-generator.tsx +166 -0
  15. package/src/components/editor/wall-measurement-label.tsx +258 -0
  16. package/src/components/feedback-dialog.tsx +265 -0
  17. package/src/components/pascal-radio.tsx +280 -0
  18. package/src/components/preview-button.tsx +16 -0
  19. package/src/components/systems/ceiling/ceiling-system.tsx +77 -0
  20. package/src/components/systems/roof/roof-edit-system.tsx +69 -0
  21. package/src/components/systems/stair/stair-edit-system.tsx +69 -0
  22. package/src/components/systems/zone/zone-label-editor-system.tsx +320 -0
  23. package/src/components/systems/zone/zone-system.tsx +87 -0
  24. package/src/components/tools/ceiling/ceiling-boundary-editor.tsx +42 -0
  25. package/src/components/tools/ceiling/ceiling-hole-editor.tsx +47 -0
  26. package/src/components/tools/ceiling/ceiling-tool.tsx +465 -0
  27. package/src/components/tools/door/door-math.ts +110 -0
  28. package/src/components/tools/door/door-tool.tsx +293 -0
  29. package/src/components/tools/door/move-door-tool.tsx +373 -0
  30. package/src/components/tools/item/item-tool.tsx +26 -0
  31. package/src/components/tools/item/move-tool.tsx +90 -0
  32. package/src/components/tools/item/placement-math.ts +85 -0
  33. package/src/components/tools/item/placement-strategies.ts +556 -0
  34. package/src/components/tools/item/placement-types.ts +117 -0
  35. package/src/components/tools/item/use-draft-node.ts +227 -0
  36. package/src/components/tools/item/use-placement-coordinator.tsx +877 -0
  37. package/src/components/tools/roof/move-roof-tool.tsx +288 -0
  38. package/src/components/tools/roof/roof-tool.tsx +318 -0
  39. package/src/components/tools/select/box-select-tool.tsx +626 -0
  40. package/src/components/tools/shared/cursor-sphere.tsx +119 -0
  41. package/src/components/tools/shared/polygon-editor.tsx +361 -0
  42. package/src/components/tools/site/site-boundary-editor.tsx +42 -0
  43. package/src/components/tools/slab/slab-boundary-editor.tsx +42 -0
  44. package/src/components/tools/slab/slab-hole-editor.tsx +47 -0
  45. package/src/components/tools/slab/slab-tool.tsx +322 -0
  46. package/src/components/tools/stair/stair-defaults.ts +7 -0
  47. package/src/components/tools/stair/stair-tool.tsx +194 -0
  48. package/src/components/tools/tool-manager.tsx +120 -0
  49. package/src/components/tools/wall/wall-drafting.ts +140 -0
  50. package/src/components/tools/wall/wall-tool.tsx +210 -0
  51. package/src/components/tools/window/move-window-tool.tsx +410 -0
  52. package/src/components/tools/window/window-math.ts +117 -0
  53. package/src/components/tools/window/window-tool.tsx +303 -0
  54. package/src/components/tools/zone/zone-boundary-editor.tsx +39 -0
  55. package/src/components/tools/zone/zone-tool.tsx +364 -0
  56. package/src/components/ui/action-menu/action-button.tsx +59 -0
  57. package/src/components/ui/action-menu/camera-actions.tsx +74 -0
  58. package/src/components/ui/action-menu/control-modes.tsx +240 -0
  59. package/src/components/ui/action-menu/furnish-tools.tsx +102 -0
  60. package/src/components/ui/action-menu/index.tsx +152 -0
  61. package/src/components/ui/action-menu/structure-tools.tsx +100 -0
  62. package/src/components/ui/action-menu/view-toggles.tsx +397 -0
  63. package/src/components/ui/command-palette/editor-commands.tsx +396 -0
  64. package/src/components/ui/command-palette/index.tsx +730 -0
  65. package/src/components/ui/controls/action-button.tsx +33 -0
  66. package/src/components/ui/controls/material-picker.tsx +194 -0
  67. package/src/components/ui/controls/metric-control.tsx +262 -0
  68. package/src/components/ui/controls/panel-section.tsx +65 -0
  69. package/src/components/ui/controls/segmented-control.tsx +45 -0
  70. package/src/components/ui/controls/slider-control.tsx +245 -0
  71. package/src/components/ui/controls/toggle-control.tsx +38 -0
  72. package/src/components/ui/floating-level-selector.tsx +355 -0
  73. package/src/components/ui/helpers/ceiling-helper.tsx +20 -0
  74. package/src/components/ui/helpers/helper-manager.tsx +33 -0
  75. package/src/components/ui/helpers/item-helper.tsx +40 -0
  76. package/src/components/ui/helpers/roof-helper.tsx +16 -0
  77. package/src/components/ui/helpers/slab-helper.tsx +20 -0
  78. package/src/components/ui/helpers/wall-helper.tsx +20 -0
  79. package/src/components/ui/item-catalog/catalog-items.tsx +1580 -0
  80. package/src/components/ui/item-catalog/item-catalog.tsx +219 -0
  81. package/src/components/ui/panels/ceiling-panel.tsx +230 -0
  82. package/src/components/ui/panels/collections/collections-popover.tsx +356 -0
  83. package/src/components/ui/panels/door-panel.tsx +600 -0
  84. package/src/components/ui/panels/item-panel.tsx +306 -0
  85. package/src/components/ui/panels/panel-manager.tsx +59 -0
  86. package/src/components/ui/panels/panel-wrapper.tsx +80 -0
  87. package/src/components/ui/panels/presets/presets-popover.tsx +511 -0
  88. package/src/components/ui/panels/reference-panel.tsx +177 -0
  89. package/src/components/ui/panels/roof-panel.tsx +262 -0
  90. package/src/components/ui/panels/roof-segment-panel.tsx +326 -0
  91. package/src/components/ui/panels/slab-panel.tsx +228 -0
  92. package/src/components/ui/panels/stair-panel.tsx +304 -0
  93. package/src/components/ui/panels/stair-segment-panel.tsx +339 -0
  94. package/src/components/ui/panels/wall-panel.tsx +123 -0
  95. package/src/components/ui/panels/window-panel.tsx +441 -0
  96. package/src/components/ui/primitives/button.tsx +69 -0
  97. package/src/components/ui/primitives/card.tsx +75 -0
  98. package/src/components/ui/primitives/color-dot.tsx +61 -0
  99. package/src/components/ui/primitives/context-menu.tsx +227 -0
  100. package/src/components/ui/primitives/dialog.tsx +129 -0
  101. package/src/components/ui/primitives/dropdown-menu.tsx +228 -0
  102. package/src/components/ui/primitives/error-boundary.tsx +52 -0
  103. package/src/components/ui/primitives/input.tsx +21 -0
  104. package/src/components/ui/primitives/number-input.tsx +187 -0
  105. package/src/components/ui/primitives/opacity-control.tsx +79 -0
  106. package/src/components/ui/primitives/popover.tsx +42 -0
  107. package/src/components/ui/primitives/separator.tsx +28 -0
  108. package/src/components/ui/primitives/sheet.tsx +130 -0
  109. package/src/components/ui/primitives/shortcut-token.tsx +64 -0
  110. package/src/components/ui/primitives/sidebar.tsx +855 -0
  111. package/src/components/ui/primitives/skeleton.tsx +13 -0
  112. package/src/components/ui/primitives/slider.tsx +58 -0
  113. package/src/components/ui/primitives/switch.tsx +29 -0
  114. package/src/components/ui/primitives/tooltip.tsx +57 -0
  115. package/src/components/ui/scene-loader.tsx +40 -0
  116. package/src/components/ui/sidebar/app-sidebar.tsx +103 -0
  117. package/src/components/ui/sidebar/icon-rail.tsx +147 -0
  118. package/src/components/ui/sidebar/panels/settings-panel/audio-settings-dialog.tsx +100 -0
  119. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +438 -0
  120. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +188 -0
  121. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +80 -0
  122. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +126 -0
  123. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +64 -0
  124. package/src/components/ui/sidebar/panels/site-panel/index.tsx +1543 -0
  125. package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +98 -0
  126. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +117 -0
  127. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +65 -0
  128. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +214 -0
  129. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +96 -0
  130. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +216 -0
  131. package/src/components/ui/sidebar/panels/site-panel/tree-node-actions.tsx +115 -0
  132. package/src/components/ui/sidebar/panels/site-panel/tree-node-drag.tsx +342 -0
  133. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +271 -0
  134. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +106 -0
  135. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +64 -0
  136. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +87 -0
  137. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +167 -0
  138. package/src/components/ui/sidebar/tab-bar.tsx +39 -0
  139. package/src/components/ui/slider-demo.tsx +36 -0
  140. package/src/components/ui/slider.tsx +81 -0
  141. package/src/components/ui/viewer-toolbar.tsx +342 -0
  142. package/src/components/viewer-overlay.tsx +499 -0
  143. package/src/components/viewer-zone-system.tsx +48 -0
  144. package/src/contexts/presets-context.tsx +121 -0
  145. package/src/hooks/use-auto-save.ts +194 -0
  146. package/src/hooks/use-contextual-tools.ts +52 -0
  147. package/src/hooks/use-grid-events.ts +106 -0
  148. package/src/hooks/use-keyboard.ts +214 -0
  149. package/src/hooks/use-mobile.ts +19 -0
  150. package/src/hooks/use-reduced-motion.ts +20 -0
  151. package/src/index.tsx +33 -0
  152. package/src/lib/constants.ts +3 -0
  153. package/src/lib/level-selection.ts +31 -0
  154. package/src/lib/scene.ts +394 -0
  155. package/src/lib/sfx/index.ts +2 -0
  156. package/src/lib/sfx-bus.ts +49 -0
  157. package/src/lib/sfx-player.ts +60 -0
  158. package/src/lib/utils.ts +43 -0
  159. package/src/store/use-audio.tsx +45 -0
  160. package/src/store/use-command-registry.ts +36 -0
  161. package/src/store/use-editor.tsx +522 -0
  162. package/src/store/use-palette-view-registry.ts +45 -0
  163. package/src/store/use-upload.ts +90 -0
  164. package/src/three-types.ts +3 -0
  165. package/tsconfig.json +9 -0
@@ -0,0 +1,396 @@
1
+ 'use client'
2
+
3
+ import type { AnyNodeId } from '@pascal-app/core'
4
+ import { LevelNode, useScene } from '@pascal-app/core'
5
+ import { useViewer } from '@pascal-app/viewer'
6
+ import {
7
+ AppWindow,
8
+ ArrowRight,
9
+ Box,
10
+ Building2,
11
+ Camera,
12
+ Copy,
13
+ DoorOpen,
14
+ Eye,
15
+ EyeOff,
16
+ FileJson,
17
+ Grid3X3,
18
+ Hexagon,
19
+ Layers,
20
+ Map,
21
+ Maximize2,
22
+ Minimize2,
23
+ Moon,
24
+ MousePointer2,
25
+ Package,
26
+ PencilLine,
27
+ Plus,
28
+ Redo2,
29
+ Square,
30
+ SquareStack,
31
+ Sun,
32
+ Trash2,
33
+ Undo2,
34
+ Video,
35
+ } from 'lucide-react'
36
+ import { useEffect } from 'react'
37
+ import { deleteLevelWithFallbackSelection } from '../../../lib/level-selection'
38
+ import { useCommandRegistry } from '../../../store/use-command-registry'
39
+ import type { StructureTool } from '../../../store/use-editor'
40
+ import useEditor from '../../../store/use-editor'
41
+ import { useCommandPalette } from './index'
42
+
43
+ export function EditorCommands() {
44
+ const register = useCommandRegistry((s) => s.register)
45
+ const { navigateTo, setInputValue, setOpen } = useCommandPalette()
46
+
47
+ const { setPhase, setMode, setTool, setStructureLayer, isPreviewMode, setPreviewMode } =
48
+ useEditor()
49
+
50
+ const exportScene = useViewer((s) => s.exportScene)
51
+
52
+ // Re-register when exportScene availability changes (it's a conditional action)
53
+ useEffect(() => {
54
+ const run = (fn: () => void) => {
55
+ fn()
56
+ setOpen(false)
57
+ }
58
+
59
+ const activateTool = (tool: StructureTool) => {
60
+ run(() => {
61
+ setPhase('structure')
62
+ setMode('build')
63
+ if (tool === 'zone') setStructureLayer('zones')
64
+ setTool(tool)
65
+ })
66
+ }
67
+
68
+ return register([
69
+ // ── Scene ────────────────────────────────────────────────────────────
70
+ {
71
+ id: 'editor.tool.wall',
72
+ label: 'Wall Tool',
73
+ group: 'Scene',
74
+ icon: <Square className="h-4 w-4" />,
75
+ keywords: ['draw', 'build', 'structure'],
76
+ execute: () => activateTool('wall'),
77
+ },
78
+ {
79
+ id: 'editor.tool.slab',
80
+ label: 'Slab Tool',
81
+ group: 'Scene',
82
+ icon: <Layers className="h-4 w-4" />,
83
+ keywords: ['floor', 'build'],
84
+ execute: () => activateTool('slab'),
85
+ },
86
+ {
87
+ id: 'editor.tool.ceiling',
88
+ label: 'Ceiling Tool',
89
+ group: 'Scene',
90
+ icon: <Grid3X3 className="h-4 w-4" />,
91
+ keywords: ['top', 'build'],
92
+ execute: () => activateTool('ceiling'),
93
+ },
94
+ {
95
+ id: 'editor.tool.door',
96
+ label: 'Door Tool',
97
+ group: 'Scene',
98
+ icon: <DoorOpen className="h-4 w-4" />,
99
+ keywords: ['opening', 'entrance'],
100
+ execute: () => activateTool('door'),
101
+ },
102
+ {
103
+ id: 'editor.tool.window',
104
+ label: 'Window Tool',
105
+ group: 'Scene',
106
+ icon: <AppWindow className="h-4 w-4" />,
107
+ keywords: ['opening', 'glass'],
108
+ execute: () => activateTool('window'),
109
+ },
110
+ {
111
+ id: 'editor.tool.item',
112
+ label: 'Item Tool',
113
+ group: 'Scene',
114
+ icon: <Package className="h-4 w-4" />,
115
+ keywords: ['furniture', 'object', 'asset', 'furnish'],
116
+ execute: () => activateTool('item'),
117
+ },
118
+ {
119
+ id: 'editor.tool.stair',
120
+ label: 'Stair Tool',
121
+ group: 'Scene',
122
+ icon: <ArrowRight className="h-4 w-4" />,
123
+ keywords: ['stairs', 'staircase', 'flight', 'landing', 'steps'],
124
+ execute: () => activateTool('stair'),
125
+ },
126
+ {
127
+ id: 'editor.tool.zone',
128
+ label: 'Zone Tool',
129
+ group: 'Scene',
130
+ icon: <Hexagon className="h-4 w-4" />,
131
+ keywords: ['area', 'room', 'space'],
132
+ execute: () => activateTool('zone'),
133
+ },
134
+ {
135
+ id: 'editor.delete-selection',
136
+ label: 'Delete Selection',
137
+ group: 'Scene',
138
+ icon: <Trash2 className="h-4 w-4" />,
139
+ keywords: ['remove', 'erase'],
140
+ shortcut: ['⌫'],
141
+ when: () => useViewer.getState().selection.selectedIds.length > 0,
142
+ execute: () =>
143
+ run(() => {
144
+ const { selectedIds } = useViewer.getState().selection
145
+ useScene.getState().deleteNodes(selectedIds as any[])
146
+ }),
147
+ },
148
+
149
+ // ── Levels ───────────────────────────────────────────────────────────
150
+ {
151
+ id: 'editor.level.goto',
152
+ label: 'Go to Level',
153
+ group: 'Levels',
154
+ icon: <ArrowRight className="h-4 w-4" />,
155
+ keywords: ['level', 'floor', 'go', 'navigate', 'switch', 'select'],
156
+ navigate: true,
157
+ when: () => Object.values(useScene.getState().nodes).some((n) => n.type === 'level'),
158
+ execute: () => navigateTo('goto-level'),
159
+ },
160
+ {
161
+ id: 'editor.level.add',
162
+ label: 'Add Level',
163
+ group: 'Levels',
164
+ icon: <Plus className="h-4 w-4" />,
165
+ keywords: ['level', 'floor', 'add', 'create', 'new'],
166
+ execute: () =>
167
+ run(() => {
168
+ const { nodes } = useScene.getState()
169
+ const building = Object.values(nodes).find((n) => n.type === 'building')
170
+ if (!building) return
171
+ const newLevel = LevelNode.parse({
172
+ level: building.children.length,
173
+ children: [],
174
+ parentId: building.id,
175
+ })
176
+ useScene.getState().createNode(newLevel, building.id)
177
+ useViewer.getState().setSelection({ levelId: newLevel.id })
178
+ }),
179
+ },
180
+ {
181
+ id: 'editor.level.rename',
182
+ label: 'Rename Level',
183
+ group: 'Levels',
184
+ icon: <PencilLine className="h-4 w-4" />,
185
+ keywords: ['level', 'floor', 'rename', 'name'],
186
+ navigate: true,
187
+ when: () => !!useViewer.getState().selection.levelId,
188
+ execute: () => {
189
+ const activeLevelId = useViewer.getState().selection.levelId
190
+ if (!activeLevelId) return
191
+ const level = useScene.getState().nodes[activeLevelId as AnyNodeId] as LevelNode
192
+ setInputValue(level?.name ?? '')
193
+ navigateTo('rename-level')
194
+ },
195
+ },
196
+ {
197
+ id: 'editor.level.delete',
198
+ label: 'Delete Level',
199
+ group: 'Levels',
200
+ icon: <Trash2 className="h-4 w-4" />,
201
+ keywords: ['level', 'floor', 'delete', 'remove'],
202
+ when: () => {
203
+ const levelId = useViewer.getState().selection.levelId
204
+ if (!levelId) return false
205
+ const node = useScene.getState().nodes[levelId as AnyNodeId] as LevelNode
206
+ return node?.type === 'level' && node.level !== 0
207
+ },
208
+ execute: () =>
209
+ run(() => {
210
+ const activeLevelId = useViewer.getState().selection.levelId
211
+ if (!activeLevelId) return
212
+ deleteLevelWithFallbackSelection(activeLevelId as AnyNodeId)
213
+ }),
214
+ },
215
+
216
+ // ── Viewer Controls ──────────────────────────────────────────────────
217
+ {
218
+ id: 'editor.viewer.wall-mode',
219
+ label: 'Wall Mode',
220
+ group: 'Viewer Controls',
221
+ icon: <Layers className="h-4 w-4" />,
222
+ keywords: ['wall', 'cutaway', 'up', 'down', 'view'],
223
+ badge: () => {
224
+ const mode = useViewer.getState().wallMode
225
+ return { cutaway: 'Cutaway', up: 'Up', down: 'Down' }[mode]
226
+ },
227
+ navigate: true,
228
+ execute: () => navigateTo('wall-mode'),
229
+ },
230
+ {
231
+ id: 'editor.viewer.level-mode',
232
+ label: 'Level Mode',
233
+ group: 'Viewer Controls',
234
+ icon: <SquareStack className="h-4 w-4" />,
235
+ keywords: ['level', 'floor', 'exploded', 'stacked', 'solo'],
236
+ badge: () => {
237
+ const mode = useViewer.getState().levelMode
238
+ return { manual: 'Manual', stacked: 'Stacked', exploded: 'Exploded', solo: 'Solo' }[mode]
239
+ },
240
+ navigate: true,
241
+ execute: () => navigateTo('level-mode'),
242
+ },
243
+ {
244
+ id: 'editor.viewer.camera-mode',
245
+ label: () => {
246
+ const mode = useViewer.getState().cameraMode
247
+ return `Camera: Switch to ${mode === 'perspective' ? 'Orthographic' : 'Perspective'}`
248
+ },
249
+ group: 'Viewer Controls',
250
+ icon: <Video className="h-4 w-4" />,
251
+ keywords: ['camera', 'ortho', 'perspective', '2d', '3d', 'view'],
252
+ execute: () =>
253
+ run(() => {
254
+ const { cameraMode, setCameraMode } = useViewer.getState()
255
+ setCameraMode(cameraMode === 'perspective' ? 'orthographic' : 'perspective')
256
+ }),
257
+ },
258
+ {
259
+ id: 'editor.viewer.theme',
260
+ label: () => {
261
+ const theme = useViewer.getState().theme
262
+ return theme === 'dark' ? 'Switch to Light Theme' : 'Switch to Dark Theme'
263
+ },
264
+ group: 'Viewer Controls',
265
+ icon: <Sun className="h-4 w-4" />, // icon is static; label conveys the action
266
+ keywords: ['theme', 'dark', 'light', 'appearance', 'color'],
267
+ execute: () =>
268
+ run(() => {
269
+ const { theme, setTheme } = useViewer.getState()
270
+ setTheme(theme === 'dark' ? 'light' : 'dark')
271
+ }),
272
+ },
273
+ {
274
+ id: 'editor.viewer.camera-snapshot',
275
+ label: 'Camera Snapshot',
276
+ group: 'Viewer Controls',
277
+ icon: <Camera className="h-4 w-4" />,
278
+ keywords: ['camera', 'snapshot', 'capture', 'save', 'view', 'bookmark'],
279
+ navigate: true,
280
+ execute: () => navigateTo('camera-view'),
281
+ },
282
+
283
+ // ── View ─────────────────────────────────────────────────────────────
284
+ {
285
+ id: 'editor.view.preview',
286
+ label: () => (isPreviewMode ? 'Exit Preview' : 'Enter Preview'),
287
+ group: 'View',
288
+ icon: isPreviewMode ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />,
289
+ keywords: ['preview', 'view', 'read-only', 'present'],
290
+ execute: () => run(() => setPreviewMode(!isPreviewMode)),
291
+ },
292
+ {
293
+ id: 'editor.view.fullscreen',
294
+ label: 'Toggle Fullscreen',
295
+ group: 'View',
296
+ icon: <Maximize2 className="h-4 w-4" />,
297
+ keywords: ['fullscreen', 'maximize', 'expand', 'window'],
298
+ execute: () =>
299
+ run(() => {
300
+ if (document.fullscreenElement) document.exitFullscreen()
301
+ else document.documentElement.requestFullscreen()
302
+ }),
303
+ },
304
+
305
+ // ── History ──────────────────────────────────────────────────────────
306
+ {
307
+ id: 'editor.history.undo',
308
+ label: 'Undo',
309
+ group: 'History',
310
+ icon: <Undo2 className="h-4 w-4" />,
311
+ keywords: ['undo', 'revert', 'back'],
312
+ execute: () => run(() => useScene.temporal.getState().undo()),
313
+ },
314
+ {
315
+ id: 'editor.history.redo',
316
+ label: 'Redo',
317
+ group: 'History',
318
+ icon: <Redo2 className="h-4 w-4" />,
319
+ keywords: ['redo', 'forward', 'repeat'],
320
+ execute: () => run(() => useScene.temporal.getState().redo()),
321
+ },
322
+
323
+ // ── Export & Share ───────────────────────────────────────────────────
324
+ {
325
+ id: 'editor.export.json',
326
+ label: 'Export Scene (JSON)',
327
+ group: 'Export & Share',
328
+ icon: <FileJson className="h-4 w-4" />,
329
+ keywords: ['export', 'download', 'json', 'save', 'data'],
330
+ execute: () =>
331
+ run(() => {
332
+ const { nodes, rootNodeIds } = useScene.getState()
333
+ const blob = new Blob([JSON.stringify({ nodes, rootNodeIds }, null, 2)], {
334
+ type: 'application/json',
335
+ })
336
+ const url = URL.createObjectURL(blob)
337
+ Object.assign(document.createElement('a'), {
338
+ href: url,
339
+ download: `scene_${new Date().toISOString().split('T')[0]}.json`,
340
+ }).click()
341
+ URL.revokeObjectURL(url)
342
+ }),
343
+ },
344
+ ...(exportScene
345
+ ? [
346
+ {
347
+ id: 'editor.export.glb',
348
+ label: 'Export 3D Model (GLB)',
349
+ group: 'Export & Share',
350
+ icon: <Box className="h-4 w-4" />,
351
+ keywords: ['export', 'glb', 'gltf', '3d', 'model', 'download'],
352
+ execute: () => run(() => exportScene()),
353
+ } as const,
354
+ ]
355
+ : []),
356
+ {
357
+ id: 'editor.export.share-link',
358
+ label: 'Copy Share Link',
359
+ group: 'Export & Share',
360
+ icon: <Copy className="h-4 w-4" />,
361
+ keywords: ['share', 'copy', 'url', 'link'],
362
+ execute: () => run(() => navigator.clipboard.writeText(window.location.href)),
363
+ },
364
+ {
365
+ id: 'editor.export.screenshot',
366
+ label: 'Take Screenshot',
367
+ group: 'Export & Share',
368
+ icon: <Camera className="h-4 w-4" />,
369
+ keywords: ['screenshot', 'capture', 'image', 'photo', 'png'],
370
+ execute: () =>
371
+ run(() => {
372
+ const canvas = document.querySelector('canvas')
373
+ if (!canvas) return
374
+ Object.assign(document.createElement('a'), {
375
+ href: canvas.toDataURL('image/png'),
376
+ download: `screenshot_${new Date().toISOString().split('T')[0]}.png`,
377
+ }).click()
378
+ }),
379
+ },
380
+ ])
381
+ }, [
382
+ register,
383
+ navigateTo,
384
+ setInputValue,
385
+ setOpen,
386
+ setPhase,
387
+ setMode,
388
+ setTool,
389
+ setStructureLayer,
390
+ isPreviewMode,
391
+ setPreviewMode,
392
+ exportScene,
393
+ ])
394
+
395
+ return null
396
+ }