@pascal-app/editor 0.9.1 → 0.9.2

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 (262) hide show
  1. package/package.json +12 -9
  2. package/src/components/editor/alignment-3d-guide-layer.tsx +10 -2
  3. package/src/components/editor/bake-exporter.tsx +47 -0
  4. package/src/components/editor/custom-camera-controls.tsx +63 -25
  5. package/src/components/editor/editor-layout-mobile.tsx +5 -0
  6. package/src/components/editor/editor-layout-v2.tsx +18 -0
  7. package/src/components/editor/export-manager.tsx +71 -63
  8. package/src/components/editor/fence-tangent-lines-3d.tsx +87 -0
  9. package/src/components/editor/first-person/build-collider-world.test.ts +36 -3
  10. package/src/components/editor/first-person/build-collider-world.ts +32 -5
  11. package/src/components/editor/first-person-controls.tsx +118 -7
  12. package/src/components/editor/floating-action-menu.tsx +410 -33
  13. package/src/components/editor/floating-building-action-menu.tsx +1 -1
  14. package/src/components/editor/floorplan-background-selection.test.ts +58 -0
  15. package/src/components/editor/floorplan-background-selection.ts +3 -2
  16. package/src/components/editor/floorplan-panel.tsx +1804 -849
  17. package/src/components/editor/grid.tsx +166 -39
  18. package/src/components/editor/group-actions.ts +457 -0
  19. package/src/components/editor/group-floating-action-menu.tsx +130 -0
  20. package/src/components/editor/group-move-3d.ts +397 -0
  21. package/src/components/editor/group-rotate-handle.tsx +102 -64
  22. package/src/components/editor/group-selection-box-3d.tsx +173 -0
  23. package/src/components/editor/group-transform-shared.test.ts +192 -1
  24. package/src/components/editor/group-transform-shared.ts +240 -7
  25. package/src/components/editor/handles/handle-arrow.tsx +81 -23
  26. package/src/components/editor/handles/use-handle-drag.ts +37 -4
  27. package/src/components/editor/index.tsx +248 -27
  28. package/src/components/editor/measurement-pill.tsx +62 -22
  29. package/src/components/editor/node-action-menu.tsx +14 -1
  30. package/src/components/editor/node-arrow-handles.tsx +366 -173
  31. package/src/components/editor/opening-guides-3d-layer.tsx +144 -0
  32. package/src/components/editor/quick-measurement-card.tsx +70 -0
  33. package/src/components/editor/quick-measurement-hud.tsx +28 -0
  34. package/src/components/editor/riser-diagram-panel.tsx +137 -0
  35. package/src/components/editor/selection-manager.tsx +812 -466
  36. package/src/components/editor/site-edge-labels.tsx +4 -4
  37. package/src/components/editor/slab-hole-highlights.tsx +13 -5
  38. package/src/components/editor/snapshot-capture-overlay.tsx +255 -115
  39. package/src/components/editor/three-context-bridge.ts +22 -0
  40. package/src/components/editor/thumbnail-generator.tsx +136 -43
  41. package/src/components/editor/use-floorplan-background-placement.ts +113 -45
  42. package/src/components/editor/use-floorplan-scene-data.ts +5 -5
  43. package/src/components/editor/use-mesh-settle-epoch.ts +26 -0
  44. package/src/components/editor/wall-measurement-label.tsx +3 -13
  45. package/src/components/editor/wall-move-side-handles.tsx +36 -21
  46. package/src/components/editor/wall-snap-beacon-layer.tsx +160 -6
  47. package/src/components/editor-2d/floorplan-action-menu-layer.tsx +9 -5
  48. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +1 -1
  49. package/src/components/editor-2d/floorplan-group-action-menu.tsx +87 -0
  50. package/src/components/editor-2d/floorplan-group-move.tsx +701 -0
  51. package/src/components/editor-2d/floorplan-measurement-tool-layer.test.ts +113 -0
  52. package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +1656 -0
  53. package/src/components/editor-2d/floorplan-quick-measure-layer.tsx +212 -0
  54. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +169 -4
  55. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +300 -59
  56. package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +5 -3
  57. package/src/components/editor-2d/renderers/floorplan-label-angle.test.ts +15 -0
  58. package/src/components/editor-2d/renderers/floorplan-label-angle.ts +14 -0
  59. package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +4 -1
  60. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +308 -0
  61. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +1704 -342
  62. package/src/components/editor-2d/renderers/floorplan-voronoi-layer.tsx +128 -0
  63. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +419 -60
  64. package/src/components/systems/ceiling/ceiling-system.tsx +97 -4
  65. package/src/components/systems/roof/roof-edit-system.tsx +1768 -13
  66. package/src/components/systems/selection-affordance-manager.tsx +38 -0
  67. package/src/components/systems/zone/zone-label-editor-system.tsx +14 -1
  68. package/src/components/systems/zone/zone-system.tsx +42 -13
  69. package/src/components/tools/elevator/elevator-tool.tsx +34 -8
  70. package/src/components/tools/elevator/move-elevator-tool.tsx +13 -2
  71. package/src/components/tools/fence/fence-drafting.ts +80 -8
  72. package/src/components/tools/item/move-tool.tsx +17 -13
  73. package/src/components/tools/item/placement-math.test.ts +13 -1
  74. package/src/components/tools/item/placement-math.ts +28 -2
  75. package/src/components/tools/item/placement-strategies.ts +246 -5
  76. package/src/components/tools/item/placement-types.ts +13 -1
  77. package/src/components/tools/item/use-draft-node.ts +31 -1
  78. package/src/components/tools/item/use-placement-coordinator.tsx +761 -121
  79. package/src/components/tools/registry/move-registry-node-tool.tsx +561 -99
  80. package/src/components/tools/roof/roof-tool.tsx +320 -94
  81. package/src/components/tools/select/box-select-state.ts +18 -7
  82. package/src/components/tools/select/box-select-tool.tsx +176 -40
  83. package/src/components/tools/select/marquee-geometry.test.ts +161 -0
  84. package/src/components/tools/select/marquee-geometry.ts +155 -0
  85. package/src/components/tools/select/plane-box-select-tool.tsx +1 -8
  86. package/src/components/tools/select/select-candidates.ts +1 -1
  87. package/src/components/tools/shared/cursor-sphere.tsx +62 -26
  88. package/src/components/tools/shared/drag-bounding-box.tsx +28 -4
  89. package/src/components/tools/shared/facing-indicator.tsx +87 -0
  90. package/src/components/tools/shared/facing-pose-indicator.tsx +54 -0
  91. package/src/components/tools/shared/placement-box.tsx +183 -1
  92. package/src/components/tools/shared/polygon-editor.tsx +276 -35
  93. package/src/components/tools/site/site-boundary-editor.tsx +88 -36
  94. package/src/components/tools/stair/stair-defaults.ts +1 -0
  95. package/src/components/tools/stair/stair-tool.tsx +82 -11
  96. package/src/components/tools/tool-manager.tsx +86 -25
  97. package/src/components/tools/wall/wall-drafting.test.ts +330 -0
  98. package/src/components/tools/wall/wall-drafting.ts +180 -103
  99. package/src/components/tools/wall/wall-snap-geometry.test.ts +44 -0
  100. package/src/components/tools/wall/wall-snap-geometry.ts +67 -6
  101. package/src/components/tools/zone/zone-boundary-editor.tsx +5 -1
  102. package/src/components/tools/zone/zone-tool.tsx +82 -88
  103. package/src/components/ui/action-menu/camera-actions.tsx +24 -17
  104. package/src/components/ui/action-menu/control-modes.tsx +40 -93
  105. package/src/components/ui/action-menu/furnish-tools.tsx +5 -5
  106. package/src/components/ui/action-menu/index.tsx +2 -4
  107. package/src/components/ui/action-menu/measurement-control.tsx +188 -0
  108. package/src/components/ui/action-menu/structure-tools.tsx +22 -13
  109. package/src/components/ui/action-menu/view-toggles.tsx +42 -90
  110. package/src/components/ui/command-palette/editor-commands.tsx +2 -2
  111. package/src/components/ui/command-palette/index.tsx +4 -3
  112. package/src/components/ui/controls/material-paint-panel.tsx +86 -17
  113. package/src/components/ui/controls/material-picker.tsx +83 -152
  114. package/src/components/ui/controls/material-properties-editor.tsx +108 -0
  115. package/src/components/ui/controls/metric-control.tsx +118 -44
  116. package/src/components/ui/controls/scene-material-list.tsx +247 -0
  117. package/src/components/ui/controls/slider-control.tsx +88 -38
  118. package/src/components/ui/floating-level-selector.tsx +1 -1
  119. package/src/components/ui/helpers/building-helper.tsx +10 -23
  120. package/src/components/ui/helpers/contextual-helper-panel.tsx +428 -0
  121. package/src/components/ui/helpers/helper-manager.tsx +227 -14
  122. package/src/components/ui/helpers/item-helper.tsx +28 -32
  123. package/src/components/ui/helpers/registered-tool-helper.tsx +45 -11
  124. package/src/components/ui/helpers/roof-helper.tsx +10 -12
  125. package/src/components/ui/icon-ref.tsx +47 -0
  126. package/src/components/ui/item-catalog/catalog-items.tsx +39 -1
  127. package/src/components/ui/item-catalog/item-catalog.tsx +13 -36
  128. package/src/components/ui/level-duplicate-dialog.tsx +1 -1
  129. package/src/components/ui/panels/node-display.ts +17 -17
  130. package/src/components/ui/panels/panel-manager.tsx +22 -13
  131. package/src/components/ui/panels/panel-wrapper.tsx +24 -3
  132. package/src/components/ui/panels/parametric-inspector.tsx +15 -2
  133. package/src/components/ui/panels/reference-panel.tsx +54 -19
  134. package/src/components/ui/primitives/shortcut-token.tsx +39 -3
  135. package/src/components/ui/primitives/sidebar.tsx +1 -0
  136. package/src/components/ui/sidebar/app-sidebar.tsx +5 -1
  137. package/src/components/ui/sidebar/icon-rail.tsx +50 -31
  138. package/src/components/ui/sidebar/panels/plugins-panel.tsx +218 -0
  139. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +71 -40
  140. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +60 -6
  141. package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +12 -4
  142. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -3
  143. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +10 -7
  144. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -1
  145. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +7 -2
  146. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +10 -7
  147. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -1
  148. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +1 -1
  149. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +10 -7
  150. package/src/components/ui/sidebar/panels/site-panel/index.tsx +40 -17
  151. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +31 -17
  152. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +1 -1
  153. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +72 -25
  154. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +7 -3
  155. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +12 -8
  156. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +4 -3
  157. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +10 -7
  158. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -1
  159. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +2 -2
  160. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +44 -3
  161. package/src/components/ui/sidebar/panels/site-panel/tree-structure.ts +36 -0
  162. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -1
  163. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +7 -2
  164. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +3 -3
  165. package/src/components/ui/sidebar/tab-bar.tsx +42 -28
  166. package/src/components/ui/sidebar/use-plugin-panels.tsx +132 -0
  167. package/src/components/ui/snap-target-badge.test.tsx +40 -0
  168. package/src/components/ui/snap-target-badge.tsx +88 -0
  169. package/src/components/viewer-overlay.tsx +85 -10
  170. package/src/components/viewer-zone-system.tsx +6 -1
  171. package/src/hooks/use-auto-save.test.ts +14 -0
  172. package/src/hooks/use-auto-save.ts +54 -7
  173. package/src/hooks/use-ceiling-events.ts +3 -2
  174. package/src/hooks/use-drag-action.ts +4 -1
  175. package/src/hooks/use-keyboard.ts +385 -40
  176. package/src/index.tsx +219 -6
  177. package/src/lib/active-placement-surface.ts +35 -0
  178. package/src/lib/ceiling-plan-snap.ts +24 -0
  179. package/src/lib/contextual-help.test.ts +112 -0
  180. package/src/lib/contextual-help.ts +144 -0
  181. package/src/lib/continuation.ts +64 -0
  182. package/src/lib/direct-manipulation.test.ts +192 -0
  183. package/src/lib/direct-manipulation.ts +109 -0
  184. package/src/lib/editor-api.ts +23 -35
  185. package/src/lib/floorplan/apply-alignment.test.ts +25 -0
  186. package/src/lib/floorplan/apply-alignment.ts +18 -12
  187. package/src/lib/floorplan/floorplan-export.tsx +364 -0
  188. package/src/lib/floorplan/geometry.ts +53 -0
  189. package/src/lib/floorplan/index.ts +3 -0
  190. package/src/lib/floorplan/items.ts +5 -2
  191. package/src/lib/floorplan/plan-coords.ts +21 -0
  192. package/src/lib/fresh-planar-placement.test.ts +240 -3
  193. package/src/lib/fresh-planar-placement.ts +68 -1
  194. package/src/lib/glb-export.test.ts +441 -0
  195. package/src/lib/glb-export.ts +874 -0
  196. package/src/lib/history.ts +9 -0
  197. package/src/lib/interaction/hot-set.test.ts +133 -0
  198. package/src/lib/interaction/hot-set.ts +67 -0
  199. package/src/lib/interaction/overlay-policy.test.ts +51 -0
  200. package/src/lib/interaction/overlay-policy.ts +59 -0
  201. package/src/lib/interaction/scope.ts +211 -0
  202. package/src/lib/level-selection.ts +2 -2
  203. package/src/lib/material-paint.ts +36 -49
  204. package/src/lib/measurement-kind.test.ts +30 -0
  205. package/src/lib/measurement-kind.ts +19 -0
  206. package/src/lib/measurement-label.test.ts +47 -0
  207. package/src/lib/measurement-label.ts +68 -0
  208. package/src/lib/measurement-parser.ts +116 -0
  209. package/src/lib/measurements.test.ts +183 -0
  210. package/src/lib/measurements.ts +199 -0
  211. package/src/lib/paint-scope.test.ts +454 -0
  212. package/src/lib/paint-scope.ts +461 -0
  213. package/src/lib/placement-drag-release.ts +23 -0
  214. package/src/lib/planar-cursor-placement.test.ts +57 -0
  215. package/src/lib/plugin-panels.test.ts +19 -0
  216. package/src/lib/plugin-panels.ts +91 -0
  217. package/src/lib/quick-measurement.test.ts +77 -0
  218. package/src/lib/quick-measurement.ts +109 -0
  219. package/src/lib/roof-duplication.ts +6 -6
  220. package/src/lib/roof-hover-outline-proxy.ts +22 -0
  221. package/src/lib/roof-wall-hit.ts +164 -0
  222. package/src/lib/scene-clipboard.test.ts +196 -0
  223. package/src/lib/scene-clipboard.ts +73 -8
  224. package/src/lib/scene.ts +28 -5
  225. package/src/lib/selection-routing.test.ts +298 -0
  226. package/src/lib/selection-routing.ts +175 -0
  227. package/src/lib/sfx/index.ts +1 -0
  228. package/src/lib/sfx/movement-tick.test.ts +65 -0
  229. package/src/lib/sfx/movement-tick.ts +18 -0
  230. package/src/lib/sfx-bus.ts +50 -13
  231. package/src/lib/sfx-player.test.ts +124 -0
  232. package/src/lib/sfx-player.ts +111 -66
  233. package/src/lib/slab-plan-snap.test.ts +93 -0
  234. package/src/lib/slab-plan-snap.ts +108 -0
  235. package/src/lib/snapping-mode.test.ts +138 -0
  236. package/src/lib/snapping-mode.ts +179 -0
  237. package/src/lib/stair-duplication.ts +4 -4
  238. package/src/lib/surface-plan-snap.test.ts +71 -0
  239. package/src/lib/surface-plan-snap.ts +256 -0
  240. package/src/lib/use-linear-display.ts +40 -0
  241. package/src/lib/world-grid-snap.ts +37 -5
  242. package/src/store/use-direct-manipulation-feedback.ts +20 -0
  243. package/src/store/use-editor.tsx +484 -114
  244. package/src/store/use-facing-pose.ts +44 -0
  245. package/src/store/use-fence-curve-draft.ts +21 -0
  246. package/src/store/use-floorplan-draft-preview.ts +101 -0
  247. package/src/store/use-floorplan-marquee.ts +50 -0
  248. package/src/store/use-interaction-scope.test.ts +186 -0
  249. package/src/store/use-interaction-scope.ts +132 -0
  250. package/src/store/use-measurement-draft.test.ts +530 -0
  251. package/src/store/use-measurement-draft.ts +543 -0
  252. package/src/store/use-opening-guides.ts +41 -0
  253. package/src/store/use-placement-preview.ts +11 -3
  254. package/src/store/use-quick-measurement-hud.test.ts +53 -0
  255. package/src/store/use-quick-measurement-hud.ts +77 -0
  256. package/src/store/use-segment-draft-chain.ts +28 -0
  257. package/src/store/use-stair-build-preview.ts +43 -0
  258. package/src/store/use-wall-snap-indicator.ts +2 -0
  259. package/src/components/editor/first-person/bvh-ecctrl.tsx +0 -860
  260. package/src/components/editor/group-move-handle.tsx +0 -316
  261. package/src/components/ui/panels/paint-panel.tsx +0 -163
  262. package/src/lib/level-name.ts +0 -11
@@ -6,6 +6,7 @@ import {
6
6
  type AnyNodeId,
7
7
  type BuildingNode,
8
8
  emitter,
9
+ getLevelDisplayName,
9
10
  type LevelNode,
10
11
  useScene,
11
12
  type ZoneNode,
@@ -24,19 +25,24 @@ import {
24
25
  Check,
25
26
  ChevronRight,
26
27
  Diamond,
28
+ Footprints,
27
29
  Layers,
30
+ Palette,
28
31
  PenLine,
29
32
  Sparkles,
33
+ Square,
30
34
  } from 'lucide-react'
31
35
  import Link from 'next/link'
36
+ import { flushSync } from 'react-dom'
32
37
  import { useShallow } from 'zustand/react/shallow'
33
- import { getLevelDisplayName } from '../lib/level-name'
34
38
  import { cn } from '../lib/utils'
39
+ import useEditor from '../store/use-editor'
35
40
  import { ActionButton } from './ui/action-menu/action-button'
36
41
  import {
37
42
  DropdownMenu,
38
43
  DropdownMenuContent,
39
44
  DropdownMenuItem,
45
+ DropdownMenuSeparator,
40
46
  DropdownMenuTrigger,
41
47
  } from './ui/primitives/dropdown-menu'
42
48
  import { TooltipProvider } from './ui/primitives/tooltip'
@@ -48,6 +54,24 @@ type ProjectOwner = {
48
54
  image: string | null
49
55
  }
50
56
 
57
+ function requestWalkthroughPointerLock() {
58
+ const canvas = document.querySelector<HTMLCanvasElement>('[data-pascal-viewer-3d] canvas')
59
+ if (!canvas) return
60
+
61
+ if (!canvas.hasAttribute('tabindex')) {
62
+ canvas.tabIndex = -1
63
+ }
64
+ canvas.focus({ preventScroll: true })
65
+
66
+ if (document.pointerLockElement === canvas) return
67
+
68
+ try {
69
+ canvas.requestPointerLock?.()
70
+ } catch {
71
+ return
72
+ }
73
+ }
74
+
51
75
  const levelModeLabels: Record<'stacked' | 'exploded' | 'solo', string> = {
52
76
  stacked: 'Stacked',
53
77
  exploded: 'Exploded',
@@ -64,22 +88,28 @@ const levelModeBadgeLabels: Record<'manual' | 'stacked' | 'exploded' | 'solo', s
64
88
  const wallModeConfig = {
65
89
  up: {
66
90
  icon: (props: any) => (
67
- <img alt="Full Height" height={28} src="/icons/room.png" width={28} {...props} />
91
+ <img alt="Full Height" height={28} src="/icons/room.webp" width={28} {...props} />
68
92
  ),
69
93
  label: 'Full Height',
70
94
  },
71
95
  cutaway: {
72
96
  icon: (props: any) => (
73
- <img alt="Cutaway" height={28} src="/icons/wallcut.png" width={28} {...props} />
97
+ <img alt="Cutaway" height={28} src="/icons/wallcut.webp" width={28} {...props} />
74
98
  ),
75
99
  label: 'Cutaway',
76
100
  },
77
101
  down: {
78
102
  icon: (props: any) => (
79
- <img alt="Low" height={28} src="/icons/walllow.png" width={28} {...props} />
103
+ <img alt="Low" height={28} src="/icons/walllow.webp" width={28} {...props} />
80
104
  ),
81
105
  label: 'Low',
82
106
  },
107
+ translucent: {
108
+ icon: (props: any) => (
109
+ <img alt="Translucent" height={28} src="/icons/wall.png" width={28} {...props} />
110
+ ),
111
+ label: 'Translucent',
112
+ },
83
113
  }
84
114
 
85
115
  const SHADING_OPTIONS = [
@@ -87,8 +117,14 @@ const SHADING_OPTIONS = [
87
117
  { id: 'rendered', name: 'Rendered', detail: 'Full ambient occlusion', icon: Sparkles },
88
118
  ] as const
89
119
 
120
+ const TEXTURE_OPTIONS = [
121
+ { id: true, name: 'Colored', detail: 'Show materials, textures & colors', icon: Palette },
122
+ { id: false, name: 'Monochrome', detail: 'Flat clay surfaces by role', icon: Square },
123
+ ] as const
124
+
90
125
  function RenderModeMenu() {
91
126
  const shading = useViewer((s) => s.shading)
127
+ const textures = useViewer((s) => s.textures)
92
128
  const active = SHADING_OPTIONS.find((o) => o.id === shading) ?? SHADING_OPTIONS[0]
93
129
  const ActiveIcon = active.icon
94
130
  return (
@@ -121,6 +157,23 @@ function RenderModeMenu() {
121
157
  </DropdownMenuItem>
122
158
  )
123
159
  })}
160
+ <DropdownMenuSeparator />
161
+ {TEXTURE_OPTIONS.map((option) => {
162
+ const OptionIcon = option.icon
163
+ return (
164
+ <DropdownMenuItem
165
+ key={option.name}
166
+ onSelect={() => useViewer.getState().setTextures(option.id)}
167
+ >
168
+ <OptionIcon />
169
+ <div className="flex flex-col">
170
+ <span className="text-foreground">{option.name}</span>
171
+ <span className="text-muted-foreground text-xs">{option.detail}</span>
172
+ </div>
173
+ {textures === option.id ? <Check className="ml-auto text-foreground" /> : null}
174
+ </DropdownMenuItem>
175
+ )
176
+ })}
124
177
  </DropdownMenuContent>
125
178
  </DropdownMenu>
126
179
  )
@@ -455,7 +508,7 @@ export const ViewerOverlay = ({
455
508
  <img
456
509
  alt="Scans"
457
510
  className="h-[28px] w-[28px] object-contain"
458
- src="/icons/mesh.png"
511
+ src="/icons/mesh.webp"
459
512
  />
460
513
  </ActionButton>
461
514
  )}
@@ -476,7 +529,7 @@ export const ViewerOverlay = ({
476
529
  <img
477
530
  alt="Guides"
478
531
  className="h-[28px] w-[28px] object-contain"
479
- src="/icons/floorplan.png"
532
+ src="/icons/floorplan.webp"
480
533
  />
481
534
  </ActionButton>
482
535
  )}
@@ -554,7 +607,12 @@ export const ViewerOverlay = ({
554
607
  }
555
608
  label={`Walls: ${wallModeConfig[wallMode as keyof typeof wallModeConfig].label}`}
556
609
  onClick={() => {
557
- const modes: ('cutaway' | 'up' | 'down')[] = ['cutaway', 'up', 'down']
610
+ const modes: ('cutaway' | 'up' | 'down' | 'translucent')[] = [
611
+ 'cutaway',
612
+ 'up',
613
+ 'down',
614
+ 'translucent',
615
+ ]
558
616
  const nextIndex = (modes.indexOf(wallMode as any) + 1) % modes.length
559
617
  useViewer.getState().setWallMode(modes[nextIndex] ?? 'cutaway')
560
618
  }}
@@ -582,7 +640,7 @@ export const ViewerOverlay = ({
582
640
  <img
583
641
  alt="Orbit Left"
584
642
  className="h-[28px] w-[28px] -scale-x-100 object-contain opacity-70 transition-opacity group-hover:opacity-100"
585
- src="/icons/rotate.png"
643
+ src="/icons/rotate.webp"
586
644
  />
587
645
  </ActionButton>
588
646
 
@@ -597,7 +655,7 @@ export const ViewerOverlay = ({
597
655
  <img
598
656
  alt="Orbit Right"
599
657
  className="h-[28px] w-[28px] object-contain opacity-70 transition-opacity group-hover:opacity-100"
600
- src="/icons/rotate.png"
658
+ src="/icons/rotate.webp"
601
659
  />
602
660
  </ActionButton>
603
661
 
@@ -612,9 +670,26 @@ export const ViewerOverlay = ({
612
670
  <img
613
671
  alt="Top View"
614
672
  className="h-[28px] w-[28px] object-contain opacity-70 transition-opacity group-hover:opacity-100"
615
- src="/icons/topview.png"
673
+ src="/icons/topview.webp"
616
674
  />
617
675
  </ActionButton>
676
+
677
+ <div className="mx-1 h-5 w-px bg-border/40" />
678
+
679
+ {/* First-person walkthrough */}
680
+ <ActionButton
681
+ className="hover:bg-white/5 hover:text-emerald-400"
682
+ label="Walkthrough"
683
+ onClick={() => {
684
+ flushSync(() => useEditor.getState().setFirstPersonMode(true))
685
+ requestWalkthroughPointerLock()
686
+ }}
687
+ size="icon"
688
+ tooltipSide="top"
689
+ variant="ghost"
690
+ >
691
+ <Footprints className="h-6 w-6" />
692
+ </ActionButton>
618
693
  </div>
619
694
  </TooltipProvider>
620
695
  </div>
@@ -4,13 +4,18 @@ import { sceneRegistry, useScene, type ZoneNode } from '@pascal-app/core'
4
4
  import { useViewer } from '@pascal-app/viewer'
5
5
  import { useFrame } from '@react-three/fiber'
6
6
  import type { Mesh } from 'three'
7
+ import { resolveOverlayPolicy } from '../lib/interaction/overlay-policy'
7
8
  import useEditor from '../store/use-editor'
9
+ import useInteractionScope from '../store/use-interaction-scope'
8
10
 
9
11
  export const ViewerZoneSystem = () => {
10
12
  useFrame(() => {
11
13
  const { levelId, zoneId } = useViewer.getState().selection
12
14
  const structureLayer = useEditor.getState().structureLayer
13
15
  const nodes = useScene.getState().nodes
16
+ // During any active interaction zone labels step back entirely (Sims-light).
17
+ const zoneLabelsHidden =
18
+ resolveOverlayPolicy(useInteractionScope.getState().scope).zoneLabels === 'hidden'
14
19
 
15
20
  sceneRegistry.byType.zone!.forEach((id) => {
16
21
  const obj = sceneRegistry.nodes.get(id)
@@ -35,7 +40,7 @@ export const ViewerZoneSystem = () => {
35
40
  })
36
41
 
37
42
  // Labels: always visible on the current level (regardless of mode or zone selection)
38
- const showLabel = !!levelId && isOnSelectedLevel
43
+ const showLabel = !zoneLabelsHidden && !!levelId && isOnSelectedLevel
39
44
  const targetOpacity = showLabel ? '1' : '0'
40
45
  const labelEl = document.getElementById(`${id}-label`)
41
46
  if (labelEl && labelEl.style.opacity !== targetOpacity) {
@@ -0,0 +1,14 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+ import { isSuspiciousNodeDrop } from './use-auto-save'
3
+
4
+ describe('isSuspiciousNodeDrop', () => {
5
+ test('blocks populated scenes from being flushed as empty skeletons', () => {
6
+ expect(isSuspiciousNodeDrop(12, 0)).toBe(true)
7
+ expect(isSuspiciousNodeDrop(12, 4)).toBe(true)
8
+ })
9
+
10
+ test('allows ordinary edits and intentionally empty starting scenes', () => {
11
+ expect(isSuspiciousNodeDrop(12, 11)).toBe(false)
12
+ expect(isSuspiciousNodeDrop(4, 0)).toBe(false)
13
+ })
14
+ })
@@ -5,6 +5,11 @@ import { type MutableRefObject, useCallback, useEffect, useRef } from 'react'
5
5
  import { type SceneGraph, saveSceneToLocalStorage } from '../lib/scene'
6
6
 
7
7
  const AUTOSAVE_DEBOUNCE_MS = 1000
8
+ const STRUCTURAL_NODE_COUNT = 4
9
+
10
+ export function isSuspiciousNodeDrop(previousNodeCount: number, currentNodeCount: number) {
11
+ return previousNodeCount > STRUCTURAL_NODE_COUNT && currentNodeCount <= STRUCTURAL_NODE_COUNT
12
+ }
8
13
 
9
14
  export type SaveStatus = 'idle' | 'pending' | 'saving' | 'saved' | 'paused' | 'error'
10
15
 
@@ -61,6 +66,13 @@ export function useAutoSave({
61
66
  useEffect(() => {
62
67
  let lastNodesSnapshot = JSON.stringify(useScene.getState().nodes)
63
68
  let lastNodeCount = Object.keys(useScene.getState().nodes).length
69
+ // Collections + scene materials are document-level state that persists with
70
+ // the graph but lives outside `nodes`. Track them by reference (zustand
71
+ // hands out a new object on every mutation) so a material edit or a
72
+ // collection change still triggers a save.
73
+ let lastCollectionsRef = useScene.getState().collections
74
+ let lastMaterialsRef = useScene.getState().materials
75
+ let lastInstalledPluginsRef = useScene.getState().installedPlugins
64
76
 
65
77
  async function executeSave() {
66
78
  if (isLoadingSceneRef.current || isVersionPreviewModeRef.current) {
@@ -69,14 +81,19 @@ export function useAutoSave({
69
81
  return
70
82
  }
71
83
 
72
- const { nodes, rootNodeIds } = useScene.getState()
73
- const sceneGraph = { nodes, rootNodeIds } as SceneGraph
84
+ const { nodes, rootNodeIds, collections, materials, installedPlugins } = useScene.getState()
85
+ const sceneGraph = {
86
+ nodes,
87
+ rootNodeIds,
88
+ collections,
89
+ materials,
90
+ installedPlugins,
91
+ } as SceneGraph
74
92
 
75
93
  // Guard: refuse to autosave if the scene went from populated to nearly empty.
76
94
  // This catches accidental full deletions before they're persisted.
77
95
  const currentNodeCount = Object.keys(nodes).length
78
- const STRUCTURAL_NODE_COUNT = 4 // site + building + levels (empty scene skeleton)
79
- if (lastNodeCount > STRUCTURAL_NODE_COUNT && currentNodeCount <= STRUCTURAL_NODE_COUNT) {
96
+ if (isSuspiciousNodeDrop(lastNodeCount, currentNodeCount)) {
80
97
  console.warn(
81
98
  `[autosave] Blocked: scene dropped from ${lastNodeCount} to ${currentNodeCount} nodes. Likely accidental deletion.`,
82
99
  )
@@ -118,19 +135,33 @@ export function useAutoSave({
118
135
  const unsubscribe = useScene.subscribe((state) => {
119
136
  if (isLoadingSceneRef.current) {
120
137
  lastNodesSnapshot = JSON.stringify(state.nodes)
138
+ lastCollectionsRef = state.collections
139
+ lastMaterialsRef = state.materials
140
+ lastInstalledPluginsRef = state.installedPlugins
121
141
  return
122
142
  }
123
143
 
124
144
  if (isVersionPreviewModeRef.current) {
125
145
  setSaveStatus('paused')
126
146
  lastNodesSnapshot = JSON.stringify(state.nodes)
147
+ lastCollectionsRef = state.collections
148
+ lastMaterialsRef = state.materials
149
+ lastInstalledPluginsRef = state.installedPlugins
127
150
  return
128
151
  }
129
152
 
130
153
  const currentNodesSnapshot = JSON.stringify(state.nodes)
131
- if (currentNodesSnapshot === lastNodesSnapshot) return
154
+ const changed =
155
+ currentNodesSnapshot !== lastNodesSnapshot ||
156
+ state.collections !== lastCollectionsRef ||
157
+ state.materials !== lastMaterialsRef ||
158
+ state.installedPlugins !== lastInstalledPluginsRef
159
+ if (!changed) return
132
160
 
133
161
  lastNodesSnapshot = currentNodesSnapshot
162
+ lastCollectionsRef = state.collections
163
+ lastMaterialsRef = state.materials
164
+ lastInstalledPluginsRef = state.installedPlugins
134
165
  hasDirtyChangesRef.current = true
135
166
  onDirtyRef.current?.()
136
167
  setSaveStatus('pending')
@@ -155,9 +186,25 @@ export function useAutoSave({
155
186
  // (mobile Safari, bfcache) where `beforeunload` does not.
156
187
  function flushOnExit() {
157
188
  if (!hasDirtyChangesRef.current) return
189
+ const { nodes, rootNodeIds, collections, materials, installedPlugins } = useScene.getState()
190
+ const currentNodeCount = Object.keys(nodes).length
191
+ if (isSuspiciousNodeDrop(lastNodeCount, currentNodeCount)) {
192
+ console.warn(
193
+ `[autosave] Blocked unload flush: scene dropped from ${lastNodeCount} to ${currentNodeCount} nodes. Likely accidental deletion.`,
194
+ )
195
+ setSaveStatus('error')
196
+ return
197
+ }
198
+
158
199
  hasDirtyChangesRef.current = false
159
- const { nodes, rootNodeIds } = useScene.getState()
160
- const sceneGraph = { nodes, rootNodeIds } as SceneGraph
200
+ lastNodeCount = currentNodeCount
201
+ const sceneGraph = {
202
+ nodes,
203
+ rootNodeIds,
204
+ collections,
205
+ materials,
206
+ installedPlugins,
207
+ } as SceneGraph
161
208
  if (onSaveRef.current) {
162
209
  onSaveRef.current(sceneGraph, { keepalive: true }).catch(() => {})
163
210
  } else {
@@ -12,6 +12,7 @@ import { useThree } from '@react-three/fiber'
12
12
  import { useEffect, useRef } from 'react'
13
13
  import { type Object3D, Plane, Raycaster, Vector2, Vector3 } from 'three'
14
14
  import useEditor from '../store/use-editor'
15
+ import { getMovingNode } from '../store/use-interaction-scope'
15
16
 
16
17
  const UP = new Vector3(0, 1, 0)
17
18
 
@@ -59,7 +60,7 @@ export function useCeilingEvents() {
59
60
  const isActive = (): boolean => {
60
61
  const ed = useEditor.getState()
61
62
  if (ed.selectedItem?.attachTo === 'ceiling') return true
62
- const moving = ed.movingNode
63
+ const moving = getMovingNode()
63
64
  return moving?.type === 'item' && moving.asset?.attachTo === 'ceiling'
64
65
  }
65
66
 
@@ -85,7 +86,7 @@ export function useCeilingEvents() {
85
86
 
86
87
  for (const id of ceilingIds) {
87
88
  const node = nodes[id as AnyNodeId] as CeilingNode | undefined
88
- if (!node || node.type !== 'ceiling' || node.polygon.length < 3) continue
89
+ if (node?.type !== 'ceiling' || node.polygon.length < 3) continue
89
90
  if (resolveLevelId(node, nodes) !== activeLevelId) continue
90
91
  const mesh = sceneRegistry.nodes.get(id)
91
92
  if (!mesh) continue
@@ -19,7 +19,10 @@ import { useEffect, useRef } from 'react'
19
19
  const sceneApi = createSceneApi(useScene)
20
20
 
21
21
  function modifiersFromGridEvent(event: GridEvent): Modifiers {
22
- const ne = event.nativeEvent?.nativeEvent as Partial<KeyboardEvent> | undefined
22
+ // Both grid-event emit paths (use-grid-events.ts and the floorplan panel)
23
+ // store the raw DOM event directly at `.nativeEvent` — there is no second
24
+ // `.nativeEvent` hop.
25
+ const ne = event.nativeEvent as Partial<MouseEvent> | undefined
23
26
  return {
24
27
  shift: ne?.shiftKey ?? false,
25
28
  alt: ne?.altKey ?? false,