@pascal-app/editor 0.8.0 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (223) hide show
  1. package/package.json +8 -9
  2. package/src/components/editor/alignment-3d-guide-layer.tsx +160 -0
  3. package/src/components/editor/custom-camera-controls.tsx +754 -25
  4. package/src/components/editor/editor-layout-mobile.tsx +3 -3
  5. package/src/components/editor/editor-layout-v2.tsx +58 -51
  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 +201 -83
  9. package/src/components/editor/first-person/bvh-ecctrl.tsx +8 -8
  10. package/src/components/editor/first-person-controls.tsx +825 -20
  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 +5829 -12005
  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 +178 -149
  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 +433 -156
  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 +136 -62
  28. package/src/components/editor/thumbnail-generator.tsx +9 -3
  29. package/src/components/editor/use-floorplan-background-placement.ts +119 -32
  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-geometry-renderer.tsx +266 -0
  45. package/src/components/editor-2d/renderers/floorplan-measurements-layer.tsx +108 -66
  46. package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +59 -0
  47. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +1963 -0
  48. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +24 -9
  49. package/src/components/editor-2d/svg-paths.ts +10 -2
  50. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +26 -3
  51. package/src/components/systems/ceiling/ceiling-system.tsx +12 -8
  52. package/src/components/systems/roof/roof-edit-system.tsx +86 -12
  53. package/src/components/systems/stair/stair-edit-system.tsx +1 -1
  54. package/src/components/tools/elevator/elevator-defaults.ts +8 -0
  55. package/src/components/tools/elevator/elevator-tool.tsx +349 -0
  56. package/src/components/tools/elevator/move-elevator-tool.tsx +252 -0
  57. package/src/components/tools/fence/fence-drafting.ts +64 -22
  58. package/src/components/tools/item/move-tool.tsx +41 -105
  59. package/src/components/tools/item/placement-math.test.ts +10 -0
  60. package/src/components/tools/item/placement-math.ts +31 -5
  61. package/src/components/tools/item/placement-strategies.ts +190 -15
  62. package/src/components/tools/item/placement-types.ts +10 -1
  63. package/src/components/tools/item/use-draft-node.ts +20 -1
  64. package/src/components/tools/item/use-placement-coordinator.tsx +694 -191
  65. package/src/components/tools/registry/move-registry-node-tool.tsx +602 -0
  66. package/src/components/tools/roof/roof-tool.tsx +108 -12
  67. package/src/components/tools/select/box-select-state.ts +77 -0
  68. package/src/components/tools/select/box-select-tool.tsx +324 -538
  69. package/src/components/tools/select/plane-box-select-tool.tsx +577 -0
  70. package/src/components/tools/select/screen-rectangle-selection.ts +84 -0
  71. package/src/components/tools/select/select-candidates.test.ts +109 -0
  72. package/src/components/tools/select/select-candidates.ts +85 -0
  73. package/src/components/tools/shared/affordance-dispatch.ts +30 -0
  74. package/src/components/tools/shared/drag-bounding-box.tsx +171 -0
  75. package/src/components/tools/shared/floor-stack-preview.ts +25 -0
  76. package/src/components/tools/shared/fresh-placement-visibility.ts +61 -0
  77. package/src/components/tools/shared/placement-box-geometry.ts +123 -0
  78. package/src/components/tools/shared/placement-box.tsx +123 -0
  79. package/src/components/tools/shared/polygon-editor.tsx +617 -167
  80. package/src/components/tools/shared/segment-angle.ts +56 -0
  81. package/src/components/tools/site/site-boundary-editor.tsx +464 -12
  82. package/src/components/tools/site/site-flag-model.ts +18 -0
  83. package/src/components/tools/stair/stair-defaults.ts +1 -1
  84. package/src/components/tools/stair/stair-tool.tsx +269 -35
  85. package/src/components/tools/tool-manager.tsx +172 -56
  86. package/src/components/tools/wall/wall-drafting.ts +98 -98
  87. package/src/components/tools/wall/wall-snap-geometry.test.ts +79 -0
  88. package/src/components/tools/wall/wall-snap-geometry.ts +270 -0
  89. package/src/components/tools/zone/zone-tool.tsx +16 -6
  90. package/src/components/ui/action-menu/action-button.tsx +21 -1
  91. package/src/components/ui/action-menu/control-modes.tsx +5 -82
  92. package/src/components/ui/action-menu/index.tsx +1 -113
  93. package/src/components/ui/action-menu/structure-tools.tsx +13 -88
  94. package/src/components/ui/action-menu/view-toggles.tsx +243 -6
  95. package/src/components/ui/command-palette/editor-commands.tsx +18 -13
  96. package/src/components/ui/command-palette/index.tsx +2 -1
  97. package/src/components/ui/controls/material-paint-panel.tsx +83 -0
  98. package/src/components/ui/controls/material-picker.tsx +11 -32
  99. package/src/components/ui/controls/metric-control.tsx +33 -11
  100. package/src/components/ui/floating-level-selector.tsx +47 -10
  101. package/src/components/ui/helpers/helper-manager.tsx +16 -17
  102. package/src/components/ui/helpers/registered-tool-helper.tsx +25 -0
  103. package/src/components/ui/item-catalog/catalog-items.tsx +1 -0
  104. package/src/components/ui/item-catalog/item-catalog.tsx +3 -0
  105. package/src/components/ui/level-duplicate-dialog.tsx +2 -1
  106. package/src/components/ui/panels/node-display.ts +1 -0
  107. package/src/components/ui/panels/panel-manager.tsx +35 -47
  108. package/src/components/ui/panels/panel-wrapper.tsx +217 -12
  109. package/src/components/ui/panels/parametric-inspector.tsx +469 -0
  110. package/src/components/ui/primitives/dropdown-menu.tsx +12 -8
  111. package/src/components/ui/scene-loader.tsx +1 -3
  112. package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +263 -0
  113. package/src/components/ui/sidebar/panels/items-panel/index.tsx +82 -8
  114. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +59 -9
  115. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +6 -1
  116. package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +259 -0
  117. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +3 -1
  118. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +5 -2
  119. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +83 -0
  120. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +89 -0
  121. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +75 -0
  122. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +2 -2
  123. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +83 -0
  124. package/src/components/ui/sidebar/panels/site-panel/index.tsx +4 -6
  125. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +2 -1
  126. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +93 -0
  127. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +41 -5
  128. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +128 -0
  129. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +5 -2
  130. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +83 -0
  131. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +7 -1
  132. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +68 -36
  133. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +51 -2
  134. package/src/components/ui/sidebar/tab-bar.tsx +61 -1
  135. package/src/components/viewer-overlay.tsx +169 -52
  136. package/src/components/viewer-zone-system.tsx +1 -1
  137. package/src/hooks/use-auto-save.ts +10 -3
  138. package/src/hooks/use-ceiling-events.ts +175 -0
  139. package/src/hooks/use-drag-action.ts +124 -0
  140. package/src/hooks/use-keyboard.ts +115 -62
  141. package/src/hooks/use-selection.ts +64 -0
  142. package/src/index.tsx +237 -5
  143. package/src/lib/constants.ts +6 -2
  144. package/src/lib/editor-api.ts +62 -0
  145. package/src/lib/elevator-support.ts +96 -0
  146. package/src/lib/floorplan/apply-alignment.ts +111 -0
  147. package/src/lib/floorplan/index.ts +7 -0
  148. package/src/lib/floorplan/selection-tool.ts +40 -0
  149. package/src/lib/floorplan/stairs.ts +12 -1
  150. package/src/lib/fresh-planar-placement.test.ts +102 -0
  151. package/src/lib/fresh-planar-placement.ts +63 -0
  152. package/src/lib/history.ts +2 -1
  153. package/src/lib/level-duplication.ts +54 -42
  154. package/src/lib/level-name.ts +11 -0
  155. package/src/lib/material-paint.ts +211 -19
  156. package/src/lib/placement-metadata.ts +29 -0
  157. package/src/lib/planar-cursor-placement.test.ts +43 -0
  158. package/src/lib/planar-cursor-placement.ts +42 -0
  159. package/src/lib/roof-duplication.ts +1 -1
  160. package/src/lib/scene-clipboard.ts +267 -0
  161. package/src/lib/scene.ts +13 -4
  162. package/src/lib/sfx-bus.ts +23 -1
  163. package/src/lib/sfx-player.ts +70 -22
  164. package/src/lib/site-boundary.ts +1 -0
  165. package/src/lib/stair-levels.test.ts +146 -0
  166. package/src/lib/stair-levels.ts +177 -0
  167. package/src/lib/world-grid-snap.ts +199 -0
  168. package/src/lib/zone-content.ts +130 -0
  169. package/src/store/use-alignment-guides.ts +21 -0
  170. package/src/store/use-editor.tsx +320 -37
  171. package/src/store/use-placement-preview.ts +31 -0
  172. package/src/store/use-wall-move-ghosts.ts +36 -0
  173. package/src/store/use-wall-snap-indicator.ts +32 -0
  174. package/src/components/editor/preset-thumbnail-generator.tsx +0 -125
  175. package/src/components/editor-2d/renderers/floorplan-roof-layer.tsx +0 -113
  176. package/src/components/tools/building/move-building-tool.tsx +0 -157
  177. package/src/components/tools/ceiling/ceiling-boundary-editor.tsx +0 -43
  178. package/src/components/tools/ceiling/ceiling-hole-editor.tsx +0 -49
  179. package/src/components/tools/ceiling/ceiling-tool.tsx +0 -465
  180. package/src/components/tools/ceiling/move-ceiling-tool.tsx +0 -264
  181. package/src/components/tools/column/column-tool.tsx +0 -97
  182. package/src/components/tools/column/move-column-tool.tsx +0 -105
  183. package/src/components/tools/door/door-math.ts +0 -110
  184. package/src/components/tools/door/door-tool.tsx +0 -324
  185. package/src/components/tools/door/move-door-tool.tsx +0 -412
  186. package/src/components/tools/fence/curve-fence-tool.tsx +0 -178
  187. package/src/components/tools/fence/fence-tool.tsx +0 -346
  188. package/src/components/tools/fence/move-fence-endpoint-tool.tsx +0 -441
  189. package/src/components/tools/fence/move-fence-tool.tsx +0 -302
  190. package/src/components/tools/item/item-tool.tsx +0 -26
  191. package/src/components/tools/roof/move-roof-tool.tsx +0 -364
  192. package/src/components/tools/slab/move-slab-tool.tsx +0 -182
  193. package/src/components/tools/slab/slab-boundary-editor.tsx +0 -43
  194. package/src/components/tools/slab/slab-hole-editor.tsx +0 -49
  195. package/src/components/tools/slab/slab-tool.tsx +0 -322
  196. package/src/components/tools/spawn/move-spawn-tool.tsx +0 -101
  197. package/src/components/tools/spawn/spawn-tool.tsx +0 -130
  198. package/src/components/tools/wall/curve-wall-tool.tsx +0 -178
  199. package/src/components/tools/wall/move-wall-endpoint-tool.tsx +0 -426
  200. package/src/components/tools/wall/move-wall-tool.tsx +0 -358
  201. package/src/components/tools/wall/wall-tool.tsx +0 -332
  202. package/src/components/tools/window/move-window-tool.tsx +0 -447
  203. package/src/components/tools/window/window-math.ts +0 -117
  204. package/src/components/tools/window/window-tool.tsx +0 -332
  205. package/src/components/ui/helpers/ceiling-helper.tsx +0 -20
  206. package/src/components/ui/helpers/slab-helper.tsx +0 -20
  207. package/src/components/ui/helpers/wall-helper.tsx +0 -20
  208. package/src/components/ui/panels/ceiling-panel.tsx +0 -249
  209. package/src/components/ui/panels/column-panel.tsx +0 -759
  210. package/src/components/ui/panels/door-panel.tsx +0 -1299
  211. package/src/components/ui/panels/fence-panel.tsx +0 -222
  212. package/src/components/ui/panels/item-panel.tsx +0 -306
  213. package/src/components/ui/panels/presets/presets-popover.tsx +0 -511
  214. package/src/components/ui/panels/roof-panel.tsx +0 -280
  215. package/src/components/ui/panels/roof-segment-panel.tsx +0 -311
  216. package/src/components/ui/panels/slab-panel.tsx +0 -250
  217. package/src/components/ui/panels/spawn-panel.tsx +0 -161
  218. package/src/components/ui/panels/stair-panel.tsx +0 -577
  219. package/src/components/ui/panels/stair-segment-panel.tsx +0 -325
  220. package/src/components/ui/panels/wall-panel.tsx +0 -273
  221. package/src/components/ui/panels/window-panel.tsx +0 -968
  222. package/src/contexts/presets-context.tsx +0 -121
  223. package/src/hooks/use-contextual-tools.ts +0 -61
@@ -8,19 +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
- import { useAutoFrame } from '../../hooks/use-auto-frame'
24
14
  import { type SaveStatus, useAutoSave } from '../../hooks/use-auto-save'
25
15
  import { useKeyboard } from '../../hooks/use-keyboard'
26
16
  import {
@@ -62,18 +52,23 @@ import { FloatingActionMenu } from './floating-action-menu'
62
52
  import { FloatingBuildingActionMenu } from './floating-building-action-menu'
63
53
  import { FloorplanPanel } from './floorplan-panel'
64
54
  import { Grid } from './grid'
65
- 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'
66
58
  import { SelectionManager } from './selection-manager'
67
59
  import { SiteEdgeLabels } from './site-edge-labels'
60
+ import { SlabHoleHighlights } from './slab-hole-highlights'
68
61
  import { SnapshotCaptureOverlay } from './snapshot-capture-overlay'
69
62
  import { type SnapshotCameraData, ThumbnailGenerator } from './thumbnail-generator'
70
63
  import { WallMeasurementLabel } from './wall-measurement-label'
64
+ import { WallMoveSideHandles } from './wall-move-side-handles'
65
+ import { WallOpeningHighlights } from './wall-opening-highlights'
71
66
 
72
67
  const CAMERA_CONTROLS_HINT_DISMISSED_STORAGE_KEY = 'editor-camera-controls-hint-dismissed:v1'
73
68
  const DELETE_CURSOR_BADGE_COLOR = '#ef4444'
74
69
  const DELETE_CURSOR_BADGE_OFFSET_X = 14
75
70
  const DELETE_CURSOR_BADGE_OFFSET_Y = 14
76
- const PAINT_CURSOR_BADGE_COLOR = '#f59e0b'
71
+ const PAINT_CURSOR_BADGE_COLOR = '#818cf8'
77
72
  const PAINT_CURSOR_BADGE_DISABLED_COLOR = '#94a3b8'
78
73
  const PAINT_CURSOR_BADGE_OFFSET_X = 14
79
74
  const PAINT_CURSOR_BADGE_OFFSET_Y = 14
@@ -88,6 +83,7 @@ const EDITOR_HOVER_STYLES: HoverStyles = {
88
83
  pulse: false,
89
84
  },
90
85
  }
86
+ const EDITOR_DEFAULT_RENDER = { shading: 'solid' } as const
91
87
 
92
88
  /**
93
89
  * Wire up module-level singletons (spatial grid, space detection, SFX) for
@@ -125,12 +121,18 @@ export interface EditorProps {
125
121
  sidebarTabs?: (SidebarTab & { component: React.ComponentType })[]
126
122
  viewerToolbarLeft?: ReactNode
127
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
128
130
 
129
131
  projectId?: string | null
130
132
 
131
133
  // Persistence — defaults to localStorage when omitted
132
134
  onLoad?: () => Promise<SceneGraph | null>
133
- onSave?: (scene: SceneGraph) => Promise<void>
135
+ onSave?: (scene: SceneGraph, options?: { keepalive?: boolean }) => Promise<void>
134
136
  onDirty?: () => void
135
137
  onSaveStatusChange?: (status: SaveStatus) => void
136
138
 
@@ -153,9 +155,6 @@ export interface EditorProps {
153
155
  sitePanelProps?: SitePanelProps
154
156
  extraSidebarPanels?: ExtraPanel[]
155
157
 
156
- // Presets storage backend (defaults to localStorage)
157
- presetsAdapter?: PresetsAdapter
158
-
159
158
  // Command palette fallback when no commands match
160
159
  commandPaletteEmptyAction?: CommandPaletteEmptyAction
161
160
  }
@@ -334,6 +333,7 @@ const EDITOR_CAMERA_CONTROL_HINTS: CameraControlHint[] = [
334
333
  {
335
334
  action: 'Pan',
336
335
  keys: [{ value: 'Space' }, { value: 'Left click' }],
336
+ alternativeKeys: [{ value: 'Middle click' }],
337
337
  },
338
338
  { action: 'Rotate', keys: [{ value: 'Right click' }] },
339
339
  { action: 'Zoom', keys: [{ value: 'Scroll' }] },
@@ -528,67 +528,89 @@ function DeleteCursorBadge({ position }: { position: { x: number; y: number } })
528
528
 
529
529
  function PaintCursorBadge({
530
530
  position,
531
- label,
532
531
  disabled,
533
- icon,
534
532
  }: {
535
533
  position: { x: number; y: number }
536
- label: string
537
534
  disabled: boolean
538
- icon: string
539
535
  }) {
540
536
  const accentColor = disabled ? PAINT_CURSOR_BADGE_DISABLED_COLOR : PAINT_CURSOR_BADGE_COLOR
537
+ const lineHeight = 18
541
538
 
542
539
  return (
543
540
  <div
544
541
  aria-hidden="true"
545
542
  className="pointer-events-none absolute z-40"
546
543
  style={{
547
- left: position.x + PAINT_CURSOR_BADGE_OFFSET_X,
548
- top: position.y + PAINT_CURSOR_BADGE_OFFSET_Y,
544
+ left: position.x,
545
+ top: position.y,
549
546
  }}
550
547
  >
551
548
  <div
552
- 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)]"
553
559
  style={{
554
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))`,
555
562
  }}
556
563
  >
557
- <Icon
564
+ {/* eslint-disable-next-line @next/next/no-img-element */}
565
+ <img
566
+ alt=""
558
567
  aria-hidden="true"
559
- className="drop-shadow-[0_2px_4px_rgba(0,0,0,0.5)]"
560
- color={accentColor}
561
- height={16}
562
- icon={icon}
563
- 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"
564
570
  />
565
- <span className="font-medium text-[11px]" style={{ color: accentColor }}>
566
- {label}
567
- </span>
568
571
  </div>
569
572
  </div>
570
573
  )
571
574
  }
572
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
+
573
584
  // ── Viewer scene content: memoized so <Viewer> doesn't re-render on mode/viewMode changes ──
574
585
 
575
586
  const ViewerSceneContent = memo(function ViewerSceneContent({
576
587
  isVersionPreviewMode,
577
588
  isLoading,
578
589
  isFirstPersonMode,
590
+ isStudioMode,
579
591
  onThumbnailCapture,
580
592
  }: {
581
593
  isVersionPreviewMode: boolean
582
594
  isLoading: boolean
583
595
  isFirstPersonMode: boolean
596
+ isStudioMode: boolean
584
597
  onThumbnailCapture?: (blob: Blob, cameraData: SnapshotCameraData) => void
585
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
586
602
  return (
587
603
  <>
588
- {!isFirstPersonMode && <SelectionManager />}
589
- {!(isVersionPreviewMode || isFirstPersonMode) && <BoxSelectTool />}
590
- {!(isVersionPreviewMode || isFirstPersonMode) && <FloatingActionMenu />}
591
- {!(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 />}
592
614
  {!isFirstPersonMode && <WallMeasurementLabel />}
593
615
  <ExportManager />
594
616
  {isFirstPersonMode ? <ViewerZoneSystem /> : <ZoneSystem />}
@@ -596,16 +618,13 @@ const ViewerSceneContent = memo(function ViewerSceneContent({
596
618
  <CeilingSelectionAffordanceSystem />
597
619
  <RoofEditSystem />
598
620
  <StairEditSystem />
599
- {!(isLoading || isFirstPersonMode) && (
600
- <Grid cellColor="#aaa" fadeDistance={500} sectionColor="#ccc" />
601
- )}
602
- {!(isLoading || isVersionPreviewMode || isFirstPersonMode) && <ToolManager />}
621
+ {!(isLoading || isFirstPersonMode) && <SnapAwareGrid />}
622
+ {!(isLoading || noEditing) && <ToolManager />}
603
623
  {isFirstPersonMode && <FirstPersonControls />}
604
624
  <CustomCameraControls />
605
625
  <ThumbnailGenerator onThumbnailCapture={onThumbnailCapture} />
606
- <PresetThumbnailGenerator />
607
626
  {!isFirstPersonMode && <SiteEdgeLabels />}
608
- {isFirstPersonMode && <InteractiveSystem />}
627
+ <InteractiveSystem />
609
628
  </>
610
629
  )
611
630
  })
@@ -678,7 +697,7 @@ function DeleteCursorLayer({
678
697
 
679
698
  return (
680
699
  <div
681
- className="pointer-events-none"
700
+ className="pointer-events-none z-40"
682
701
  ref={badgeRef}
683
702
  style={{ display: 'none', position: 'absolute', left: 0, top: 0 }}
684
703
  >
@@ -696,15 +715,12 @@ function PaintCursorLayer({
696
715
  }) {
697
716
  const mode = useEditor((s) => s.mode)
698
717
  const activePaintMaterial = useEditor((s) => s.activePaintMaterial)
699
- const activePaintTarget = useEditor((s) => s.activePaintTarget)
700
- const badgeRef = useRef<HTMLDivElement>(null)
718
+ const [position, setPosition] = useState<{ x: number; y: number } | null>(null)
701
719
  const active = mode === 'material-paint' && !isVersionPreviewMode
702
720
 
703
721
  useEffect(() => {
704
722
  if (!active) {
705
- if (badgeRef.current) {
706
- badgeRef.current.style.display = 'none'
707
- }
723
+ setPosition(null)
708
724
  return
709
725
  }
710
726
  const el = containerRef.current
@@ -712,20 +728,16 @@ function PaintCursorLayer({
712
728
  let frame = 0
713
729
  let nextX = 0
714
730
  let nextY = 0
715
- const badge = badgeRef.current
716
731
 
717
732
  const flushPosition = () => {
718
733
  frame = 0
719
- if (!badge) return
720
- badge.style.display = 'block'
721
- badge.style.transform = `translate(${nextX + PAINT_CURSOR_BADGE_OFFSET_X}px, ${nextY + PAINT_CURSOR_BADGE_OFFSET_Y}px)`
734
+ setPosition({ x: nextX, y: nextY })
722
735
  }
723
736
 
724
- const onMove = (e: PointerEvent) => {
737
+ const updateFromEvent = (e: PointerEvent) => {
725
738
  const rect = el.getBoundingClientRect()
726
739
  nextX = e.clientX - rect.left
727
740
  nextY = e.clientY - rect.top
728
-
729
741
  if (frame === 0) {
730
742
  frame = window.requestAnimationFrame(flushPosition)
731
743
  }
@@ -735,17 +747,19 @@ function PaintCursorLayer({
735
747
  window.cancelAnimationFrame(frame)
736
748
  frame = 0
737
749
  }
738
- if (badge) {
739
- badge.style.display = 'none'
740
- }
750
+ setPosition(null)
741
751
  }
742
- el.addEventListener('pointermove', onMove)
752
+ el.addEventListener('pointermove', updateFromEvent)
753
+ el.addEventListener('pointerenter', updateFromEvent)
754
+ el.addEventListener('pointerdown', updateFromEvent)
743
755
  el.addEventListener('pointerleave', onLeave)
744
756
  return () => {
745
757
  if (frame !== 0) {
746
758
  window.cancelAnimationFrame(frame)
747
759
  }
748
- el.removeEventListener('pointermove', onMove)
760
+ el.removeEventListener('pointermove', updateFromEvent)
761
+ el.removeEventListener('pointerenter', updateFromEvent)
762
+ el.removeEventListener('pointerdown', updateFromEvent)
749
763
  el.removeEventListener('pointerleave', onLeave)
750
764
  }
751
765
  }, [active, containerRef])
@@ -755,29 +769,15 @@ function PaintCursorLayer({
755
769
  (activePaintMaterial.material !== undefined ||
756
770
  activePaintMaterial.materialPreset !== undefined),
757
771
  )
758
- const label = hasMaterial ? `Paint ${activePaintTarget}` : 'Choose material'
759
- const icon = 'mdi:format-color-fill'
760
772
 
761
- useLayoutEffect(() => {
762
- if (!active && badgeRef.current) {
763
- badgeRef.current.style.display = 'none'
764
- }
765
- }, [active])
766
-
767
- if (!active) return null
773
+ if (!active || !position) return null
768
774
 
769
775
  return (
770
776
  <div
771
- className="pointer-events-none"
772
- ref={badgeRef}
773
- 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)` }}
774
779
  >
775
- <PaintCursorBadge
776
- disabled={!hasMaterial}
777
- icon={icon}
778
- label={label}
779
- position={{ x: 0, y: 0 }}
780
- />
780
+ <PaintCursorBadge disabled={!hasMaterial} position={{ x: 0, y: 0 }} />
781
781
  </div>
782
782
  )
783
783
  }
@@ -789,15 +789,21 @@ const ViewerCanvas = memo(function ViewerCanvas({
789
789
  isVersionPreviewMode,
790
790
  isLoading,
791
791
  isFirstPersonMode,
792
+ isStudioMode,
792
793
  hasLoadedInitialScene,
793
794
  showLoader,
795
+ sceneReadyKey,
796
+ onSceneReadyChange,
794
797
  onThumbnailCapture,
795
798
  }: {
796
799
  isVersionPreviewMode: boolean
797
800
  isLoading: boolean
798
801
  isFirstPersonMode: boolean
802
+ isStudioMode: boolean
799
803
  hasLoadedInitialScene: boolean
800
804
  showLoader: boolean
805
+ sceneReadyKey: number
806
+ onSceneReadyChange: (ready: boolean) => void
801
807
  onThumbnailCapture?: (blob: Blob, cameraData: SnapshotCameraData) => void
802
808
  }) {
803
809
  const viewMode = useEditor((s) => s.viewMode)
@@ -839,7 +845,7 @@ const ViewerCanvas = memo(function ViewerCanvas({
839
845
  window.removeEventListener('pointermove', handlePointerMove)
840
846
  window.removeEventListener('pointerup', handlePointerUp)
841
847
  }
842
- }, [])
848
+ }, [setFloorplanPaneRatio])
843
849
 
844
850
  useEffect(() => {
845
851
  setIsCameraControlsHintVisible(!readCameraControlsHintDismissed())
@@ -899,19 +905,24 @@ const ViewerCanvas = memo(function ViewerCanvas({
899
905
  ) : null}
900
906
  <SelectionPersistenceManager enabled={hasLoadedInitialScene && !showLoader} />
901
907
  <Viewer
908
+ defaultRender={EDITOR_DEFAULT_RENDER}
902
909
  hoverStyles={EDITOR_HOVER_STYLES}
910
+ onSceneReadyChange={onSceneReadyChange}
911
+ renderContext="editor"
912
+ sceneReadyKey={sceneReadyKey}
903
913
  selectionManager={isFirstPersonMode ? 'default' : 'custom'}
904
914
  >
905
915
  <ViewerSceneContent
906
916
  isFirstPersonMode={isFirstPersonMode}
907
- isLoading={isLoading}
917
+ isLoading={showLoader}
918
+ isStudioMode={isStudioMode}
908
919
  isVersionPreviewMode={isVersionPreviewMode}
909
920
  onThumbnailCapture={onThumbnailCapture}
910
921
  />
911
922
  </Viewer>
912
923
  </div>
913
924
  </div>
914
- {!(isLoading || isVersionPreviewMode) && <ZoneLabelEditorSystem />}
925
+ {!(showLoader || isVersionPreviewMode) && <ZoneLabelEditorSystem />}
915
926
  </ErrorBoundary>
916
927
  )
917
928
  })
@@ -924,6 +935,7 @@ export default function Editor({
924
935
  sidebarTabs,
925
936
  viewerToolbarLeft,
926
937
  viewerToolbarRight,
938
+ inspectorFooter,
927
939
  projectId,
928
940
  onLoad,
929
941
  onSave,
@@ -938,12 +950,12 @@ export default function Editor({
938
950
  settingsPanelProps,
939
951
  sitePanelProps,
940
952
  extraSidebarPanels,
941
- presetsAdapter,
942
953
  commandPaletteEmptyAction,
943
954
  }: EditorProps) {
944
955
  const isFirstPersonMode = useEditor((s) => s.isFirstPersonMode)
956
+ const isStudioMode = useEditor((s) => s.workspaceMode === 'studio')
945
957
 
946
- useKeyboard({ isVersionPreviewMode, disabled: isFirstPersonMode })
958
+ useKeyboard({ isVersionPreviewMode, disabled: isFirstPersonMode || isStudioMode })
947
959
 
948
960
  const { isLoadingSceneRef } = useAutoSave({
949
961
  onSave,
@@ -954,6 +966,8 @@ export default function Editor({
954
966
 
955
967
  const [isSceneLoading, setIsSceneLoading] = useState(false)
956
968
  const [hasLoadedInitialScene, setHasLoadedInitialScene] = useState(false)
969
+ const [sceneReadyKey, setSceneReadyKey] = useState(0)
970
+ const [isViewerSceneReady, setIsViewerSceneReady] = useState(false)
957
971
  const isPreviewMode = useEditor((s) => s.isPreviewMode)
958
972
  const isCaptureMode = useEditor((s) => s.isCaptureMode)
959
973
 
@@ -980,15 +994,24 @@ export default function Editor({
980
994
  async function load() {
981
995
  isLoadingSceneRef.current = true
982
996
  setHasLoadedInitialScene(false)
997
+ setIsViewerSceneReady(false)
983
998
  setIsSceneLoading(true)
999
+ useScene.getState().unloadScene()
1000
+ useViewer.getState().resetSelection()
984
1001
 
985
1002
  try {
986
1003
  const sceneGraph = onLoad ? await onLoad() : loadSceneFromLocalStorage()
987
1004
  if (!cancelled) {
988
1005
  applySceneGraphToEditor(sceneGraph)
1006
+ setIsViewerSceneReady(false)
1007
+ setSceneReadyKey((key) => key + 1)
989
1008
  }
990
1009
  } catch {
991
- if (!cancelled) applySceneGraphToEditor(null)
1010
+ if (!cancelled) {
1011
+ applySceneGraphToEditor(null)
1012
+ setIsViewerSceneReady(false)
1013
+ setSceneReadyKey((key) => key + 1)
1014
+ }
992
1015
  } finally {
993
1016
  if (!cancelled) {
994
1017
  setIsSceneLoading(false)
@@ -1032,7 +1055,11 @@ export default function Editor({
1032
1055
  }
1033
1056
  }, [])
1034
1057
 
1035
- const showLoader = isLoading || isSceneLoading
1058
+ const handleSceneReadyChange = useCallback((ready: boolean) => {
1059
+ setIsViewerSceneReady(ready)
1060
+ }, [])
1061
+
1062
+ const showLoader = isLoading || isSceneLoading || !hasLoadedInitialScene || !isViewerSceneReady
1036
1063
 
1037
1064
  const firstPersonPreviousLevelRef = useRef(useViewer.getState().selection.levelId)
1038
1065
  const wasFirstPersonModeRef = useRef(isFirstPersonMode)
@@ -1071,7 +1098,12 @@ export default function Editor({
1071
1098
  }, [isFirstPersonMode])
1072
1099
 
1073
1100
  const previewViewerContent = (
1074
- <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
+ >
1075
1107
  <ExportManager />
1076
1108
  <ViewerZoneSystem />
1077
1109
  <CeilingSystem />
@@ -1079,7 +1111,6 @@ export default function Editor({
1079
1111
  <StairEditSystem />
1080
1112
  <CustomCameraControls />
1081
1113
  <ThumbnailGenerator onThumbnailCapture={onThumbnailCapture} />
1082
- <PresetThumbnailGenerator />
1083
1114
  <InteractiveSystem />
1084
1115
  </Viewer>
1085
1116
  )
@@ -1089,8 +1120,11 @@ export default function Editor({
1089
1120
  hasLoadedInitialScene={hasLoadedInitialScene}
1090
1121
  isFirstPersonMode={isFirstPersonMode}
1091
1122
  isLoading={isLoading}
1123
+ isStudioMode={isStudioMode}
1092
1124
  isVersionPreviewMode={isVersionPreviewMode}
1125
+ onSceneReadyChange={handleSceneReadyChange}
1093
1126
  onThumbnailCapture={onThumbnailCapture}
1127
+ sceneReadyKey={sceneReadyKey}
1094
1128
  showLoader={showLoader}
1095
1129
  />
1096
1130
  )
@@ -1115,18 +1149,19 @@ export default function Editor({
1115
1149
  }
1116
1150
 
1117
1151
  const tabBarTabs =
1118
- sidebarTabs?.map(({ id, label, mobileDefaultSnap, mobileIcon }) => ({
1152
+ sidebarTabs?.map(({ id, label, mobileDefaultSnap, mobileIcon, icon }) => ({
1119
1153
  id,
1120
1154
  label,
1121
1155
  mobileDefaultSnap,
1122
1156
  mobileIcon,
1157
+ icon,
1123
1158
  })) ?? []
1124
1159
 
1125
1160
  return (
1126
- <PresetsProvider adapter={presetsAdapter}>
1161
+ <>
1127
1162
  {showLoader && (
1128
1163
  <div className="fixed inset-0 z-60">
1129
- <SceneLoader />
1164
+ <SceneLoader className="bg-background" />
1130
1165
  </div>
1131
1166
  )}
1132
1167
 
@@ -1142,14 +1177,14 @@ export default function Editor({
1142
1177
  overlays={
1143
1178
  <>
1144
1179
  {!isCaptureMode && <FloatingLevelSelector />}
1145
- {!(isVersionPreviewMode || isCaptureMode) && (
1180
+ {!(isVersionPreviewMode || isCaptureMode || isStudioMode) && (
1146
1181
  <div className="pointer-events-auto">
1147
1182
  <ActionMenu />
1148
1183
  </div>
1149
1184
  )}
1150
- {!(isVersionPreviewMode || isCaptureMode) && (
1185
+ {!(isVersionPreviewMode || isCaptureMode || isStudioMode) && (
1151
1186
  <div className="pointer-events-auto">
1152
- <PanelManager />
1187
+ <PanelManager inspectorFooter={inspectorFooter} />
1153
1188
  </div>
1154
1189
  )}
1155
1190
  {!isCaptureMode && (
@@ -1157,6 +1192,11 @@ export default function Editor({
1157
1192
  <HelperManager />
1158
1193
  </div>
1159
1194
  )}
1195
+ {isFirstPersonMode && (
1196
+ <FirstPersonOverlay
1197
+ onExit={() => useEditor.getState().setFirstPersonMode(false)}
1198
+ />
1199
+ )}
1160
1200
  {viewerBanner}
1161
1201
  {projectId ? <SnapshotCaptureOverlay projectId={projectId} /> : null}
1162
1202
  </>
@@ -1170,15 +1210,9 @@ export default function Editor({
1170
1210
  />
1171
1211
  <EditorCommands />
1172
1212
  <CommandPalette emptyAction={commandPaletteEmptyAction} />
1173
- {/* First-person overlay — rendered on top of normal layout */}
1174
- {isFirstPersonMode && (
1175
- <div className="pointer-events-none fixed inset-0 z-50">
1176
- <FirstPersonOverlay onExit={() => useEditor.getState().setFirstPersonMode(false)} />
1177
- </div>
1178
- )}
1179
1213
  </>
1180
1214
  )}
1181
- </PresetsProvider>
1215
+ </>
1182
1216
  )
1183
1217
  }
1184
1218
 
@@ -1189,57 +1223,52 @@ export default function Editor({
1189
1223
  const overlayLeft = LAYOUT_PADDING + (isSidebarCollapsed ? 8 : sidebarWidth) + LAYOUT_GAP
1190
1224
 
1191
1225
  return (
1192
- <PresetsProvider adapter={presetsAdapter}>
1193
- <div className="dark flex h-full w-full gap-3 bg-neutral-100 p-3 text-foreground">
1194
- {showLoader && (
1195
- <div className="fixed inset-0 z-60">
1196
- <SceneLoader />
1197
- </div>
1198
- )}
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
+ )}
1199
1232
 
1200
- {!isLoading && isPreviewMode ? (
1201
- <>
1202
- <ViewerOverlay onBack={() => useEditor.getState().setPreviewMode(false)} />
1203
- <div className="h-full w-full">{previewViewerContent}</div>
1204
- </>
1205
- ) : (
1206
- <>
1207
- {/* Sidebar */}
1208
- <SidebarSlot>
1209
- <AppSidebar
1210
- appMenuButton={appMenuButton}
1211
- commandPaletteEmptyAction={commandPaletteEmptyAction}
1212
- extraPanels={extraSidebarPanels}
1213
- settingsPanelProps={settingsPanelProps}
1214
- sidebarTop={sidebarTop}
1215
- sitePanelProps={sitePanelProps}
1216
- />
1217
- </SidebarSlot>
1218
-
1219
- {/* Viewer area */}
1220
- <div className="relative flex-1 overflow-hidden rounded-xl">{viewerCanvas}</div>
1221
-
1222
- {/* Fixed UI overlays scoped to the viewer area */}
1223
- <ViewerOverlays left={overlayLeft}>
1224
- <div className="pointer-events-auto">
1225
- <ActionMenu />
1226
- </div>
1227
- <div className="pointer-events-auto">
1228
- <PanelManager />
1229
- </div>
1230
- <div className="pointer-events-auto">
1231
- <HelperManager />
1232
- </div>
1233
- </ViewerOverlays>
1234
- {/* First-person overlay — rendered on top of normal layout */}
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>
1235
1266
  {isFirstPersonMode && (
1236
- <div className="pointer-events-none fixed inset-0 z-50">
1237
- <FirstPersonOverlay onExit={() => useEditor.getState().setFirstPersonMode(false)} />
1238
- </div>
1267
+ <FirstPersonOverlay onExit={() => useEditor.getState().setFirstPersonMode(false)} />
1239
1268
  )}
1240
- </>
1241
- )}
1242
- </div>
1243
- </PresetsProvider>
1269
+ </ViewerOverlays>
1270
+ </>
1271
+ )}
1272
+ </div>
1244
1273
  )
1245
1274
  }