@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
@@ -8,18 +8,9 @@ import {
8
8
  useScene,
9
9
  } from '@pascal-app/core'
10
10
  import { type HoverStyles, InteractiveSystem, useViewer, Viewer } from '@pascal-app/viewer'
11
- import {
12
- memo,
13
- type ReactNode,
14
- useCallback,
15
- useEffect,
16
- useLayoutEffect,
17
- useRef,
18
- useState,
19
- } from 'react'
11
+ import { memo, type ReactNode, useCallback, useEffect, useRef, useState } from 'react'
20
12
  import { ViewerOverlay } from '../../components/viewer-overlay'
21
13
  import { ViewerZoneSystem } from '../../components/viewer-zone-system'
22
- import { type PresetsAdapter, PresetsProvider } from '../../contexts/presets-context'
23
14
  import { type SaveStatus, useAutoSave } from '../../hooks/use-auto-save'
24
15
  import { useKeyboard } from '../../hooks/use-keyboard'
25
16
  import {
@@ -61,31 +52,38 @@ import { FloatingActionMenu } from './floating-action-menu'
61
52
  import { FloatingBuildingActionMenu } from './floating-building-action-menu'
62
53
  import { FloorplanPanel } from './floorplan-panel'
63
54
  import { Grid } from './grid'
64
- import { PresetThumbnailGenerator } from './preset-thumbnail-generator'
55
+ import { GroupMoveHandle } from './group-move-handle'
56
+ import { GroupRotateHandle } from './group-rotate-handle'
57
+ import { NodeArrowHandles } from './node-arrow-handles'
65
58
  import { SelectionManager } from './selection-manager'
66
59
  import { SiteEdgeLabels } from './site-edge-labels'
60
+ import { SlabHoleHighlights } from './slab-hole-highlights'
61
+ import { SnapshotCaptureOverlay } from './snapshot-capture-overlay'
67
62
  import { type SnapshotCameraData, ThumbnailGenerator } from './thumbnail-generator'
68
63
  import { WallMeasurementLabel } from './wall-measurement-label'
64
+ import { WallMoveSideHandles } from './wall-move-side-handles'
65
+ import { WallOpeningHighlights } from './wall-opening-highlights'
69
66
 
70
67
  const CAMERA_CONTROLS_HINT_DISMISSED_STORAGE_KEY = 'editor-camera-controls-hint-dismissed:v1'
71
68
  const DELETE_CURSOR_BADGE_COLOR = '#ef4444'
72
69
  const DELETE_CURSOR_BADGE_OFFSET_X = 14
73
70
  const DELETE_CURSOR_BADGE_OFFSET_Y = 14
74
- const PAINT_CURSOR_BADGE_COLOR = '#f59e0b'
71
+ const PAINT_CURSOR_BADGE_COLOR = '#818cf8'
75
72
  const PAINT_CURSOR_BADGE_DISABLED_COLOR = '#94a3b8'
76
73
  const PAINT_CURSOR_BADGE_OFFSET_X = 14
77
74
  const PAINT_CURSOR_BADGE_OFFSET_Y = 14
78
75
  const EDITOR_HOVER_STYLES: HoverStyles = {
79
- default: { visibleColor: 0x00_aaff, hiddenColor: 0xf3_ff47, strength: 5, pulse: true },
80
- delete: { visibleColor: 0xef_4444, hiddenColor: 0x99_1b1b, strength: 6, pulse: false },
81
- 'paint-ready': { visibleColor: 0xf5_9e0b, hiddenColor: 0xfd_e068, strength: 5, pulse: true },
76
+ default: { visibleColor: 0x00_aa_ff, hiddenColor: 0xf3_ff_47, strength: 5, pulse: true },
77
+ delete: { visibleColor: 0xef_44_44, hiddenColor: 0x99_1b_1b, strength: 6, pulse: false },
78
+ 'paint-ready': { visibleColor: 0xf5_9e_0b, hiddenColor: 0xfd_e0_68, strength: 5, pulse: true },
82
79
  'paint-disabled': {
83
- visibleColor: 0x94_a3b8,
84
- hiddenColor: 0x47_5569,
80
+ visibleColor: 0x94_a3_b8,
81
+ hiddenColor: 0x47_55_69,
85
82
  strength: 4,
86
83
  pulse: false,
87
84
  },
88
85
  }
86
+ const EDITOR_DEFAULT_RENDER = { shading: 'solid' } as const
89
87
 
90
88
  /**
91
89
  * Wire up module-level singletons (spatial grid, space detection, SFX) for
@@ -123,12 +121,18 @@ export interface EditorProps {
123
121
  sidebarTabs?: (SidebarTab & { component: React.ComponentType })[]
124
122
  viewerToolbarLeft?: ReactNode
125
123
  viewerToolbarRight?: ReactNode
124
+ /**
125
+ * Docked below the node inspector (v2). Hosts mount the "save as preset"
126
+ * affordance here so it reads as part of the inspector surface and shows
127
+ * only while a node is selected.
128
+ */
129
+ inspectorFooter?: ReactNode
126
130
 
127
131
  projectId?: string | null
128
132
 
129
133
  // Persistence — defaults to localStorage when omitted
130
134
  onLoad?: () => Promise<SceneGraph | null>
131
- onSave?: (scene: SceneGraph) => Promise<void>
135
+ onSave?: (scene: SceneGraph, options?: { keepalive?: boolean }) => Promise<void>
132
136
  onDirty?: () => void
133
137
  onSaveStatusChange?: (status: SaveStatus) => void
134
138
 
@@ -151,9 +155,6 @@ export interface EditorProps {
151
155
  sitePanelProps?: SitePanelProps
152
156
  extraSidebarPanels?: ExtraPanel[]
153
157
 
154
- // Presets storage backend (defaults to localStorage)
155
- presetsAdapter?: PresetsAdapter
156
-
157
158
  // Command palette fallback when no commands match
158
159
  commandPaletteEmptyAction?: CommandPaletteEmptyAction
159
160
  }
@@ -332,6 +333,7 @@ const EDITOR_CAMERA_CONTROL_HINTS: CameraControlHint[] = [
332
333
  {
333
334
  action: 'Pan',
334
335
  keys: [{ value: 'Space' }, { value: 'Left click' }],
336
+ alternativeKeys: [{ value: 'Middle click' }],
335
337
  },
336
338
  { action: 'Rotate', keys: [{ value: 'Right click' }] },
337
339
  { action: 'Zoom', keys: [{ value: 'Scroll' }] },
@@ -462,7 +464,7 @@ function ViewerCanvasControlsHint({
462
464
  <div className="pointer-events-none absolute top-14 left-1/2 z-40 max-w-[calc(100%-2rem)] -translate-x-1/2">
463
465
  <section
464
466
  aria-label="Camera controls hint"
465
- className="pointer-events-auto flex items-start gap-3 rounded-2xl border border-border/35 bg-background/90 px-3.5 py-2.5 shadow-[0_22px_40px_-28px_rgba(15,23,42,0.65),0_10px_24px_-20px_rgba(15,23,42,0.55)] backdrop-blur-xl"
467
+ className="pointer-events-auto flex items-start gap-3 rounded-2xl border border-border/35 bg-background/90 px-3.5 py-2.5 shadow-elevation-4 backdrop-blur-xl"
466
468
  >
467
469
  <div className="grid min-w-0 flex-1 grid-cols-3 items-start divide-x divide-border/18">
468
470
  {hints.map((hint) => (
@@ -526,67 +528,89 @@ function DeleteCursorBadge({ position }: { position: { x: number; y: number } })
526
528
 
527
529
  function PaintCursorBadge({
528
530
  position,
529
- label,
530
531
  disabled,
531
- icon,
532
532
  }: {
533
533
  position: { x: number; y: number }
534
- label: string
535
534
  disabled: boolean
536
- icon: string
537
535
  }) {
538
536
  const accentColor = disabled ? PAINT_CURSOR_BADGE_DISABLED_COLOR : PAINT_CURSOR_BADGE_COLOR
537
+ const lineHeight = 18
539
538
 
540
539
  return (
541
540
  <div
542
541
  aria-hidden="true"
543
542
  className="pointer-events-none absolute z-40"
544
543
  style={{
545
- left: position.x + PAINT_CURSOR_BADGE_OFFSET_X,
546
- top: position.y + PAINT_CURSOR_BADGE_OFFSET_Y,
544
+ left: position.x,
545
+ top: position.y,
547
546
  }}
548
547
  >
549
548
  <div
550
- className="flex items-center gap-2 rounded-xl border border-white/5 bg-zinc-900/95 px-3 py-2 shadow-[0_8px_16px_-4px_rgba(0,0,0,0.3),0_4px_8px_-4px_rgba(0,0,0,0.2)]"
549
+ className="-translate-x-1/2 -translate-y-full absolute top-0 left-1/2"
550
+ style={{
551
+ backgroundColor: accentColor,
552
+ boxShadow: `0 0 12px ${accentColor}cc`,
553
+ height: lineHeight,
554
+ width: 2,
555
+ }}
556
+ />
557
+ <div
558
+ className="absolute top-0 left-1/2 flex h-8 w-8 items-center justify-center rounded-xl border border-white/5 bg-zinc-900/95 shadow-[0_8px_16px_-4px_rgba(0,0,0,0.3),0_4px_8px_-4px_rgba(0,0,0,0.2)]"
551
559
  style={{
552
560
  boxShadow: `0 8px 16px -4px rgba(0,0,0,0.3), 0 4px 8px -4px rgba(0,0,0,0.2), 0 0 18px ${accentColor}22`,
561
+ transform: `translate(-50%, calc(-100% - ${lineHeight}px))`,
553
562
  }}
554
563
  >
555
- <Icon
564
+ {/* eslint-disable-next-line @next/next/no-img-element */}
565
+ <img
566
+ alt=""
556
567
  aria-hidden="true"
557
- className="drop-shadow-[0_2px_4px_rgba(0,0,0,0.5)]"
558
- color={accentColor}
559
- height={16}
560
- icon={icon}
561
- width={16}
568
+ className="h-5 w-5 object-contain drop-shadow-[0_2px_4px_rgba(0,0,0,0.5)]"
569
+ src="/icons/paint.png"
562
570
  />
563
- <span className="font-medium text-[11px]" style={{ color: accentColor }}>
564
- {label}
565
- </span>
566
571
  </div>
567
572
  </div>
568
573
  )
569
574
  }
570
575
 
576
+ // Subscribes to `gridSnapStep` so the visible grid cell size matches whatever
577
+ // the wall draft tool snaps to — otherwise the cursor lands between visible
578
+ // grid lines when the user picks a finer snap (0.25 / 0.1 / 0.05).
579
+ function SnapAwareGrid() {
580
+ const gridSnapStep = useEditor((s) => s.gridSnapStep)
581
+ return <Grid cellColor="#aaa" cellSize={gridSnapStep} fadeDistance={500} sectionColor="#ccc" />
582
+ }
583
+
571
584
  // ── Viewer scene content: memoized so <Viewer> doesn't re-render on mode/viewMode changes ──
572
585
 
573
586
  const ViewerSceneContent = memo(function ViewerSceneContent({
574
587
  isVersionPreviewMode,
575
588
  isLoading,
576
589
  isFirstPersonMode,
590
+ isStudioMode,
577
591
  onThumbnailCapture,
578
592
  }: {
579
593
  isVersionPreviewMode: boolean
580
594
  isLoading: boolean
581
595
  isFirstPersonMode: boolean
596
+ isStudioMode: boolean
582
597
  onThumbnailCapture?: (blob: Blob, cameraData: SnapshotCameraData) => void
583
598
  }) {
599
+ // Studio mode is a clean render/snapshot surface — no selection or editing
600
+ // affordances. It mirrors version-preview's chrome gating on the canvas.
601
+ const noEditing = isVersionPreviewMode || isFirstPersonMode || isStudioMode
584
602
  return (
585
603
  <>
586
- {!isFirstPersonMode && <SelectionManager />}
587
- {!(isVersionPreviewMode || isFirstPersonMode) && <BoxSelectTool />}
588
- {!(isVersionPreviewMode || isFirstPersonMode) && <FloatingActionMenu />}
589
- {!(isVersionPreviewMode || isFirstPersonMode) && <FloatingBuildingActionMenu />}
604
+ {!(isFirstPersonMode || isStudioMode) && <SelectionManager />}
605
+ {!noEditing && <BoxSelectTool />}
606
+ {!noEditing && <NodeArrowHandles />}
607
+ {!noEditing && <GroupRotateHandle />}
608
+ {!noEditing && <GroupMoveHandle />}
609
+ {!noEditing && <WallOpeningHighlights />}
610
+ {!noEditing && <SlabHoleHighlights />}
611
+ {!noEditing && <WallMoveSideHandles />}
612
+ {!noEditing && <FloatingActionMenu />}
613
+ {!noEditing && <FloatingBuildingActionMenu />}
590
614
  {!isFirstPersonMode && <WallMeasurementLabel />}
591
615
  <ExportManager />
592
616
  {isFirstPersonMode ? <ViewerZoneSystem /> : <ZoneSystem />}
@@ -594,16 +618,13 @@ const ViewerSceneContent = memo(function ViewerSceneContent({
594
618
  <CeilingSelectionAffordanceSystem />
595
619
  <RoofEditSystem />
596
620
  <StairEditSystem />
597
- {!(isLoading || isFirstPersonMode) && (
598
- <Grid cellColor="#aaa" fadeDistance={500} sectionColor="#ccc" />
599
- )}
600
- {!(isLoading || isVersionPreviewMode || isFirstPersonMode) && <ToolManager />}
621
+ {!(isLoading || isFirstPersonMode) && <SnapAwareGrid />}
622
+ {!(isLoading || noEditing) && <ToolManager />}
601
623
  {isFirstPersonMode && <FirstPersonControls />}
602
624
  <CustomCameraControls />
603
625
  <ThumbnailGenerator onThumbnailCapture={onThumbnailCapture} />
604
- <PresetThumbnailGenerator />
605
626
  {!isFirstPersonMode && <SiteEdgeLabels />}
606
- {isFirstPersonMode && <InteractiveSystem />}
627
+ <InteractiveSystem />
607
628
  </>
608
629
  )
609
630
  })
@@ -676,7 +697,7 @@ function DeleteCursorLayer({
676
697
 
677
698
  return (
678
699
  <div
679
- className="pointer-events-none"
700
+ className="pointer-events-none z-40"
680
701
  ref={badgeRef}
681
702
  style={{ display: 'none', position: 'absolute', left: 0, top: 0 }}
682
703
  >
@@ -694,15 +715,12 @@ function PaintCursorLayer({
694
715
  }) {
695
716
  const mode = useEditor((s) => s.mode)
696
717
  const activePaintMaterial = useEditor((s) => s.activePaintMaterial)
697
- const activePaintTarget = useEditor((s) => s.activePaintTarget)
698
- const badgeRef = useRef<HTMLDivElement>(null)
718
+ const [position, setPosition] = useState<{ x: number; y: number } | null>(null)
699
719
  const active = mode === 'material-paint' && !isVersionPreviewMode
700
720
 
701
721
  useEffect(() => {
702
722
  if (!active) {
703
- if (badgeRef.current) {
704
- badgeRef.current.style.display = 'none'
705
- }
723
+ setPosition(null)
706
724
  return
707
725
  }
708
726
  const el = containerRef.current
@@ -710,20 +728,16 @@ function PaintCursorLayer({
710
728
  let frame = 0
711
729
  let nextX = 0
712
730
  let nextY = 0
713
- const badge = badgeRef.current
714
731
 
715
732
  const flushPosition = () => {
716
733
  frame = 0
717
- if (!badge) return
718
- badge.style.display = 'block'
719
- badge.style.transform = `translate(${nextX + PAINT_CURSOR_BADGE_OFFSET_X}px, ${nextY + PAINT_CURSOR_BADGE_OFFSET_Y}px)`
734
+ setPosition({ x: nextX, y: nextY })
720
735
  }
721
736
 
722
- const onMove = (e: PointerEvent) => {
737
+ const updateFromEvent = (e: PointerEvent) => {
723
738
  const rect = el.getBoundingClientRect()
724
739
  nextX = e.clientX - rect.left
725
740
  nextY = e.clientY - rect.top
726
-
727
741
  if (frame === 0) {
728
742
  frame = window.requestAnimationFrame(flushPosition)
729
743
  }
@@ -733,17 +747,19 @@ function PaintCursorLayer({
733
747
  window.cancelAnimationFrame(frame)
734
748
  frame = 0
735
749
  }
736
- if (badge) {
737
- badge.style.display = 'none'
738
- }
750
+ setPosition(null)
739
751
  }
740
- el.addEventListener('pointermove', onMove)
752
+ el.addEventListener('pointermove', updateFromEvent)
753
+ el.addEventListener('pointerenter', updateFromEvent)
754
+ el.addEventListener('pointerdown', updateFromEvent)
741
755
  el.addEventListener('pointerleave', onLeave)
742
756
  return () => {
743
757
  if (frame !== 0) {
744
758
  window.cancelAnimationFrame(frame)
745
759
  }
746
- el.removeEventListener('pointermove', onMove)
760
+ el.removeEventListener('pointermove', updateFromEvent)
761
+ el.removeEventListener('pointerenter', updateFromEvent)
762
+ el.removeEventListener('pointerdown', updateFromEvent)
747
763
  el.removeEventListener('pointerleave', onLeave)
748
764
  }
749
765
  }, [active, containerRef])
@@ -753,29 +769,15 @@ function PaintCursorLayer({
753
769
  (activePaintMaterial.material !== undefined ||
754
770
  activePaintMaterial.materialPreset !== undefined),
755
771
  )
756
- const label = !hasMaterial ? 'Choose material' : `Paint ${activePaintTarget}`
757
- const icon = 'mdi:format-color-fill'
758
772
 
759
- useLayoutEffect(() => {
760
- if (!active && badgeRef.current) {
761
- badgeRef.current.style.display = 'none'
762
- }
763
- }, [active])
764
-
765
- if (!active) return null
773
+ if (!active || !position) return null
766
774
 
767
775
  return (
768
776
  <div
769
- className="pointer-events-none"
770
- ref={badgeRef}
771
- style={{ display: 'none', position: 'absolute', left: 0, top: 0 }}
777
+ className="pointer-events-none absolute z-40"
778
+ style={{ left: 0, top: 0, transform: `translate(${position.x}px, ${position.y}px)` }}
772
779
  >
773
- <PaintCursorBadge
774
- disabled={!hasMaterial}
775
- icon={icon}
776
- label={label}
777
- position={{ x: 0, y: 0 }}
778
- />
780
+ <PaintCursorBadge disabled={!hasMaterial} position={{ x: 0, y: 0 }} />
779
781
  </div>
780
782
  )
781
783
  }
@@ -786,16 +788,22 @@ function PaintCursorLayer({
786
788
  const ViewerCanvas = memo(function ViewerCanvas({
787
789
  isVersionPreviewMode,
788
790
  isLoading,
791
+ isFirstPersonMode,
792
+ isStudioMode,
789
793
  hasLoadedInitialScene,
790
794
  showLoader,
791
- isFirstPersonMode,
795
+ sceneReadyKey,
796
+ onSceneReadyChange,
792
797
  onThumbnailCapture,
793
798
  }: {
794
799
  isVersionPreviewMode: boolean
795
800
  isLoading: boolean
801
+ isFirstPersonMode: boolean
802
+ isStudioMode: boolean
796
803
  hasLoadedInitialScene: boolean
797
804
  showLoader: boolean
798
- isFirstPersonMode: boolean
805
+ sceneReadyKey: number
806
+ onSceneReadyChange: (ready: boolean) => void
799
807
  onThumbnailCapture?: (blob: Blob, cameraData: SnapshotCameraData) => void
800
808
  }) {
801
809
  const viewMode = useEditor((s) => s.viewMode)
@@ -897,19 +905,24 @@ const ViewerCanvas = memo(function ViewerCanvas({
897
905
  ) : null}
898
906
  <SelectionPersistenceManager enabled={hasLoadedInitialScene && !showLoader} />
899
907
  <Viewer
908
+ defaultRender={EDITOR_DEFAULT_RENDER}
900
909
  hoverStyles={EDITOR_HOVER_STYLES}
910
+ onSceneReadyChange={onSceneReadyChange}
911
+ renderContext="editor"
912
+ sceneReadyKey={sceneReadyKey}
901
913
  selectionManager={isFirstPersonMode ? 'default' : 'custom'}
902
914
  >
903
915
  <ViewerSceneContent
904
916
  isFirstPersonMode={isFirstPersonMode}
905
- isLoading={isLoading}
917
+ isLoading={showLoader}
918
+ isStudioMode={isStudioMode}
906
919
  isVersionPreviewMode={isVersionPreviewMode}
907
920
  onThumbnailCapture={onThumbnailCapture}
908
921
  />
909
922
  </Viewer>
910
923
  </div>
911
924
  </div>
912
- {!(isLoading || isVersionPreviewMode) && <ZoneLabelEditorSystem />}
925
+ {!(showLoader || isVersionPreviewMode) && <ZoneLabelEditorSystem />}
913
926
  </ErrorBoundary>
914
927
  )
915
928
  })
@@ -922,6 +935,7 @@ export default function Editor({
922
935
  sidebarTabs,
923
936
  viewerToolbarLeft,
924
937
  viewerToolbarRight,
938
+ inspectorFooter,
925
939
  projectId,
926
940
  onLoad,
927
941
  onSave,
@@ -936,11 +950,13 @@ export default function Editor({
936
950
  settingsPanelProps,
937
951
  sitePanelProps,
938
952
  extraSidebarPanels,
939
- presetsAdapter,
940
953
  commandPaletteEmptyAction,
941
954
  }: EditorProps) {
942
955
  const isFirstPersonMode = useEditor((s) => s.isFirstPersonMode)
943
- useKeyboard({ isVersionPreviewMode, disabled: isFirstPersonMode })
956
+ const isStudioMode = useEditor((s) => s.workspaceMode === 'studio')
957
+
958
+ useKeyboard({ isVersionPreviewMode, disabled: isFirstPersonMode || isStudioMode })
959
+
944
960
  const { isLoadingSceneRef } = useAutoSave({
945
961
  onSave,
946
962
  onDirty,
@@ -950,9 +966,10 @@ export default function Editor({
950
966
 
951
967
  const [isSceneLoading, setIsSceneLoading] = useState(false)
952
968
  const [hasLoadedInitialScene, setHasLoadedInitialScene] = useState(false)
969
+ const [sceneReadyKey, setSceneReadyKey] = useState(0)
970
+ const [isViewerSceneReady, setIsViewerSceneReady] = useState(false)
953
971
  const isPreviewMode = useEditor((s) => s.isPreviewMode)
954
- const firstPersonPreviousLevelRef = useRef(useViewer.getState().selection.levelId)
955
- const wasFirstPersonModeRef = useRef(isFirstPersonMode)
972
+ const isCaptureMode = useEditor((s) => s.isCaptureMode)
956
973
 
957
974
  const sidebarWidth = useSidebarStore((s) => s.width)
958
975
  const isSidebarCollapsed = useSidebarStore((s) => s.isCollapsed)
@@ -970,39 +987,6 @@ export default function Editor({
970
987
  }
971
988
  }, [projectId])
972
989
 
973
- useEffect(() => {
974
- const wasFirstPersonMode = wasFirstPersonModeRef.current
975
- wasFirstPersonModeRef.current = isFirstPersonMode
976
-
977
- if (isFirstPersonMode && !wasFirstPersonMode) {
978
- const viewer = useViewer.getState()
979
- firstPersonPreviousLevelRef.current = viewer.selection.levelId
980
- viewer.setCameraMode('perspective')
981
- viewer.setWallMode('up')
982
- viewer.setWalkthroughMode(true)
983
- viewer.setSelection({ selectedIds: [], zoneId: null })
984
- return
985
- }
986
-
987
- if (!(wasFirstPersonMode && !isFirstPersonMode)) return
988
-
989
- const viewer = useViewer.getState()
990
- const previousLevelId = firstPersonPreviousLevelRef.current
991
- firstPersonPreviousLevelRef.current = null
992
- viewer.setWalkthroughMode(false)
993
-
994
- if (!previousLevelId) return
995
-
996
- const previousLevelNode = useScene.getState().nodes[previousLevelId]
997
- if (previousLevelNode?.type === 'level') {
998
- viewer.setSelection({
999
- levelId: previousLevelId,
1000
- zoneId: null,
1001
- selectedIds: [],
1002
- })
1003
- }
1004
- }, [isFirstPersonMode])
1005
-
1006
990
  // Load scene on mount (or when onLoad identity changes, e.g. project switch)
1007
991
  useEffect(() => {
1008
992
  let cancelled = false
@@ -1010,15 +994,24 @@ export default function Editor({
1010
994
  async function load() {
1011
995
  isLoadingSceneRef.current = true
1012
996
  setHasLoadedInitialScene(false)
997
+ setIsViewerSceneReady(false)
1013
998
  setIsSceneLoading(true)
999
+ useScene.getState().unloadScene()
1000
+ useViewer.getState().resetSelection()
1014
1001
 
1015
1002
  try {
1016
1003
  const sceneGraph = onLoad ? await onLoad() : loadSceneFromLocalStorage()
1017
1004
  if (!cancelled) {
1018
1005
  applySceneGraphToEditor(sceneGraph)
1006
+ setIsViewerSceneReady(false)
1007
+ setSceneReadyKey((key) => key + 1)
1019
1008
  }
1020
1009
  } catch {
1021
- if (!cancelled) applySceneGraphToEditor(null)
1010
+ if (!cancelled) {
1011
+ applySceneGraphToEditor(null)
1012
+ setIsViewerSceneReady(false)
1013
+ setSceneReadyKey((key) => key + 1)
1014
+ }
1022
1015
  } finally {
1023
1016
  if (!cancelled) {
1024
1017
  setIsSceneLoading(false)
@@ -1062,10 +1055,55 @@ export default function Editor({
1062
1055
  }
1063
1056
  }, [])
1064
1057
 
1065
- const showLoader = isLoading || isSceneLoading
1058
+ const handleSceneReadyChange = useCallback((ready: boolean) => {
1059
+ setIsViewerSceneReady(ready)
1060
+ }, [])
1061
+
1062
+ const showLoader = isLoading || isSceneLoading || !hasLoadedInitialScene || !isViewerSceneReady
1063
+
1064
+ const firstPersonPreviousLevelRef = useRef(useViewer.getState().selection.levelId)
1065
+ const wasFirstPersonModeRef = useRef(isFirstPersonMode)
1066
+
1067
+ useEffect(() => {
1068
+ const wasFirstPersonMode = wasFirstPersonModeRef.current
1069
+ wasFirstPersonModeRef.current = isFirstPersonMode
1070
+
1071
+ if (isFirstPersonMode && !wasFirstPersonMode) {
1072
+ const viewer = useViewer.getState()
1073
+ firstPersonPreviousLevelRef.current = viewer.selection.levelId
1074
+ viewer.setCameraMode('perspective')
1075
+ viewer.setWallMode('up')
1076
+ viewer.setWalkthroughMode(true)
1077
+ viewer.setSelection({ selectedIds: [], zoneId: null })
1078
+ return
1079
+ }
1080
+
1081
+ if (!(wasFirstPersonMode && !isFirstPersonMode)) return
1082
+
1083
+ const viewer = useViewer.getState()
1084
+ const previousLevelId = firstPersonPreviousLevelRef.current
1085
+ firstPersonPreviousLevelRef.current = null
1086
+ viewer.setWalkthroughMode(false)
1087
+
1088
+ if (!previousLevelId) return
1089
+
1090
+ const previousLevelNode = useScene.getState().nodes[previousLevelId]
1091
+ if (previousLevelNode?.type === 'level') {
1092
+ viewer.setSelection({
1093
+ levelId: previousLevelId,
1094
+ zoneId: null,
1095
+ selectedIds: [],
1096
+ })
1097
+ }
1098
+ }, [isFirstPersonMode])
1066
1099
 
1067
1100
  const previewViewerContent = (
1068
- <Viewer hoverStyles={EDITOR_HOVER_STYLES} selectionManager="default">
1101
+ <Viewer
1102
+ defaultRender={EDITOR_DEFAULT_RENDER}
1103
+ hoverStyles={EDITOR_HOVER_STYLES}
1104
+ renderContext="editor"
1105
+ selectionManager="default"
1106
+ >
1069
1107
  <ExportManager />
1070
1108
  <ViewerZoneSystem />
1071
1109
  <CeilingSystem />
@@ -1073,7 +1111,6 @@ export default function Editor({
1073
1111
  <StairEditSystem />
1074
1112
  <CustomCameraControls />
1075
1113
  <ThumbnailGenerator onThumbnailCapture={onThumbnailCapture} />
1076
- <PresetThumbnailGenerator />
1077
1114
  <InteractiveSystem />
1078
1115
  </Viewer>
1079
1116
  )
@@ -1083,8 +1120,11 @@ export default function Editor({
1083
1120
  hasLoadedInitialScene={hasLoadedInitialScene}
1084
1121
  isFirstPersonMode={isFirstPersonMode}
1085
1122
  isLoading={isLoading}
1123
+ isStudioMode={isStudioMode}
1086
1124
  isVersionPreviewMode={isVersionPreviewMode}
1125
+ onSceneReadyChange={handleSceneReadyChange}
1087
1126
  onThumbnailCapture={onThumbnailCapture}
1127
+ sceneReadyKey={sceneReadyKey}
1088
1128
  showLoader={showLoader}
1089
1129
  />
1090
1130
  )
@@ -1109,18 +1149,19 @@ export default function Editor({
1109
1149
  }
1110
1150
 
1111
1151
  const tabBarTabs =
1112
- sidebarTabs?.map(({ id, label, mobileDefaultSnap, mobileIcon }) => ({
1152
+ sidebarTabs?.map(({ id, label, mobileDefaultSnap, mobileIcon, icon }) => ({
1113
1153
  id,
1114
1154
  label,
1115
1155
  mobileDefaultSnap,
1116
1156
  mobileIcon,
1157
+ icon,
1117
1158
  })) ?? []
1118
1159
 
1119
1160
  return (
1120
- <PresetsProvider adapter={presetsAdapter}>
1161
+ <>
1121
1162
  {showLoader && (
1122
1163
  <div className="fixed inset-0 z-60">
1123
- <SceneLoader />
1164
+ <SceneLoader className="bg-background" />
1124
1165
  </div>
1125
1166
  )}
1126
1167
 
@@ -1135,21 +1176,29 @@ export default function Editor({
1135
1176
  navbarSlot={navbarSlot}
1136
1177
  overlays={
1137
1178
  <>
1138
- <FloatingLevelSelector />
1139
- {!isVersionPreviewMode && (
1179
+ {!isCaptureMode && <FloatingLevelSelector />}
1180
+ {!(isVersionPreviewMode || isCaptureMode || isStudioMode) && (
1140
1181
  <div className="pointer-events-auto">
1141
1182
  <ActionMenu />
1142
1183
  </div>
1143
1184
  )}
1144
- {!isVersionPreviewMode && (
1185
+ {!(isVersionPreviewMode || isCaptureMode || isStudioMode) && (
1186
+ <div className="pointer-events-auto">
1187
+ <PanelManager inspectorFooter={inspectorFooter} />
1188
+ </div>
1189
+ )}
1190
+ {!isCaptureMode && (
1145
1191
  <div className="pointer-events-auto">
1146
- <PanelManager />
1192
+ <HelperManager />
1147
1193
  </div>
1148
1194
  )}
1149
- <div className="pointer-events-auto">
1150
- <HelperManager />
1151
- </div>
1195
+ {isFirstPersonMode && (
1196
+ <FirstPersonOverlay
1197
+ onExit={() => useEditor.getState().setFirstPersonMode(false)}
1198
+ />
1199
+ )}
1152
1200
  {viewerBanner}
1201
+ {projectId ? <SnapshotCaptureOverlay projectId={projectId} /> : null}
1153
1202
  </>
1154
1203
  }
1155
1204
  renderTabContent={renderTabContent}
@@ -1159,17 +1208,11 @@ export default function Editor({
1159
1208
  viewerToolbarLeft={viewerToolbarLeft}
1160
1209
  viewerToolbarRight={viewerToolbarRight}
1161
1210
  />
1162
- {/* First-person overlay — rendered on top of normal layout */}
1163
- {isFirstPersonMode && (
1164
- <div className="pointer-events-none fixed inset-0 z-50">
1165
- <FirstPersonOverlay onExit={() => useEditor.getState().setFirstPersonMode(false)} />
1166
- </div>
1167
- )}
1168
1211
  <EditorCommands />
1169
1212
  <CommandPalette emptyAction={commandPaletteEmptyAction} />
1170
1213
  </>
1171
1214
  )}
1172
- </PresetsProvider>
1215
+ </>
1173
1216
  )
1174
1217
  }
1175
1218
 
@@ -1180,51 +1223,52 @@ export default function Editor({
1180
1223
  const overlayLeft = LAYOUT_PADDING + (isSidebarCollapsed ? 8 : sidebarWidth) + LAYOUT_GAP
1181
1224
 
1182
1225
  return (
1183
- <PresetsProvider adapter={presetsAdapter}>
1184
- <div className="dark flex h-full w-full gap-3 bg-neutral-100 p-3 text-foreground">
1185
- {showLoader && (
1186
- <div className="fixed inset-0 z-60">
1187
- <SceneLoader />
1188
- </div>
1189
- )}
1226
+ <div className="dark flex h-full w-full gap-3 bg-neutral-100 p-3 text-foreground">
1227
+ {showLoader && (
1228
+ <div className="fixed inset-0 z-60">
1229
+ <SceneLoader className="bg-background" />
1230
+ </div>
1231
+ )}
1190
1232
 
1191
- {!isLoading && isPreviewMode ? (
1192
- <>
1193
- <ViewerOverlay onBack={() => useEditor.getState().setPreviewMode(false)} />
1194
- <div className="h-full w-full">{previewViewerContent}</div>
1195
- </>
1196
- ) : (
1197
- <>
1198
- {/* Sidebar */}
1199
- <SidebarSlot>
1200
- <AppSidebar
1201
- appMenuButton={appMenuButton}
1202
- commandPaletteEmptyAction={commandPaletteEmptyAction}
1203
- extraPanels={extraSidebarPanels}
1204
- settingsPanelProps={settingsPanelProps}
1205
- sidebarTop={sidebarTop}
1206
- sitePanelProps={sitePanelProps}
1207
- />
1208
- </SidebarSlot>
1209
-
1210
- {/* Viewer area */}
1211
- <div className="relative flex-1 overflow-hidden rounded-xl">{viewerCanvas}</div>
1212
-
1213
- {/* Fixed UI overlays scoped to the viewer area */}
1214
- <ViewerOverlays left={overlayLeft}>
1215
- <div className="pointer-events-auto">
1216
- <ActionMenu />
1217
- </div>
1218
- <div className="pointer-events-auto">
1219
- <PanelManager />
1220
- </div>
1221
- <div className="pointer-events-auto">
1222
- <HelperManager />
1223
- </div>
1224
- </ViewerOverlays>
1225
- </>
1226
- )}
1227
- </div>
1228
- </PresetsProvider>
1233
+ {!isLoading && isPreviewMode ? (
1234
+ <>
1235
+ <ViewerOverlay onBack={() => useEditor.getState().setPreviewMode(false)} />
1236
+ <div className="h-full w-full">{previewViewerContent}</div>
1237
+ </>
1238
+ ) : (
1239
+ <>
1240
+ {/* Sidebar */}
1241
+ <SidebarSlot>
1242
+ <AppSidebar
1243
+ appMenuButton={appMenuButton}
1244
+ commandPaletteEmptyAction={commandPaletteEmptyAction}
1245
+ extraPanels={extraSidebarPanels}
1246
+ settingsPanelProps={settingsPanelProps}
1247
+ sidebarTop={sidebarTop}
1248
+ sitePanelProps={sitePanelProps}
1249
+ />
1250
+ </SidebarSlot>
1251
+
1252
+ {/* Viewer area */}
1253
+ <div className="relative flex-1 overflow-hidden rounded-xl">{viewerCanvas}</div>
1254
+
1255
+ {/* Fixed UI overlays scoped to the viewer area */}
1256
+ <ViewerOverlays left={overlayLeft}>
1257
+ <div className="pointer-events-auto">
1258
+ <ActionMenu />
1259
+ </div>
1260
+ <div className="pointer-events-auto">
1261
+ <PanelManager />
1262
+ </div>
1263
+ <div className="pointer-events-auto">
1264
+ <HelperManager />
1265
+ </div>
1266
+ {isFirstPersonMode && (
1267
+ <FirstPersonOverlay onExit={() => useEditor.getState().setFirstPersonMode(false)} />
1268
+ )}
1269
+ </ViewerOverlays>
1270
+ </>
1271
+ )}
1272
+ </div>
1229
1273
  )
1230
1274
  }