@pascal-app/editor 0.7.0 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (244) hide show
  1. package/package.json +12 -13
  2. package/src/components/editor/alignment-3d-guide-layer.tsx +160 -0
  3. package/src/components/editor/custom-camera-controls.tsx +755 -25
  4. package/src/components/editor/editor-layout-mobile.tsx +3 -3
  5. package/src/components/editor/editor-layout-v2.tsx +62 -54
  6. package/src/components/editor/export-manager.tsx +32 -3
  7. package/src/components/editor/first-person/build-collider-world.test.ts +149 -0
  8. package/src/components/editor/first-person/build-collider-world.ts +203 -87
  9. package/src/components/editor/first-person/bvh-ecctrl.tsx +127 -62
  10. package/src/components/editor/first-person-controls.tsx +836 -31
  11. package/src/components/editor/floating-action-menu.tsx +287 -196
  12. package/src/components/editor/floating-building-action-menu.tsx +1 -1
  13. package/src/components/editor/floorplan-panel.tsx +5837 -12006
  14. package/src/components/editor/grid.tsx +51 -17
  15. package/src/components/editor/group-move-handle.tsx +316 -0
  16. package/src/components/editor/group-rotate-handle.tsx +382 -0
  17. package/src/components/editor/group-transform-shared.test.ts +205 -0
  18. package/src/components/editor/group-transform-shared.ts +256 -0
  19. package/src/components/editor/handles/handle-arrow.tsx +523 -0
  20. package/src/components/editor/handles/use-handle-drag.ts +213 -0
  21. package/src/components/editor/index.tsx +234 -190
  22. package/src/components/editor/measurement-pill.tsx +75 -0
  23. package/src/components/editor/node-arrow-handles.tsx +1453 -0
  24. package/src/components/editor/selection-manager.tsx +435 -158
  25. package/src/components/editor/site-edge-labels.tsx +44 -20
  26. package/src/components/editor/slab-hole-highlights.tsx +466 -0
  27. package/src/components/editor/snapshot-capture-overlay.tsx +539 -0
  28. package/src/components/editor/thumbnail-generator.tsx +27 -64
  29. package/src/components/editor/use-floorplan-background-placement.ts +122 -35
  30. package/src/components/editor/use-floorplan-hit-testing.ts +24 -0
  31. package/src/components/editor/use-floorplan-scene-data.ts +14 -2
  32. package/src/components/editor/wall-measurement-label.tsx +11 -6
  33. package/src/components/editor/wall-move-side-handles.tsx +855 -0
  34. package/src/components/editor/wall-opening-highlights.tsx +181 -0
  35. package/src/components/editor/wall-snap-beacon-layer.tsx +150 -0
  36. package/src/components/editor-2d/floorplan-action-menu-layer.tsx +6 -1
  37. package/src/components/editor-2d/floorplan-alignment-guide-layer.tsx +142 -0
  38. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +5 -3
  39. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +227 -0
  40. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +616 -0
  41. package/src/components/editor-2d/floorplan-render-context.tsx +62 -0
  42. package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +75 -0
  43. package/src/components/editor-2d/floorplan-wall-move-ghost-layer.tsx +47 -0
  44. package/src/components/editor-2d/renderers/floorplan-draft-layer.tsx +6 -1
  45. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +266 -0
  46. package/src/components/editor-2d/renderers/floorplan-measurements-layer.tsx +108 -61
  47. package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +59 -0
  48. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +1963 -0
  49. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +29 -14
  50. package/src/components/editor-2d/svg-paths.ts +10 -2
  51. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +30 -9
  52. package/src/components/systems/ceiling/ceiling-system.tsx +12 -8
  53. package/src/components/systems/roof/roof-edit-system.tsx +86 -12
  54. package/src/components/systems/stair/stair-edit-system.tsx +2 -2
  55. package/src/components/systems/zone/zone-label-editor-system.tsx +0 -0
  56. package/src/components/systems/zone/zone-system.tsx +0 -0
  57. package/src/components/tools/elevator/elevator-defaults.ts +8 -0
  58. package/src/components/tools/elevator/elevator-tool.tsx +349 -0
  59. package/src/components/tools/elevator/move-elevator-tool.tsx +252 -0
  60. package/src/components/tools/fence/fence-drafting.ts +64 -22
  61. package/src/components/tools/item/move-tool.tsx +40 -107
  62. package/src/components/tools/item/placement-math.test.ts +10 -0
  63. package/src/components/tools/item/placement-math.ts +33 -9
  64. package/src/components/tools/item/placement-strategies.ts +201 -25
  65. package/src/components/tools/item/placement-types.ts +10 -1
  66. package/src/components/tools/item/use-draft-node.ts +20 -2
  67. package/src/components/tools/item/use-placement-coordinator.tsx +700 -299
  68. package/src/components/tools/registry/move-registry-node-tool.tsx +602 -0
  69. package/src/components/tools/roof/roof-tool.tsx +108 -12
  70. package/src/components/tools/select/box-select-state.ts +77 -0
  71. package/src/components/tools/select/box-select-tool.tsx +324 -543
  72. package/src/components/tools/select/plane-box-select-tool.tsx +577 -0
  73. package/src/components/tools/select/screen-rectangle-selection.ts +84 -0
  74. package/src/components/tools/select/select-candidates.test.ts +109 -0
  75. package/src/components/tools/select/select-candidates.ts +85 -0
  76. package/src/components/tools/shared/affordance-dispatch.ts +30 -0
  77. package/src/components/tools/shared/drag-bounding-box.tsx +171 -0
  78. package/src/components/tools/shared/floor-stack-preview.ts +25 -0
  79. package/src/components/tools/shared/fresh-placement-visibility.ts +61 -0
  80. package/src/components/tools/shared/placement-box-geometry.ts +123 -0
  81. package/src/components/tools/shared/placement-box.tsx +123 -0
  82. package/src/components/tools/shared/polygon-editor.tsx +617 -167
  83. package/src/components/tools/shared/segment-angle.ts +57 -1
  84. package/src/components/tools/site/site-boundary-editor.tsx +464 -12
  85. package/src/components/tools/site/site-flag-model.ts +18 -0
  86. package/src/components/tools/stair/stair-defaults.ts +1 -1
  87. package/src/components/tools/stair/stair-tool.tsx +269 -35
  88. package/src/components/tools/tool-manager.tsx +172 -56
  89. package/src/components/tools/wall/wall-drafting.ts +98 -98
  90. package/src/components/tools/wall/wall-snap-geometry.test.ts +79 -0
  91. package/src/components/tools/wall/wall-snap-geometry.ts +270 -0
  92. package/src/components/tools/zone/zone-tool.tsx +36 -11
  93. package/src/components/ui/action-menu/action-button.tsx +21 -1
  94. package/src/components/ui/action-menu/camera-actions.tsx +0 -0
  95. package/src/components/ui/action-menu/control-modes.tsx +5 -76
  96. package/src/components/ui/action-menu/furnish-tools.tsx +6 -92
  97. package/src/components/ui/action-menu/index.tsx +33 -196
  98. package/src/components/ui/action-menu/structure-tools.tsx +13 -88
  99. package/src/components/ui/action-menu/view-toggles.tsx +262 -37
  100. package/src/components/ui/command-palette/editor-commands.tsx +24 -17
  101. package/src/components/ui/command-palette/index.tsx +6 -256
  102. package/src/components/ui/controls/material-paint-panel.tsx +83 -0
  103. package/src/components/ui/controls/material-picker.tsx +17 -35
  104. package/src/components/ui/controls/metric-control.tsx +33 -11
  105. package/src/components/ui/floating-level-selector.tsx +48 -11
  106. package/src/components/ui/helpers/helper-manager.tsx +21 -17
  107. package/src/components/ui/helpers/registered-tool-helper.tsx +25 -0
  108. package/src/components/ui/item-catalog/catalog-items.tsx +1743 -315
  109. package/src/components/ui/item-catalog/item-catalog.tsx +91 -46
  110. package/src/components/ui/level-duplicate-dialog.tsx +5 -6
  111. package/src/components/ui/panels/node-display.ts +1 -0
  112. package/src/components/ui/panels/paint-panel.tsx +66 -41
  113. package/src/components/ui/panels/panel-manager.tsx +33 -74
  114. package/src/components/ui/panels/panel-wrapper.tsx +217 -12
  115. package/src/components/ui/panels/parametric-inspector.tsx +469 -0
  116. package/src/components/ui/panels/reference-panel.tsx +28 -13
  117. package/src/components/ui/primitives/dropdown-menu.tsx +12 -8
  118. package/src/components/ui/primitives/number-input.tsx +1 -1
  119. package/src/components/ui/primitives/sidebar.tsx +0 -0
  120. package/src/components/ui/scene-loader.tsx +1 -3
  121. package/src/components/ui/sidebar/app-sidebar.tsx +0 -0
  122. package/src/components/ui/sidebar/icon-rail.tsx +0 -0
  123. package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +263 -0
  124. package/src/components/ui/sidebar/panels/items-panel/index.tsx +404 -0
  125. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +59 -9
  126. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +6 -1
  127. package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +259 -0
  128. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +3 -1
  129. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +5 -2
  130. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +83 -0
  131. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +89 -0
  132. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +75 -0
  133. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +2 -2
  134. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +83 -0
  135. package/src/components/ui/sidebar/panels/site-panel/index.tsx +8 -12
  136. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +2 -1
  137. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +93 -0
  138. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +41 -5
  139. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +128 -0
  140. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +5 -2
  141. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +83 -0
  142. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -1
  143. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +68 -34
  144. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +3 -1
  145. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +52 -3
  146. package/src/components/ui/sidebar/tab-bar.tsx +61 -1
  147. package/src/components/ui/slider.tsx +1 -1
  148. package/src/components/viewer-overlay.tsx +169 -52
  149. package/src/components/viewer-zone-system.tsx +1 -1
  150. package/src/hooks/use-auto-save.ts +24 -3
  151. package/src/hooks/use-ceiling-events.ts +175 -0
  152. package/src/hooks/use-drag-action.ts +124 -0
  153. package/src/hooks/use-keyboard.ts +125 -62
  154. package/src/hooks/use-selection.ts +64 -0
  155. package/src/index.tsx +243 -4
  156. package/src/lib/constants.ts +6 -2
  157. package/src/lib/editor-api.ts +62 -0
  158. package/src/lib/elevator-support.ts +96 -0
  159. package/src/lib/floorplan/apply-alignment.ts +111 -0
  160. package/src/lib/floorplan/index.ts +7 -0
  161. package/src/lib/floorplan/selection-tool.ts +40 -0
  162. package/src/lib/floorplan/stairs.ts +12 -1
  163. package/src/lib/fresh-planar-placement.test.ts +102 -0
  164. package/src/lib/fresh-planar-placement.ts +63 -0
  165. package/src/lib/history.ts +2 -1
  166. package/src/lib/level-duplication.test.ts +0 -2
  167. package/src/lib/level-duplication.ts +55 -43
  168. package/src/lib/level-name.ts +11 -0
  169. package/src/lib/material-paint.ts +212 -20
  170. package/src/lib/placement-metadata.ts +29 -0
  171. package/src/lib/planar-cursor-placement.test.ts +43 -0
  172. package/src/lib/planar-cursor-placement.ts +42 -0
  173. package/src/lib/roof-duplication.ts +2 -2
  174. package/src/lib/scene-bounds.ts +1 -1
  175. package/src/lib/scene-clipboard.ts +267 -0
  176. package/src/lib/scene.ts +13 -4
  177. package/src/lib/sfx-bus.ts +25 -1
  178. package/src/lib/sfx-player.ts +75 -27
  179. package/src/lib/site-boundary.ts +1 -0
  180. package/src/lib/stair-duplication.ts +2 -2
  181. package/src/lib/stair-levels.test.ts +146 -0
  182. package/src/lib/stair-levels.ts +177 -0
  183. package/src/lib/world-grid-snap.ts +199 -0
  184. package/src/lib/zone-content.ts +130 -0
  185. package/src/store/use-alignment-guides.ts +21 -0
  186. package/src/store/use-editor.tsx +342 -91
  187. package/src/store/use-placement-preview.ts +31 -0
  188. package/src/store/use-wall-move-ghosts.ts +36 -0
  189. package/src/store/use-wall-snap-indicator.ts +32 -0
  190. package/tsconfig.json +2 -1
  191. package/src/components/editor/preset-thumbnail-generator.tsx +0 -125
  192. package/src/components/editor-2d/renderers/floorplan-roof-layer.tsx +0 -113
  193. package/src/components/feedback-dialog.tsx +0 -265
  194. package/src/components/pascal-radio.tsx +0 -280
  195. package/src/components/preview-button.tsx +0 -16
  196. package/src/components/tools/building/move-building-tool.tsx +0 -157
  197. package/src/components/tools/ceiling/ceiling-boundary-editor.tsx +0 -43
  198. package/src/components/tools/ceiling/ceiling-hole-editor.tsx +0 -49
  199. package/src/components/tools/ceiling/ceiling-tool.tsx +0 -465
  200. package/src/components/tools/ceiling/move-ceiling-tool.tsx +0 -257
  201. package/src/components/tools/column/column-tool.tsx +0 -97
  202. package/src/components/tools/column/move-column-tool.tsx +0 -105
  203. package/src/components/tools/door/door-math.ts +0 -110
  204. package/src/components/tools/door/door-tool.tsx +0 -324
  205. package/src/components/tools/door/move-door-tool.tsx +0 -412
  206. package/src/components/tools/fence/curve-fence-tool.tsx +0 -179
  207. package/src/components/tools/fence/fence-tool.tsx +0 -346
  208. package/src/components/tools/fence/move-fence-endpoint-tool.tsx +0 -438
  209. package/src/components/tools/fence/move-fence-tool.tsx +0 -298
  210. package/src/components/tools/item/item-tool.tsx +0 -26
  211. package/src/components/tools/roof/move-roof-tool.tsx +0 -359
  212. package/src/components/tools/slab/move-slab-tool.tsx +0 -182
  213. package/src/components/tools/slab/slab-boundary-editor.tsx +0 -43
  214. package/src/components/tools/slab/slab-hole-editor.tsx +0 -49
  215. package/src/components/tools/slab/slab-tool.tsx +0 -322
  216. package/src/components/tools/spawn/move-spawn-tool.tsx +0 -101
  217. package/src/components/tools/spawn/spawn-tool.tsx +0 -130
  218. package/src/components/tools/wall/curve-wall-tool.tsx +0 -176
  219. package/src/components/tools/wall/move-wall-endpoint-tool.tsx +0 -423
  220. package/src/components/tools/wall/move-wall-tool.tsx +0 -356
  221. package/src/components/tools/wall/wall-tool.tsx +0 -332
  222. package/src/components/tools/window/move-window-tool.tsx +0 -447
  223. package/src/components/tools/window/window-math.ts +0 -117
  224. package/src/components/tools/window/window-tool.tsx +0 -332
  225. package/src/components/ui/helpers/ceiling-helper.tsx +0 -20
  226. package/src/components/ui/helpers/slab-helper.tsx +0 -20
  227. package/src/components/ui/helpers/wall-helper.tsx +0 -20
  228. package/src/components/ui/panels/ceiling-panel.tsx +0 -250
  229. package/src/components/ui/panels/column-panel.tsx +0 -715
  230. package/src/components/ui/panels/door-panel.tsx +0 -1292
  231. package/src/components/ui/panels/fence-panel.tsx +0 -227
  232. package/src/components/ui/panels/item-panel.tsx +0 -306
  233. package/src/components/ui/panels/presets/presets-popover.tsx +0 -511
  234. package/src/components/ui/panels/roof-panel.tsx +0 -230
  235. package/src/components/ui/panels/roof-segment-panel.tsx +0 -311
  236. package/src/components/ui/panels/slab-panel.tsx +0 -250
  237. package/src/components/ui/panels/spawn-panel.tsx +0 -155
  238. package/src/components/ui/panels/stair-panel.tsx +0 -525
  239. package/src/components/ui/panels/stair-segment-panel.tsx +0 -325
  240. package/src/components/ui/panels/wall-panel.tsx +0 -177
  241. package/src/components/ui/panels/window-panel.tsx +0 -960
  242. package/src/components/ui/viewer-toolbar.tsx +0 -436
  243. package/src/contexts/presets-context.tsx +0 -121
  244. package/src/hooks/use-contextual-tools.ts +0 -61
@@ -1,5 +1,6 @@
1
1
  'use client'
2
2
 
3
+ import { Icon } from '@iconify/react'
3
4
  import {
4
5
  type AnyNodeId,
5
6
  type BuildingNode,
@@ -12,6 +13,7 @@ import { useViewer } from '@pascal-app/viewer'
12
13
  import { Check, ChevronDown, Eye, EyeOff, Layers2, Plus, Trash2 } from 'lucide-react'
13
14
  import { useCallback, useRef, useState } from 'react'
14
15
  import { useShallow } from 'zustand/react/shallow'
16
+ import { getLevelDisplayName } from '../../../lib/level-name'
15
17
  import { createLocalGuideImage } from '../../../lib/local-guide-image'
16
18
  import { cn } from '../../../lib/utils'
17
19
  import useEditor, { type GridSnapStep } from '../../../store/use-editor'
@@ -83,10 +85,6 @@ function useLowerReferenceLevels(): LevelNode[] {
83
85
  )
84
86
  }
85
87
 
86
- function getLevelDisplayName(level: LevelNode) {
87
- return level.name || `Level ${level.level}`
88
- }
89
-
90
88
  // ── Shared upload button for dropdowns ──────────────────────────────────────
91
89
 
92
90
  function UploadButton({ onError }: { onError: (message: string | null) => void }) {
@@ -258,7 +256,7 @@ function GuidesControl() {
258
256
 
259
257
  <PopoverContent
260
258
  align="center"
261
- className="w-72 rounded-xl border-border/45 bg-background/96 p-3 shadow-[0_14px_28px_-18px_rgba(15,23,42,0.55),0_6px_16px_-10px_rgba(15,23,42,0.2)] backdrop-blur-xl"
259
+ className="w-72 rounded-xl border-border/45 bg-background/96 p-3 shadow-elevation-3 backdrop-blur-xl"
262
260
  side="top"
263
261
  sideOffset={14}
264
262
  >
@@ -353,9 +351,9 @@ function GuidesControl() {
353
351
  )
354
352
  }
355
353
 
356
- // ── Grid snap ──────────────────────────────────────────────────────────────
354
+ // ── Grid snap toggle ────────────────────────────────────────────────────────
357
355
 
358
- export function GridSnapControl() {
356
+ function GridSnapControl() {
359
357
  const [isOpen, setIsOpen] = useState(false)
360
358
  const gridSnapStep = useEditor((state) => state.gridSnapStep)
361
359
  const setGridSnapStep = useEditor((state) => state.setGridSnapStep)
@@ -374,20 +372,7 @@ export function GridSnapControl() {
374
372
  )}
375
373
  type="button"
376
374
  >
377
- <svg
378
- className="h-4 w-4"
379
- fill="none"
380
- stroke="currentColor"
381
- strokeWidth={1.5}
382
- viewBox="0 0 24 24"
383
- xmlns="http://www.w3.org/2000/svg"
384
- >
385
- <path
386
- d="M3 3h7v7H3V3zm11 0h7v7h-7V3zm0 11h7v7h-7v-7zm-11 0h7v7H3v-7z"
387
- strokeLinecap="round"
388
- strokeLinejoin="round"
389
- />
390
- </svg>
375
+ <Icon height={16} icon="lucide:grid-2x2" width={16} />
391
376
  <span className="mt-1 font-medium text-[9px] leading-none">
392
377
  {formatGridSnapStep(gridSnapStep)}
393
378
  </span>
@@ -510,7 +495,7 @@ function ScansControl() {
510
495
 
511
496
  <PopoverContent
512
497
  align="center"
513
- className="w-72 rounded-xl border-border/45 bg-background/96 p-3 shadow-[0_14px_28px_-18px_rgba(15,23,42,0.55),0_6px_16px_-10px_rgba(15,23,42,0.2)] backdrop-blur-xl"
498
+ className="w-72 rounded-xl border-border/45 bg-background/96 p-3 shadow-elevation-3 backdrop-blur-xl"
514
499
  side="top"
515
500
  sideOffset={14}
516
501
  >
@@ -605,6 +590,249 @@ function ScansControl() {
605
590
  )
606
591
  }
607
592
 
593
+ // ── References (merged scans + guides) ──────────────────────────────────────
594
+ // Bottom-bar control that folds the separate Scans and Guides toggles into one
595
+ // "References" split button + a popover holding both, each with its own
596
+ // visibility toggle, upload, and per-item opacity/delete.
597
+
598
+ function ReferenceListSection({
599
+ title,
600
+ iconSrc,
601
+ noun,
602
+ emptyText,
603
+ nodes,
604
+ show,
605
+ setShow,
606
+ onError,
607
+ }: {
608
+ title: string
609
+ iconSrc: string
610
+ noun: string
611
+ emptyText: string
612
+ nodes: (GuideNode | ScanNode)[]
613
+ show: boolean
614
+ setShow: (show: boolean) => void
615
+ onError: (message: string | null) => void
616
+ }) {
617
+ const setSelection = useViewer((state) => state.setSelection)
618
+ const updateNode = useScene((state) => state.updateNode)
619
+ const deleteNode = useScene((state) => state.deleteNode)
620
+ const selectedReferenceId = useEditor((state) => state.selectedReferenceId)
621
+ const setSelectedReferenceId = useEditor((state) => state.setSelectedReferenceId)
622
+ const hasItems = nodes.length > 0
623
+
624
+ const handleSelect = useCallback(
625
+ (id: AnyNodeId) => {
626
+ setShow(true)
627
+ setSelectedReferenceId(id)
628
+ setSelection({ selectedIds: [], zoneId: null })
629
+ },
630
+ [setShow, setSelectedReferenceId, setSelection],
631
+ )
632
+
633
+ return (
634
+ <div className="space-y-2">
635
+ <div className="flex items-center gap-2">
636
+ <span className="flex h-7 w-7 shrink-0 items-center justify-center rounded-lg bg-background/80">
637
+ <img alt="" className="h-4 w-4 object-contain" src={iconSrc} />
638
+ </span>
639
+ <div className="min-w-0 flex-1">
640
+ <p className="font-medium text-foreground text-sm">{title}</p>
641
+ {hasItems && (
642
+ <p className="text-muted-foreground text-xs">
643
+ {nodes.length} {noun}
644
+ {nodes.length !== 1 ? 's' : ''} on this level
645
+ </p>
646
+ )}
647
+ </div>
648
+ <button
649
+ aria-label={show ? `Hide ${title.toLowerCase()}` : `Show ${title.toLowerCase()}`}
650
+ className="flex h-6 w-6 shrink-0 items-center justify-center rounded-full border border-border/40 text-muted-foreground transition-colors hover:bg-white/10 hover:text-foreground"
651
+ onClick={() => setShow(!show)}
652
+ type="button"
653
+ >
654
+ {show ? <Eye className="h-3.5 w-3.5" /> : <EyeOff className="h-3.5 w-3.5" />}
655
+ </button>
656
+ <UploadButton onError={onError} />
657
+ </div>
658
+
659
+ {hasItems ? (
660
+ <div className="max-h-40 space-y-2 overflow-y-auto pr-1">
661
+ {nodes.map((node, index) => (
662
+ <div
663
+ className={cn(
664
+ 'group/item space-y-2 rounded-xl border bg-background/75 p-2.5 transition-colors',
665
+ selectedReferenceId === node.id
666
+ ? 'border-foreground/35 bg-white/10'
667
+ : 'border-border/45',
668
+ )}
669
+ key={node.id}
670
+ >
671
+ <div className="flex min-w-0 items-center gap-2">
672
+ <button
673
+ className="flex min-w-0 flex-1 items-center gap-2 text-left"
674
+ onClick={() => handleSelect(node.id)}
675
+ type="button"
676
+ >
677
+ <img
678
+ alt=""
679
+ className="h-3.5 w-3.5 shrink-0 object-contain opacity-70"
680
+ src={iconSrc}
681
+ />
682
+ <p className="truncate font-medium text-foreground text-sm">
683
+ {node.name || `${noun.charAt(0).toUpperCase()}${noun.slice(1)} ${index + 1}`}
684
+ </p>
685
+ {selectedReferenceId === node.id && (
686
+ <Check className="ml-auto h-3.5 w-3.5 shrink-0 text-foreground/80" />
687
+ )}
688
+ </button>
689
+ <button
690
+ aria-label={`Delete ${noun}`}
691
+ className="flex h-5 w-5 shrink-0 items-center justify-center rounded-md text-muted-foreground/50 opacity-0 transition-all hover:bg-destructive/10 hover:text-destructive group-hover/item:opacity-100"
692
+ onClick={(event) => {
693
+ event.stopPropagation()
694
+ deleteNode(node.id)
695
+ if (selectedReferenceId === node.id) {
696
+ setSelectedReferenceId(null)
697
+ }
698
+ }}
699
+ type="button"
700
+ >
701
+ <Trash2 className="h-3 w-3" />
702
+ </button>
703
+ </div>
704
+ <SliderControl
705
+ label="Opacity"
706
+ max={100}
707
+ min={0}
708
+ onChange={(value) =>
709
+ updateNode(node.id, { opacity: Math.round(Math.min(100, Math.max(0, value))) })
710
+ }
711
+ precision={0}
712
+ step={1}
713
+ unit="%"
714
+ value={node.opacity}
715
+ />
716
+ </div>
717
+ ))}
718
+ </div>
719
+ ) : (
720
+ <div className="rounded-xl border border-border/45 border-dashed bg-background/60 px-3 py-3 text-muted-foreground text-sm">
721
+ {emptyText}
722
+ </div>
723
+ )}
724
+ </div>
725
+ )
726
+ }
727
+
728
+ function ReferencesControl() {
729
+ const showScans = useViewer((state) => state.showScans)
730
+ const setShowScans = useViewer((state) => state.setShowScans)
731
+ const showGuides = useViewer((state) => state.showGuides)
732
+ const setShowGuides = useViewer((state) => state.setShowGuides)
733
+ const [isOpen, setIsOpen] = useState(false)
734
+ const [uploadError, setUploadError] = useState<string | null>(null)
735
+
736
+ const scans = useLevelScans()
737
+ const guides = useLevelGuides()
738
+ const total = scans.length + guides.length
739
+ const anyVisible = showScans || showGuides
740
+
741
+ const toggleAll = useCallback(() => {
742
+ const next = !anyVisible
743
+ setShowScans(next)
744
+ setShowGuides(next)
745
+ }, [anyVisible, setShowScans, setShowGuides])
746
+
747
+ return (
748
+ <Popover onOpenChange={setIsOpen} open={isOpen}>
749
+ <div className="flex items-center">
750
+ <ActionButton
751
+ className={cn(
752
+ 'rounded-r-none p-0',
753
+ anyVisible
754
+ ? 'bg-white/15'
755
+ : 'opacity-60 grayscale hover:bg-white/5 hover:opacity-100 hover:grayscale-0',
756
+ )}
757
+ label={`References: ${anyVisible ? 'Visible' : 'Hidden'}`}
758
+ onClick={toggleAll}
759
+ size="icon"
760
+ variant="ghost"
761
+ >
762
+ <div className="relative">
763
+ <img
764
+ alt="References"
765
+ className="h-[28px] w-[28px] object-contain"
766
+ src="/icons/floorplan.png"
767
+ />
768
+ <span className="absolute -right-1.5 -bottom-1 min-w-[14px] rounded-full bg-white/20 px-[3px] text-center font-medium text-[9px] text-white/70 leading-[14px]">
769
+ {total}
770
+ </span>
771
+ </div>
772
+ </ActionButton>
773
+
774
+ <PopoverTrigger asChild>
775
+ <button
776
+ aria-expanded={isOpen}
777
+ aria-label="Reference settings"
778
+ className={cn(
779
+ 'flex h-11 w-6 items-center justify-center rounded-r-lg transition-colors',
780
+ anyVisible
781
+ ? isOpen
782
+ ? 'bg-white/10'
783
+ : 'bg-white/5 hover:bg-white/8'
784
+ : isOpen
785
+ ? 'bg-white/8'
786
+ : 'opacity-60 hover:bg-white/5 hover:opacity-100',
787
+ )}
788
+ type="button"
789
+ >
790
+ <ChevronDown className={cn('h-3 w-3 transition-transform', isOpen && 'rotate-180')} />
791
+ </button>
792
+ </PopoverTrigger>
793
+ </div>
794
+
795
+ <PopoverContent
796
+ align="center"
797
+ className="w-72 rounded-xl border-border/45 bg-background/96 p-3 shadow-elevation-3 backdrop-blur-xl"
798
+ side="top"
799
+ sideOffset={14}
800
+ >
801
+ <div className="space-y-3">
802
+ {uploadError && (
803
+ <div className="rounded-lg border border-destructive/30 bg-destructive/10 px-2.5 py-2 text-destructive text-xs">
804
+ {uploadError}
805
+ </div>
806
+ )}
807
+ <ReferenceListSection
808
+ emptyText="No scans on this level yet."
809
+ iconSrc="/icons/mesh.png"
810
+ nodes={scans}
811
+ noun="scan"
812
+ onError={setUploadError}
813
+ setShow={setShowScans}
814
+ show={showScans}
815
+ title="Scans"
816
+ />
817
+ <div className="h-px bg-border/45" />
818
+ <ReferenceListSection
819
+ emptyText="No guide images on this level yet."
820
+ iconSrc="/icons/floorplan.png"
821
+ nodes={guides}
822
+ noun="guide image"
823
+ onError={setUploadError}
824
+ setShow={setShowGuides}
825
+ show={showGuides}
826
+ title="Guide images"
827
+ />
828
+ </div>
829
+ </PopoverContent>
830
+ </Popover>
831
+ )
832
+ }
833
+
834
+ // ── Reference floor control ────────────────────────────────────────────────────────────────────
835
+
608
836
  function ReferenceFloorControl() {
609
837
  const showReferenceFloor = useEditor((state) => state.showReferenceFloor)
610
838
  const toggleReferenceFloor = useEditor((state) => state.toggleReferenceFloor)
@@ -694,7 +922,11 @@ function ReferenceFloorControl() {
694
922
  onClick={toggleReferenceFloor}
695
923
  type="button"
696
924
  >
697
- {showReferenceFloor ? <Eye className="h-3.5 w-3.5" /> : <EyeOff className="h-3.5 w-3.5" />}
925
+ {showReferenceFloor ? (
926
+ <Eye className="h-3.5 w-3.5" />
927
+ ) : (
928
+ <EyeOff className="h-3.5 w-3.5" />
929
+ )}
698
930
  </button>
699
931
  </div>
700
932
 
@@ -728,9 +960,7 @@ function ReferenceFloorControl() {
728
960
  )}
729
961
  />
730
962
  <span className="min-w-0 flex-1 truncate">{levelName}</span>
731
- <span className="text-[10px] text-muted-foreground">
732
- {index + 1} below
733
- </span>
963
+ <span className="text-[10px] text-muted-foreground">{index + 1} below</span>
734
964
  </button>
735
965
  )
736
966
  })}
@@ -757,24 +987,19 @@ function ReferenceFloorControl() {
757
987
  )
758
988
  }
759
989
 
760
- // ── Main ViewToggles ────────────────────────────────────────────────────────
990
+ // ── Exports ─────────────────────────────────────────────────────────────────
761
991
 
762
- export function ViewToggles() {
992
+ export { GridSnapControl }
993
+
994
+ export function SecondaryToggles() {
763
995
  return (
764
996
  <div className="flex items-center gap-1">
765
- {/* Scans (toggle + dropdown) */}
766
- <ScansControl />
767
-
768
- {/* Guides (toggle + dropdown) */}
769
- <GuidesControl />
770
-
771
- <ReferenceFloorControl />
997
+ <ReferencesControl />
772
998
  </div>
773
999
  )
774
1000
  }
775
1001
 
776
- // Secondary toggles for mobile (grid snap + scans + guides)
777
- export function SecondaryToggles() {
1002
+ export function ViewToggles() {
778
1003
  return (
779
1004
  <div className="flex items-center gap-1">
780
1005
  <GridSnapControl />
@@ -27,6 +27,7 @@ import {
27
27
  PencilLine,
28
28
  Plus,
29
29
  Redo2,
30
+ Sparkles,
30
31
  Square,
31
32
  SquareStack,
32
33
  Sun,
@@ -35,8 +36,8 @@ import {
35
36
  Video,
36
37
  } from 'lucide-react'
37
38
  import { useEffect } from 'react'
38
- import { deleteLevelWithFallbackSelection } from '../../../lib/level-selection'
39
39
  import { runRedo, runUndo } from '../../../lib/history'
40
+ import { deleteLevelWithFallbackSelection } from '../../../lib/level-selection'
40
41
  import { useCommandRegistry } from '../../../store/use-command-registry'
41
42
  import type { StructureTool } from '../../../store/use-editor'
42
43
  import useEditor from '../../../store/use-editor'
@@ -190,8 +191,11 @@ export function EditorCommands() {
190
191
  const { nodes } = useScene.getState()
191
192
  const building = Object.values(nodes).find((n) => n.type === 'building')
192
193
  if (!building) return
194
+ const levelCount = building.children.filter(
195
+ (childId) => nodes[childId as keyof typeof nodes]?.type === 'level',
196
+ ).length
193
197
  const newLevel = LevelNode.parse({
194
- level: building.children.length,
198
+ level: levelCount,
195
199
  children: [],
196
200
  parentId: building.id,
197
201
  })
@@ -278,28 +282,31 @@ export function EditorCommands() {
278
282
  }),
279
283
  },
280
284
  {
281
- id: 'editor.viewer.theme',
282
- label: () => {
283
- const theme = useViewer.getState().theme
284
- return theme === 'dark' ? 'Switch to Light Theme' : 'Switch to Dark Theme'
285
- },
285
+ id: 'editor.viewer.shading-solid',
286
+ label: 'Switch to Solid',
286
287
  group: 'Viewer Controls',
287
- icon: <Sun className="h-4 w-4" />, // icon is static; label conveys the action
288
- keywords: ['theme', 'dark', 'light', 'appearance', 'color'],
289
- execute: () =>
290
- run(() => {
291
- const { theme, setTheme } = useViewer.getState()
292
- setTheme(theme === 'dark' ? 'light' : 'dark')
293
- }),
288
+ icon: <Box className="h-4 w-4" />,
289
+ keywords: ['solid', 'shading', 'render', 'mode', 'performance'],
290
+ execute: () => run(() => useViewer.getState().setShading('solid')),
291
+ },
292
+ {
293
+ id: 'editor.viewer.shading-rendered',
294
+ label: 'Switch to Rendered',
295
+ group: 'Viewer Controls',
296
+ icon: <Sparkles className="h-4 w-4" />,
297
+ keywords: ['rendered', 'shading', 'render', 'mode', 'quality'],
298
+ execute: () => run(() => useViewer.getState().setShading('rendered')),
294
299
  },
295
300
  {
296
301
  id: 'editor.viewer.camera-snapshot',
297
- label: 'Camera Snapshot',
302
+ label: 'Take Snapshot',
298
303
  group: 'Viewer Controls',
299
304
  icon: <Camera className="h-4 w-4" />,
300
305
  keywords: ['camera', 'snapshot', 'capture', 'save', 'view', 'bookmark'],
301
- navigate: true,
302
- execute: () => navigateTo('camera-view'),
306
+ execute: () => {
307
+ setOpen(false)
308
+ useEditor.getState().setCaptureMode(true)
309
+ },
303
310
  },
304
311
 
305
312
  // ── View ─────────────────────────────────────────────────────────────