@pascal-app/editor 0.9.2 → 1.0.0-beta.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 (203) hide show
  1. package/package.json +9 -7
  2. package/src/components/editor/bake-exporter.tsx +3 -1
  3. package/src/components/editor/bake-thumbnail.tsx +95 -0
  4. package/src/components/editor/camera-dragging-lifecycle.test.ts +49 -0
  5. package/src/components/editor/camera-dragging-lifecycle.ts +41 -0
  6. package/src/components/editor/custom-camera-controls.tsx +307 -212
  7. package/src/components/editor/delete-confirmation-dialog.tsx +54 -0
  8. package/src/components/editor/elevation-3d-guide-layer.tsx +120 -0
  9. package/src/components/editor/first-person/build-collider-world.test.ts +66 -2
  10. package/src/components/editor/first-person/build-collider-world.ts +20 -0
  11. package/src/components/editor/first-person/terrain-collider.test.ts +132 -0
  12. package/src/components/editor/first-person/terrain-collider.ts +212 -0
  13. package/src/components/editor/first-person-controls.tsx +295 -132
  14. package/src/components/editor/floating-action-menu.tsx +59 -65
  15. package/src/components/editor/floorplan-background-selection.test.ts +16 -2
  16. package/src/components/editor/floorplan-background-selection.ts +4 -18
  17. package/src/components/editor/floorplan-camera-sync.test.ts +249 -0
  18. package/src/components/editor/floorplan-camera-sync.ts +213 -0
  19. package/src/components/editor/floorplan-mode-coordinator.tsx +82 -0
  20. package/src/components/editor/floorplan-navigation-presentation.test.ts +142 -0
  21. package/src/components/editor/floorplan-navigation-presentation.ts +156 -0
  22. package/src/components/editor/floorplan-panel.tsx +931 -861
  23. package/src/components/editor/group-actions.ts +155 -15
  24. package/src/components/editor/group-floating-action-menu.tsx +1 -1
  25. package/src/components/editor/group-rotate-handle.tsx +1 -1
  26. package/src/components/editor/group-selection-box-3d.tsx +1 -1
  27. package/src/components/editor/handles/handle-drag-history.test.ts +96 -0
  28. package/src/components/editor/handles/handle-drag-history.ts +14 -0
  29. package/src/components/editor/handles/preview-overrides.test.ts +26 -0
  30. package/src/components/editor/handles/preview-overrides.ts +13 -0
  31. package/src/components/editor/handles/resize-snap.test.ts +52 -0
  32. package/src/components/editor/handles/resize-snap.ts +23 -0
  33. package/src/components/editor/handles/use-handle-drag.ts +17 -7
  34. package/src/components/editor/index.tsx +42 -9
  35. package/src/components/editor/measurement-pill.tsx +6 -1
  36. package/src/components/editor/node-arrow-handles.tsx +51 -15
  37. package/src/components/editor/quick-measurement-card.tsx +9 -3
  38. package/src/components/editor/quick-measurement-hud.tsx +7 -1
  39. package/src/components/editor/selection-manager.tsx +3 -1
  40. package/src/components/editor/site-edge-labels.tsx +24 -2
  41. package/src/components/editor/snapshot-capture-overlay.tsx +4 -1
  42. package/src/components/editor/thumbnail-generator.tsx +66 -294
  43. package/src/components/editor/use-floorplan-hit-testing.test.ts +82 -0
  44. package/src/components/editor/use-floorplan-hit-testing.ts +144 -161
  45. package/src/components/editor/use-mesh-settle-epoch.ts +1 -0
  46. package/src/components/editor/wall-measurement-label.tsx +8 -16
  47. package/src/components/editor/wall-move-side-handles.tsx +193 -18
  48. package/src/components/editor/wall-opening-highlights.tsx +63 -32
  49. package/src/components/editor/wall-snap-beacon-layer.tsx +6 -4
  50. package/src/components/editor-2d/floorplan-alignment-guide-layer.tsx +2 -1
  51. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +39 -8
  52. package/src/components/editor-2d/floorplan-cursor-indicator-position.test.ts +31 -0
  53. package/src/components/editor-2d/floorplan-cursor-indicator-position.ts +39 -0
  54. package/src/components/editor-2d/floorplan-group-move.tsx +47 -4
  55. package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +21 -5
  56. package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +72 -0
  57. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +93 -59
  58. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +27 -0
  59. package/src/components/editor-2d/floorplan-render-context.test.ts +27 -0
  60. package/src/components/editor-2d/floorplan-render-context.tsx +113 -8
  61. package/src/components/editor-2d/renderers/floorplan-annotation-layout.test.ts +285 -0
  62. package/src/components/editor-2d/renderers/floorplan-annotation-layout.ts +516 -0
  63. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.test.tsx +236 -0
  64. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +618 -0
  65. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.test.tsx +369 -0
  66. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +431 -32
  67. package/src/components/editor-2d/renderers/floorplan-label-angle.test.ts +75 -1
  68. package/src/components/editor-2d/renderers/floorplan-label-angle.ts +92 -0
  69. package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +93 -28
  70. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +279 -0
  71. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +621 -281
  72. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +20 -10
  73. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +12 -7
  74. package/src/components/tools/fence/fence-drafting.ts +27 -0
  75. package/src/components/tools/item/use-draft-node.ts +131 -72
  76. package/src/components/tools/item/use-placement-coordinator.tsx +52 -4
  77. package/src/components/tools/registry/move-registry-node-tool.tsx +75 -4
  78. package/src/components/tools/roof/roof-tool.tsx +16 -0
  79. package/src/components/tools/shared/floor-stack-preview.ts +4 -0
  80. package/src/components/tools/shared/horizontal-construction-plane.ts +123 -0
  81. package/src/components/tools/shared/pointer-support-cap.test.ts +98 -0
  82. package/src/components/tools/shared/pointer-support-cap.ts +273 -0
  83. package/src/components/tools/site/site-boundary-editor.tsx +13 -6
  84. package/src/components/tools/site/stroke-pointer-ownership.test.ts +91 -0
  85. package/src/components/tools/site/stroke-pointer-ownership.ts +57 -0
  86. package/src/components/tools/site/terrain-brush-cursor.tsx +247 -0
  87. package/src/components/tools/site/terrain-sculpt-grid.tsx +192 -0
  88. package/src/components/tools/site/terrain-sculpt-tool.tsx +356 -0
  89. package/src/components/tools/stair/stair-click-guard.test.ts +58 -0
  90. package/src/components/tools/stair/stair-click-guard.ts +72 -0
  91. package/src/components/tools/stair/stair-tool.tsx +98 -47
  92. package/src/components/tools/tool-manager.tsx +67 -9
  93. package/src/components/tools/wall/wall-drafting.test.ts +118 -1
  94. package/src/components/tools/wall/wall-drafting.ts +128 -9
  95. package/src/components/tools/wall/wall-snap-geometry.test.ts +8 -0
  96. package/src/components/tools/wall/wall-snap-geometry.ts +70 -3
  97. package/src/components/tools/zone/zone-tool.tsx +16 -0
  98. package/src/components/ui/action-menu/control-modes.tsx +4 -1
  99. package/src/components/ui/action-menu/measurement-control.tsx +107 -28
  100. package/src/components/ui/command-palette/editor-commands.tsx +14 -1
  101. package/src/components/ui/controls/material-paint-panel.tsx +7 -1
  102. package/src/components/ui/controls/material-picker.tsx +86 -7
  103. package/src/components/ui/controls/terrain-sculpt-panel.tsx +193 -0
  104. package/src/components/ui/floating-level-selector.tsx +73 -19
  105. package/src/components/ui/helpers/helper-manager.tsx +46 -0
  106. package/src/components/ui/panels/multi-selection-panel.tsx +57 -0
  107. package/src/components/ui/panels/panel-manager.tsx +14 -1
  108. package/src/components/ui/panels/parametric-inspector.tsx +4 -2
  109. package/src/components/ui/panels/selection-breakdown.test.ts +24 -0
  110. package/src/components/ui/panels/selection-breakdown.ts +20 -0
  111. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +14 -4
  112. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +16 -0
  113. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +2 -1
  114. package/src/components/ui/sidebar/panels/site-panel/index.tsx +2 -0
  115. package/src/components/viewer/viewer-controls-bar.tsx +466 -0
  116. package/src/components/viewer/viewer-scene-header.tsx +235 -0
  117. package/src/components/viewer-overlay.tsx +20 -655
  118. package/src/components/walkthrough-hud.tsx +111 -0
  119. package/src/hooks/use-grid-events.ts +24 -0
  120. package/src/hooks/use-keyboard.ts +53 -30
  121. package/src/index.tsx +137 -4
  122. package/src/lib/active-placement-surface.test.ts +24 -0
  123. package/src/lib/active-placement-surface.ts +8 -1
  124. package/src/lib/camera-pose.test.ts +237 -0
  125. package/src/lib/camera-pose.ts +172 -0
  126. package/src/lib/direct-manipulation.test.ts +83 -0
  127. package/src/lib/direct-manipulation.ts +9 -0
  128. package/src/lib/door-interaction.ts +1 -1
  129. package/src/lib/elevation-guides.test.ts +107 -0
  130. package/src/lib/elevation-guides.ts +265 -0
  131. package/src/lib/floorplan/annotation-visibility.test.ts +327 -0
  132. package/src/lib/floorplan/annotation-visibility.ts +133 -0
  133. package/src/lib/floorplan/drawing-coordination.test.ts +46 -0
  134. package/src/lib/floorplan/drawing-coordination.ts +13 -0
  135. package/src/lib/floorplan/floorplan-export.test.ts +319 -0
  136. package/src/lib/floorplan/floorplan-export.tsx +745 -131
  137. package/src/lib/floorplan/floorplan-extension.test.ts +43 -0
  138. package/src/lib/floorplan/floorplan-extension.ts +181 -0
  139. package/src/lib/floorplan/floorplan-mode.test.ts +120 -0
  140. package/src/lib/floorplan/floorplan-mode.ts +73 -0
  141. package/src/lib/floorplan/floorplan-pdfkit-document.ts +194 -0
  142. package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +107 -0
  143. package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +680 -0
  144. package/src/lib/floorplan/geometry.ts +2 -3
  145. package/src/lib/fresh-planar-placement.test.ts +44 -1
  146. package/src/lib/fresh-planar-placement.ts +21 -4
  147. package/src/lib/glb-export.test.ts +116 -1
  148. package/src/lib/glb-export.ts +204 -7
  149. package/src/lib/ground-surface.ts +95 -0
  150. package/src/lib/history.test.ts +96 -0
  151. package/src/lib/history.ts +22 -0
  152. package/src/lib/interaction/overlay-policy.test.ts +2 -1
  153. package/src/lib/interaction/scope.ts +40 -8
  154. package/src/lib/level-duplication.test.ts +2 -1
  155. package/src/lib/measurements.test.ts +5 -0
  156. package/src/lib/measurements.ts +12 -1
  157. package/src/lib/quick-action-feedback.ts +33 -0
  158. package/src/lib/quick-action-nodes.test.ts +94 -0
  159. package/src/lib/quick-action-nodes.ts +47 -0
  160. package/src/lib/scene-clipboard.test.ts +130 -1
  161. package/src/lib/scene-clipboard.ts +237 -13
  162. package/src/lib/selection-routing.test.ts +16 -1
  163. package/src/lib/selection-routing.ts +5 -0
  164. package/src/lib/sfx-bus.ts +33 -2
  165. package/src/lib/sfx-player.test.ts +148 -6
  166. package/src/lib/sfx-player.ts +156 -8
  167. package/src/lib/site-boundary.test.ts +53 -0
  168. package/src/lib/site-boundary.ts +27 -0
  169. package/src/lib/snapping-mode.test.ts +29 -2
  170. package/src/lib/snapping-mode.ts +15 -3
  171. package/src/lib/stair-duplication.test.ts +103 -0
  172. package/src/lib/stair-duplication.ts +57 -85
  173. package/src/lib/stair-levels.ts +2 -0
  174. package/src/lib/terrain-sculpt.test.ts +429 -0
  175. package/src/lib/terrain-sculpt.ts +301 -0
  176. package/src/lib/terrain-verb-color.test.ts +51 -0
  177. package/src/lib/terrain-verb-color.ts +58 -0
  178. package/src/lib/touch-gesture-priority.test.ts +45 -0
  179. package/src/lib/touch-gesture-priority.ts +30 -0
  180. package/src/lib/window-interaction.ts +1 -1
  181. package/src/store/camera-pose-store.test.ts +29 -0
  182. package/src/store/camera-pose-store.ts +27 -0
  183. package/src/store/live-draft-preview-stores.test.ts +136 -0
  184. package/src/store/navigation-sync-pose-store.test.ts +34 -0
  185. package/src/store/navigation-sync-pose-store.ts +27 -0
  186. package/src/store/terrain-sculpt-mode.test.ts +223 -0
  187. package/src/store/use-delete-confirmation.ts +27 -0
  188. package/src/store/use-drawing-view.test.ts +23 -0
  189. package/src/store/use-drawing-view.ts +84 -0
  190. package/src/store/use-editor.tsx +184 -15
  191. package/src/store/use-elevation-guides.ts +40 -0
  192. package/src/store/use-fence-curve-draft.ts +38 -8
  193. package/src/store/use-first-person-hud.ts +40 -0
  194. package/src/store/use-floorplan-annotation-visibility.ts +60 -0
  195. package/src/store/use-floorplan-draft-preview.ts +54 -4
  196. package/src/store/use-floorplan-mode.ts +110 -0
  197. package/src/store/use-interaction-scope.test.ts +23 -3
  198. package/src/store/use-interaction-scope.ts +8 -0
  199. package/src/store/use-measurement-draft.test.ts +29 -0
  200. package/src/store/use-measurement-draft.ts +33 -6
  201. package/src/store/use-path-draft-preview.ts +90 -0
  202. package/src/store/use-stair-build-preview.ts +12 -2
  203. package/src/lib/floorplan/selection-tool.ts +0 -271
@@ -4,6 +4,7 @@ import type { AssetInput } from '@pascal-app/core'
4
4
  import {
5
5
  type AnyNode,
6
6
  type AnyNodeId,
7
+ type BrushSettings,
7
8
  type BuildingNode,
8
9
  type CabinetModuleNode,
9
10
  type CabinetNode,
@@ -11,6 +12,7 @@ import {
11
12
  type ChimneyMaterialRole,
12
13
  type ChimneyNode,
13
14
  type ColumnNode,
15
+ DEFAULT_BRUSH_SETTINGS,
14
16
  type DoorNode,
15
17
  type DormerNode,
16
18
  type DormerSurfaceMaterialRole,
@@ -28,6 +30,7 @@ import {
28
30
  type StairNode,
29
31
  type StairSegmentNode,
30
32
  type StairSurfaceMaterialRole,
33
+ type TerrainVerb,
31
34
  useScene,
32
35
  type WallNode,
33
36
  type WallSurfaceSide,
@@ -69,6 +72,7 @@ import {
69
72
  snapContextOf,
70
73
  snappingModesFor,
71
74
  } from '../lib/snapping-mode'
75
+ import { publishNavigationSyncPoseToStore } from './navigation-sync-pose-store'
72
76
  import useInteractionScope from './use-interaction-scope'
73
77
 
74
78
  const DEFAULT_ACTIVE_SIDEBAR_PANEL = 'build'
@@ -137,10 +141,24 @@ export type CaptureMode =
137
141
 
138
142
  export type Phase = 'site' | 'structure' | 'furnish'
139
143
 
140
- export type Mode = 'select' | 'edit' | 'delete' | 'build' | 'material-paint'
144
+ /**
145
+ * `terrain-sculpt` is a mode, not a build tool, and that is the whole answer to
146
+ * "how does terrain editing avoid conflicting with everything else".
147
+ *
148
+ * A build tool places a node and hands the pointer back. Sculpting is a
149
+ * sustained brush over the *ground* — the one surface every other tool uses as
150
+ * its reference plane — so while it is armed, clicks must not select a wall,
151
+ * drag a window, or arm a ghost. Modeling it as a mode gets that for free: it is
152
+ * mutually exclusive with `build`/`select`/`delete` by construction, every
153
+ * selection manager already early-returns unless `mode === 'select'`, and it
154
+ * holds a `sculpting` interaction scope for its whole lifetime so conflicting
155
+ * controls stay stepped back. `material-paint` is the existing precedent for
156
+ * exactly this shape.
157
+ */
158
+ export type Mode = 'select' | 'edit' | 'delete' | 'build' | 'material-paint' | 'terrain-sculpt'
141
159
 
142
160
  // Structure mode tools (building elements)
143
- export type StructureTool =
161
+ type BuiltInStructureTool =
144
162
  | 'wall'
145
163
  | 'fence'
146
164
  | 'room'
@@ -149,6 +167,7 @@ export type StructureTool =
149
167
  | 'ceiling'
150
168
  | 'roof'
151
169
  | 'column'
170
+ | 'structural-grid'
152
171
  | 'elevator'
153
172
  | 'stair'
154
173
  | 'item'
@@ -178,6 +197,9 @@ export type StructureTool =
178
197
  | 'pipe-fitting'
179
198
  | 'pipe-trap'
180
199
 
200
+ /** Registry node kinds are valid build tools without central union edits. */
201
+ export type StructureTool = BuiltInStructureTool | (string & {})
202
+
181
203
  // Furnish mode tools (items and decoration)
182
204
  export type FurnishTool = 'item' | 'cabinet'
183
205
 
@@ -363,6 +385,29 @@ type EditorState = {
363
385
  paintEraser: boolean
364
386
  setPaintEraser: (eraser: boolean) => void
365
387
  primeMaterialPaintFromSelection: () => MaterialPaintSelectionSnapshot
388
+ /**
389
+ * Terrain sculpt state. Lives here rather than in the tool component so the
390
+ * bottom-bar HUD, the keyboard shortcuts, and the brush all read one source —
391
+ * the same reason the paint mode's material/scope/eraser live here.
392
+ */
393
+ terrainVerb: TerrainVerb
394
+ setTerrainVerb: (verb: TerrainVerb) => void
395
+ terrainBrush: BrushSettings
396
+ setTerrainBrush: (settings: Partial<BrushSettings>) => void
397
+ /**
398
+ * Absolute height in metres the `flatten` verb aims at. Sampled by clicking
399
+ * the ground with the eyedropper, or typed. `null` means "sample on first
400
+ * click", which is what makes flatten usable without ever opening a number
401
+ * field.
402
+ */
403
+ terrainFlattenTarget: number | null
404
+ setTerrainFlattenTarget: (metres: number | null) => void
405
+ /**
406
+ * True while the next click should sample a flatten target instead of
407
+ * sculpting. One-shot: sampling clears it.
408
+ */
409
+ terrainSampling: boolean
410
+ setTerrainSampling: (sampling: boolean) => void
366
411
  // What the cursor is over in paint mode: the scopes it offers + labels for the
367
412
  // HUD chip. `null` when not over a paintable surface (drives the "hover a
368
413
  // surface" hint). Set by the selection-manager paint hover; not persisted.
@@ -544,8 +589,12 @@ type SelectDefaultBuildingAndLevelOptions = {
544
589
  }
545
590
 
546
591
  function normalizeModeForPhase(phase: Phase, mode: Mode | undefined): Mode {
592
+ // The site phase used to hard-return `select`, which made a site-phase brush
593
+ // mode unrepresentable. It is an allowlist now rather than a free-for-all:
594
+ // `delete` and `material-paint` still have nothing to act on at site scope, so
595
+ // letting them survive here would restore a mode with no targets.
547
596
  if (phase === 'site') {
548
- return 'select'
597
+ return mode === 'terrain-sculpt' ? mode : 'select'
549
598
  }
550
599
 
551
600
  return mode === 'build' || mode === 'delete' || mode === 'material-paint' ? mode : 'select'
@@ -563,7 +612,7 @@ export function normalizePersistedEditorUiState(
563
612
  state: Partial<PersistedEditorUiState> | null | undefined,
564
613
  ): PersistedEditorUiState {
565
614
  const phase = state?.phase === 'structure' || state?.phase === 'furnish' ? state.phase : 'site'
566
- const mode = normalizeModeForPhase(phase, state?.mode)
615
+ let mode = normalizeModeForPhase(phase, state?.mode)
567
616
 
568
617
  // Migrate old isFloorplanOpen to viewMode
569
618
  let viewMode: ViewMode = '3d'
@@ -574,6 +623,13 @@ export function normalizePersistedEditorUiState(
574
623
  }
575
624
  const isFloorplanOpen = viewMode !== '3d'
576
625
 
626
+ // Both are persisted independently, and rehydrate goes through neither setter,
627
+ // so this is the third place the sculpt/2D pair has to be reconciled. The view
628
+ // wins here for the same reason it does in `setViewMode`: a brush armed over a
629
+ // hidden canvas is a mode the user cannot use, and reviving one on load is
630
+ // worse than reviving it mid-session — nothing on screen explains it.
631
+ if (mode === 'terrain-sculpt' && viewMode === '2d') mode = 'select'
632
+
577
633
  if (phase === 'site') {
578
634
  return {
579
635
  ...DEFAULT_PERSISTED_EDITOR_UI_STATE,
@@ -824,6 +880,54 @@ export function selectSiteFloorplanContext() {
824
880
  // floorplan panes render nothing meaningful for a thumbnail.
825
881
  let viewModeBeforeCapture: ViewMode | null = null
826
882
 
883
+ /**
884
+ * Hold the interaction scope that belongs to a sustained brush mode.
885
+ *
886
+ * Paint and sculpt are the two modes whose scope lifetime is the *mode*, not a
887
+ * pointer gesture. Both must be released whenever the mode changes for any
888
+ * reason — including the phase switch that resets the mode without going through
889
+ * `setMode`. A stuck `sculpting` scope would leave selection disabled across the
890
+ * whole editor, so this is one function called from every mode transition rather
891
+ * than a rule each transition remembers.
892
+ *
893
+ * The eyedropper arm rides along for the same reason: it is one-shot state whose
894
+ * UI is unmounted the moment sculpt mode ends, so it has to be cleared on every
895
+ * exit path and not just the one through `setMode`.
896
+ *
897
+ * Every `set({ mode: ... })` in this store must be followed by a call to this —
898
+ * see the callers in `setPhase`, `setStructureLayer`, `setPreviewMode`,
899
+ * `setFirstPersonMode` and `setWorkspaceMode`. The four view-swapping ones are
900
+ * the dangerous class: they unmount `ToolManager` (via `noEditing`), so the
901
+ * sculpt tool that would otherwise release the scope on unmount is gone, and a
902
+ * scope leaked there is unrecoverable without a reload.
903
+ */
904
+ function syncBrushModeScope(mode: Mode): void {
905
+ const scope = useInteractionScope.getState()
906
+ if (mode === 'material-paint') scope.begin({ kind: 'painting' })
907
+ else if (mode === 'terrain-sculpt') scope.begin({ kind: 'sculpting' })
908
+ // `isBrushMode` is the same set as the two branches above — kept as one
909
+ // predicate so an added brush mode cannot be handled here and missed there.
910
+ else {
911
+ scope.endIf((s) => s.kind === 'painting' || s.kind === 'sculpting')
912
+ if (useEditor.getState().terrainSampling) useEditor.getState().setTerrainSampling(false)
913
+ }
914
+ }
915
+
916
+ /**
917
+ * Whether `mode` is one of the two sustained brush modes.
918
+ *
919
+ * Named because callers *outside* this store need the same test: a scope is
920
+ * single-owner, so anything that calls `begin()` while a brush mode holds its
921
+ * scope silently evicts it — the brush stays armed and painting while the rest of
922
+ * the editor believes a drag is running. Almost every producer is unreachable
923
+ * under a brush mode because it needs a selection and entering the mode clears
924
+ * one, but the clipboard paths read the clipboard instead (see
925
+ * `pasteSelectionAndPickUp`), so they have to ask.
926
+ */
927
+ export function isBrushMode(mode: Mode): boolean {
928
+ return mode === 'material-paint' || mode === 'terrain-sculpt'
929
+ }
930
+
827
931
  const useEditor = create<EditorState>()(
828
932
  persist(
829
933
  (set, get) => ({
@@ -852,6 +956,11 @@ const useEditor = create<EditorState>()(
852
956
  set({ mode: 'select', tool: null, catalogCategory: null })
853
957
  }
854
958
 
959
+ // Leaving the site phase must drop a held sculpt scope: this branch
960
+ // rewrites `mode` without going through `setMode`, so without this a
961
+ // stuck `sculpting` scope would disable selection in structure phase.
962
+ syncBrushModeScope(get().mode)
963
+
855
964
  switch (phase) {
856
965
  case 'site':
857
966
  selectSiteFloorplanContext()
@@ -870,6 +979,23 @@ const useEditor = create<EditorState>()(
870
979
  },
871
980
  mode: DEFAULT_PERSISTED_EDITOR_UI_STATE.mode,
872
981
  setMode: (mode) => {
982
+ // Sculpting is a site-phase mode. Arming it from structure/furnish moves
983
+ // the phase rather than failing silently: the user asked for the ground,
984
+ // and `normalizeModeForPhase` would otherwise reject the mode on the next
985
+ // rehydrate, leaving the UI showing a mode the store does not hold.
986
+ if (mode === 'terrain-sculpt' && get().phase !== 'site') {
987
+ get().setPhase('site')
988
+ }
989
+
990
+ // Same promotion, one axis over. The brush needs the 3D canvas: in `2d`
991
+ // the pane is only `display: none`, so the mode would arm, hold its
992
+ // scope and show its HUD while no pointer event could ever reach it.
993
+ // `split` is the smallest move that satisfies it — a plain `3d` would
994
+ // throw away a floorplan the user deliberately opened.
995
+ if (mode === 'terrain-sculpt' && get().viewMode === '2d') {
996
+ set({ viewMode: 'split', isFloorplanOpen: true })
997
+ }
998
+
873
999
  set({ mode })
874
1000
 
875
1001
  const { phase, structureLayer, tool } = get()
@@ -893,9 +1019,14 @@ const useEditor = create<EditorState>()(
893
1019
  set({ tool: null })
894
1020
  }
895
1021
 
896
- const scope = useInteractionScope.getState()
897
- if (mode === 'material-paint') scope.begin({ kind: 'painting' })
898
- else scope.endIf((s) => s.kind === 'painting')
1022
+ if (mode === 'terrain-sculpt') {
1023
+ // Sculpting acts on the ground, never on a node. Clearing the
1024
+ // selection on entry is what stops the selected wall's gizmo from
1025
+ // sitting under the brush, competing for the same clicks.
1026
+ useViewer.getState().setSelection({ selectedIds: [], zoneId: null })
1027
+ }
1028
+
1029
+ syncBrushModeScope(mode)
899
1030
  },
900
1031
  tool: DEFAULT_PERSISTED_EDITOR_UI_STATE.tool,
901
1032
  setTool: (tool) => set({ tool }),
@@ -921,6 +1052,7 @@ const useEditor = create<EditorState>()(
921
1052
  set({ structureLayer: layer, tool })
922
1053
  } else {
923
1054
  set({ structureLayer: layer, mode: 'select', tool: null })
1055
+ syncBrushModeScope('select')
924
1056
  }
925
1057
 
926
1058
  const viewer = useViewer.getState()
@@ -1040,6 +1172,21 @@ const useEditor = create<EditorState>()(
1040
1172
  },
1041
1173
  paintHover: null,
1042
1174
  setPaintHover: (info) => set({ paintHover: info }),
1175
+ terrainVerb: 'flatten',
1176
+ setTerrainVerb: (verb) =>
1177
+ set({
1178
+ terrainVerb: verb,
1179
+ // Switching away from flatten drops the sampling arm — an eyedropper
1180
+ // that survives into the raise brush would swallow its first click.
1181
+ terrainSampling: verb === 'flatten' ? get().terrainSampling : false,
1182
+ }),
1183
+ terrainBrush: DEFAULT_BRUSH_SETTINGS,
1184
+ setTerrainBrush: (settings) => set({ terrainBrush: { ...get().terrainBrush, ...settings } }),
1185
+ terrainFlattenTarget: null,
1186
+ setTerrainFlattenTarget: (metres) =>
1187
+ set({ terrainFlattenTarget: metres, terrainSampling: false }),
1188
+ terrainSampling: false,
1189
+ setTerrainSampling: (sampling) => set({ terrainSampling: sampling }),
1043
1190
  canFindNode: false,
1044
1191
  setCanFindNode: (canFind) => set({ canFindNode: canFind }),
1045
1192
  selectedReferenceId: null,
@@ -1090,6 +1237,7 @@ const useEditor = create<EditorState>()(
1090
1237
  setPreviewMode: (preview) => {
1091
1238
  if (preview) {
1092
1239
  set({ isPreviewMode: true, mode: 'select', tool: null, catalogCategory: null })
1240
+ syncBrushModeScope('select')
1093
1241
  // Clear zone/item selection for clean viewer drill-down hierarchy
1094
1242
  useViewer.getState().setSelection({ selectedIds: [], zoneId: null })
1095
1243
  } else {
@@ -1132,7 +1280,14 @@ const useEditor = create<EditorState>()(
1132
1280
  })
1133
1281
  },
1134
1282
  viewMode: DEFAULT_PERSISTED_EDITOR_UI_STATE.viewMode,
1135
- setViewMode: (mode) => set({ viewMode: mode, isFloorplanOpen: mode !== '3d' }),
1283
+ setViewMode: (mode) => {
1284
+ set({ viewMode: mode, isFloorplanOpen: mode !== '3d' })
1285
+ // Going the other way, the view wins and the mode yields. Hiding the 3D
1286
+ // pane leaves the brush unreachable, and a held `sculpting` scope would
1287
+ // then keep selection suppressed in a floorplan the user is trying to
1288
+ // work in — a mode you cannot use and cannot see how to leave.
1289
+ if (mode === '2d' && get().mode === 'terrain-sculpt') get().setMode('select')
1290
+ },
1136
1291
  splitOrientation: DEFAULT_PERSISTED_EDITOR_LAYOUT_STATE.splitOrientation,
1137
1292
  setSplitOrientation: (orientation) => set({ splitOrientation: orientation }),
1138
1293
  isFloorplanOpen: DEFAULT_PERSISTED_EDITOR_UI_STATE.isFloorplanOpen,
@@ -1148,13 +1303,14 @@ const useEditor = create<EditorState>()(
1148
1303
  setRiserOpen: (open) => set({ isRiserOpen: open }),
1149
1304
  toggleRiserOpen: () => set((state) => ({ isRiserOpen: !state.isRiserOpen })),
1150
1305
  navigationSyncPose: null,
1151
- publishNavigationSyncPose: (pose) =>
1152
- set((state) => ({
1153
- navigationSyncPose: {
1154
- ...pose,
1155
- revision: (state.navigationSyncPose?.revision ?? 0) + 1,
1156
- },
1157
- })),
1306
+ publishNavigationSyncPose: (pose) => {
1307
+ const navigationSyncPose = {
1308
+ ...pose,
1309
+ revision: (get().navigationSyncPose?.revision ?? 0) + 1,
1310
+ }
1311
+ publishNavigationSyncPoseToStore(navigationSyncPose)
1312
+ set({ navigationSyncPose })
1313
+ },
1158
1314
  floorplanSelectionTool: 'click' as FloorplanSelectionTool,
1159
1315
  setFloorplanSelectionTool: (tool) => set({ floorplanSelectionTool: tool }),
1160
1316
  gridSnapStep: DEFAULT_PERSISTED_EDITOR_LAYOUT_STATE.gridSnapStep,
@@ -1229,6 +1385,7 @@ const useEditor = create<EditorState>()(
1229
1385
  tool: null,
1230
1386
  catalogCategory: null,
1231
1387
  })
1388
+ syncBrushModeScope('select')
1232
1389
  } else {
1233
1390
  const prevMode = get()._viewModeBeforeFirstPerson
1234
1391
  set({
@@ -1253,6 +1410,7 @@ const useEditor = create<EditorState>()(
1253
1410
  tool: null,
1254
1411
  catalogCategory: null,
1255
1412
  })
1413
+ syncBrushModeScope('select')
1256
1414
  // Clear selection so no edit affordances bleed into the clean canvas.
1257
1415
  useViewer.getState().setSelection({ selectedIds: [], zoneId: null })
1258
1416
  } else {
@@ -1296,6 +1454,13 @@ const useEditor = create<EditorState>()(
1296
1454
  : {}),
1297
1455
  }
1298
1456
  },
1457
+ // `mode` is persisted, but the interaction scope a brush mode holds is not
1458
+ // — it lives in a separate, non-persisted store. Rehydrating into
1459
+ // `terrain-sculpt` (or paint) without re-claiming the scope would restore
1460
+ // the brush with selection still enabled, so every dab could grab a wall.
1461
+ onRehydrateStorage: () => (state) => {
1462
+ if (state) syncBrushModeScope(state.mode)
1463
+ },
1299
1464
  partialize: (state) => ({
1300
1465
  phase: state.phase,
1301
1466
  mode: state.mode,
@@ -1378,6 +1543,10 @@ export function getActiveSnapContext(): SnapContext | null {
1378
1543
  mode: editor.mode,
1379
1544
  tool: editor.tool,
1380
1545
  profileOf: (typeOrTool) => nodeRegistry.get(typeOrTool)?.snapProfile,
1546
+ profileOfNode: (nodeId) => {
1547
+ const node = useScene.getState().nodes[nodeId as AnyNodeId]
1548
+ return node ? nodeRegistry.get(node.type)?.snapProfile : undefined
1549
+ },
1381
1550
  draftDirectionalOf: (typeOrTool) => nodeRegistry.get(typeOrTool)?.snapDraftDirectional ?? true,
1382
1551
  })
1383
1552
  }
@@ -0,0 +1,40 @@
1
+ import { create } from 'zustand'
2
+
3
+ export type ElevationGuide3D = {
4
+ ownerId: string
5
+ levelId: string
6
+ center: readonly [number, number]
7
+ direction: readonly [number, number]
8
+ elevation: number
9
+ label: string
10
+ }
11
+
12
+ type ElevationGuidesState = {
13
+ guide: ElevationGuide3D | null
14
+ publish(guide: ElevationGuide3D): void
15
+ clear(ownerId: string): void
16
+ }
17
+
18
+ const useElevationGuides = create<ElevationGuidesState>((set) => ({
19
+ guide: null,
20
+ publish: (guide) =>
21
+ set((state) => {
22
+ const current = state.guide
23
+ if (
24
+ current?.ownerId === guide.ownerId &&
25
+ current.levelId === guide.levelId &&
26
+ current.elevation === guide.elevation &&
27
+ current.label === guide.label &&
28
+ current.center[0] === guide.center[0] &&
29
+ current.center[1] === guide.center[1] &&
30
+ current.direction[0] === guide.direction[0] &&
31
+ current.direction[1] === guide.direction[1]
32
+ ) {
33
+ return state
34
+ }
35
+ return { guide }
36
+ }),
37
+ clear: (ownerId) => set((state) => (state.guide?.ownerId === ownerId ? { guide: null } : state)),
38
+ }))
39
+
40
+ export default useElevationGuides
@@ -1,21 +1,51 @@
1
- // Ephemeral store: how many points the in-progress curved-fence draft has
2
- // placed. Written by the 3D spline draft tool (`@pascal-app/nodes`
3
- // fence/tool.tsx) and read by the contextual helper so the "finish curve" hint
4
- // only surfaces once the user has actually started drawing. Reset on commit,
5
- // cancel, and unmount — never persisted, never in undo history.
1
+ // Ephemeral store for the in-progress curved-fence draft. Written by the 3D
2
+ // spline tool and read by the contextual helper plus out-of-tree consumers.
3
+ // Reset on commit, cancel, and unmount never persisted or recorded in undo
4
+ // history.
6
5
 
7
6
  import { create } from 'zustand'
8
7
 
8
+ export type FenceCurveDraftPoint = [number, number]
9
+
9
10
  type FenceCurveDraftState = {
10
11
  pointCount: number
11
- setPointCount(count: number): void
12
+ points: FenceCurveDraftPoint[]
13
+ cursor: FenceCurveDraftPoint | null
14
+ setDraft(points: readonly FenceCurveDraftPoint[], cursor: FenceCurveDraftPoint | null): void
12
15
  reset(): void
13
16
  }
14
17
 
18
+ function pointsEqual(a: readonly FenceCurveDraftPoint[], b: readonly FenceCurveDraftPoint[]) {
19
+ return (
20
+ a.length === b.length &&
21
+ a.every((point, index) => point[0] === b[index]?.[0] && point[1] === b[index]?.[1])
22
+ )
23
+ }
24
+
15
25
  const useFenceCurveDraft = create<FenceCurveDraftState>((set) => ({
16
26
  pointCount: 0,
17
- setPointCount: (count) => set({ pointCount: count }),
18
- reset: () => set({ pointCount: 0 }),
27
+ points: [],
28
+ cursor: null,
29
+ setDraft: (points, cursor) =>
30
+ set((state) => {
31
+ const sameCursor =
32
+ (!cursor && !state.cursor) ||
33
+ Boolean(
34
+ cursor && state.cursor && state.cursor[0] === cursor[0] && state.cursor[1] === cursor[1],
35
+ )
36
+ if (sameCursor && pointsEqual(state.points, points)) return state
37
+ return {
38
+ pointCount: points.length,
39
+ points: points.map(([x, z]) => [x, z] as FenceCurveDraftPoint),
40
+ cursor: cursor ? [cursor[0], cursor[1]] : null,
41
+ }
42
+ }),
43
+ reset: () =>
44
+ set((state) =>
45
+ state.pointCount === 0 && state.points.length === 0 && state.cursor === null
46
+ ? state
47
+ : { pointCount: 0, points: [], cursor: null },
48
+ ),
19
49
  }))
20
50
 
21
51
  export default useFenceCurveDraft
@@ -0,0 +1,40 @@
1
+ import { create } from 'zustand'
2
+
3
+ export type WalkthroughInteract = { label: string; verb: string } | null
4
+
5
+ export type FirstPersonHudState = {
6
+ floorLabel: string | null
7
+ zoneLabel: string | null
8
+ interact: WalkthroughInteract
9
+ setHud: (hud: Partial<Pick<FirstPersonHudState, 'floorLabel' | 'zoneLabel' | 'interact'>>) => void
10
+ reset: () => void
11
+ }
12
+
13
+ export const useFirstPersonHud = create<FirstPersonHudState>((set) => ({
14
+ floorLabel: null,
15
+ zoneLabel: null,
16
+ interact: null,
17
+ setHud: (hud) =>
18
+ set((state) => {
19
+ const floorLabel = hud.floorLabel === undefined ? state.floorLabel : hud.floorLabel
20
+ const zoneLabel = hud.zoneLabel === undefined ? state.zoneLabel : hud.zoneLabel
21
+ const interact = hud.interact === undefined ? state.interact : hud.interact
22
+
23
+ if (
24
+ floorLabel === state.floorLabel &&
25
+ zoneLabel === state.zoneLabel &&
26
+ interact?.label === state.interact?.label &&
27
+ interact?.verb === state.interact?.verb
28
+ ) {
29
+ return state
30
+ }
31
+
32
+ return { floorLabel, zoneLabel, interact }
33
+ }),
34
+ reset: () =>
35
+ set((state) =>
36
+ state.floorLabel === null && state.zoneLabel === null && state.interact === null
37
+ ? state
38
+ : { floorLabel: null, zoneLabel: null, interact: null },
39
+ ),
40
+ }))
@@ -0,0 +1,60 @@
1
+ 'use client'
2
+
3
+ import { create } from 'zustand'
4
+ import { persist } from 'zustand/middleware'
5
+ import {
6
+ DEFAULT_FLOORPLAN_ANNOTATION_VISIBILITY,
7
+ type FloorplanAnnotationCategory,
8
+ type FloorplanAnnotationVisibility,
9
+ normalizeFloorplanAnnotationVisibility,
10
+ } from '../lib/floorplan/annotation-visibility'
11
+ import {
12
+ DEFAULT_FLOORPLAN_WALL_DIMENSION_REFERENCE,
13
+ type FloorplanWallDimensionReference,
14
+ normalizeFloorplanWallDimensionReference,
15
+ } from '../lib/floorplan/floorplan-extension'
16
+
17
+ type FloorplanAnnotationVisibilityState = {
18
+ visibility: FloorplanAnnotationVisibility
19
+ wallDimensionReference: FloorplanWallDimensionReference
20
+ setCategory: (category: FloorplanAnnotationCategory, visible: boolean) => void
21
+ setWallDimensionReference: (reference: FloorplanWallDimensionReference) => void
22
+ reset: () => void
23
+ }
24
+
25
+ const useFloorplanAnnotationVisibility = create<FloorplanAnnotationVisibilityState>()(
26
+ persist(
27
+ (set) => ({
28
+ visibility: { ...DEFAULT_FLOORPLAN_ANNOTATION_VISIBILITY },
29
+ wallDimensionReference: DEFAULT_FLOORPLAN_WALL_DIMENSION_REFERENCE,
30
+ setCategory: (category, visible) =>
31
+ set((state) => ({ visibility: { ...state.visibility, [category]: visible } })),
32
+ setWallDimensionReference: (wallDimensionReference) => set({ wallDimensionReference }),
33
+ reset: () =>
34
+ set({
35
+ visibility: { ...DEFAULT_FLOORPLAN_ANNOTATION_VISIBILITY },
36
+ wallDimensionReference: DEFAULT_FLOORPLAN_WALL_DIMENSION_REFERENCE,
37
+ }),
38
+ }),
39
+ {
40
+ name: 'pascal-floorplan-annotation-visibility',
41
+ merge: (persistedState, currentState) => ({
42
+ ...currentState,
43
+ visibility: normalizeFloorplanAnnotationVisibility(
44
+ (persistedState as { visibility?: unknown } | undefined)?.visibility,
45
+ ),
46
+ wallDimensionReference: normalizeFloorplanWallDimensionReference(
47
+ (persistedState as { wallDimensionReference?: unknown } | undefined)
48
+ ?.wallDimensionReference,
49
+ ),
50
+ }),
51
+ partialize: (state) =>
52
+ ({
53
+ visibility: state.visibility,
54
+ wallDimensionReference: state.wallDimensionReference,
55
+ }) as FloorplanAnnotationVisibilityState,
56
+ },
57
+ ),
58
+ )
59
+
60
+ export default useFloorplanAnnotationVisibility
@@ -17,6 +17,8 @@ import { create } from 'zustand'
17
17
  /** Screen-space (SVG-local px) cursor point — drives the coordinate badge. */
18
18
  type SvgPoint = { x: number; y: number }
19
19
 
20
+ export type FloorplanPolygonDraftType = 'ceiling' | 'slab' | 'zone'
21
+
20
22
  type FloorplanDraftPreviewState = {
21
23
  /** Snapped plan-XZ point under the cursor; drives the crosshair + the
22
24
  * cursor-following polygon-draft preview. `null` when idle. */
@@ -28,11 +30,18 @@ type FloorplanDraftPreviewState = {
28
30
  cursorPosition: SvgPoint | null
29
31
  /** Live END point of the open wall / fence / roof draft segment — the per-move
30
32
  * endpoint that drives the 2D draft polygon + measurement. Each is `null`
31
- * unless that tool's draft is open. The START points stay in panel state
32
- * (set per click, low-frequency). */
33
+ * unless that tool's draft is open. The START points are mirrored here (set
34
+ * per click / per 3D draft move) so out-of-tree consumers — e.g. the hosted
35
+ * host-owned preview publishers — can observe the whole open
36
+ * segment; panel state remains the 2D interaction source of truth. */
33
37
  wallDraftEnd: WallPlanPoint | null
34
38
  fenceDraftEnd: WallPlanPoint | null
35
39
  roofDraftEnd: WallPlanPoint | null
40
+ wallDraftStart: WallPlanPoint | null
41
+ fenceDraftStart: WallPlanPoint | null
42
+ roofDraftStart: WallPlanPoint | null
43
+ polygonDraftType: FloorplanPolygonDraftType | null
44
+ polygonDraftPoints: WallPlanPoint[]
36
45
  /** Set the snapped cursor point. No-ops (skips the store update, so
37
46
  * subscribers don't re-render) when unchanged — `grid:move` fires far more
38
47
  * often than the snapped cell actually changes. */
@@ -42,11 +51,28 @@ type FloorplanDraftPreviewState = {
42
51
  setWallDraftEnd(point: WallPlanPoint | null): void
43
52
  setFenceDraftEnd(point: WallPlanPoint | null): void
44
53
  setRoofDraftEnd(point: WallPlanPoint | null): void
54
+ setWallDraftStart(point: WallPlanPoint | null): void
55
+ setFenceDraftStart(point: WallPlanPoint | null): void
56
+ setRoofDraftStart(point: WallPlanPoint | null): void
57
+ setPolygonDraft(type: FloorplanPolygonDraftType | null, points: readonly WallPlanPoint[]): void
45
58
  reset(): void
46
59
  }
47
60
 
61
+ function planPointsEqual(a: readonly WallPlanPoint[], b: readonly WallPlanPoint[]) {
62
+ return (
63
+ a.length === b.length &&
64
+ a.every((point, index) => point[0] === b[index]?.[0] && point[1] === b[index]?.[1])
65
+ )
66
+ }
67
+
48
68
  function setPlanPointField(
49
- field: 'wallDraftEnd' | 'fenceDraftEnd' | 'roofDraftEnd',
69
+ field:
70
+ | 'fenceDraftEnd'
71
+ | 'fenceDraftStart'
72
+ | 'roofDraftEnd'
73
+ | 'roofDraftStart'
74
+ | 'wallDraftEnd'
75
+ | 'wallDraftStart',
50
76
  point: WallPlanPoint | null,
51
77
  ) {
52
78
  return (
@@ -65,6 +91,11 @@ export const useFloorplanDraftPreview = create<FloorplanDraftPreviewState>((set)
65
91
  wallDraftEnd: null,
66
92
  fenceDraftEnd: null,
67
93
  roofDraftEnd: null,
94
+ wallDraftStart: null,
95
+ fenceDraftStart: null,
96
+ roofDraftStart: null,
97
+ polygonDraftType: null,
98
+ polygonDraftPoints: [],
68
99
  setCursorPoint: (point) =>
69
100
  set((state) => {
70
101
  const prev = state.cursorPoint
@@ -82,13 +113,27 @@ export const useFloorplanDraftPreview = create<FloorplanDraftPreviewState>((set)
82
113
  setWallDraftEnd: (point) => set(setPlanPointField('wallDraftEnd', point)),
83
114
  setFenceDraftEnd: (point) => set(setPlanPointField('fenceDraftEnd', point)),
84
115
  setRoofDraftEnd: (point) => set(setPlanPointField('roofDraftEnd', point)),
116
+ setWallDraftStart: (point) => set(setPlanPointField('wallDraftStart', point)),
117
+ setFenceDraftStart: (point) => set(setPlanPointField('fenceDraftStart', point)),
118
+ setRoofDraftStart: (point) => set(setPlanPointField('roofDraftStart', point)),
119
+ setPolygonDraft: (type, points) =>
120
+ set((state) =>
121
+ state.polygonDraftType === type && planPointsEqual(state.polygonDraftPoints, points)
122
+ ? state
123
+ : { polygonDraftType: type, polygonDraftPoints: points.map(([x, z]) => [x, z]) },
124
+ ),
85
125
  reset: () =>
86
126
  set((state) =>
87
127
  state.cursorPoint === null &&
88
128
  state.cursorPosition === null &&
89
129
  state.wallDraftEnd === null &&
90
130
  state.fenceDraftEnd === null &&
91
- state.roofDraftEnd === null
131
+ state.roofDraftEnd === null &&
132
+ state.wallDraftStart === null &&
133
+ state.fenceDraftStart === null &&
134
+ state.roofDraftStart === null &&
135
+ state.polygonDraftType === null &&
136
+ state.polygonDraftPoints.length === 0
92
137
  ? state
93
138
  : {
94
139
  cursorPoint: null,
@@ -96,6 +141,11 @@ export const useFloorplanDraftPreview = create<FloorplanDraftPreviewState>((set)
96
141
  wallDraftEnd: null,
97
142
  fenceDraftEnd: null,
98
143
  roofDraftEnd: null,
144
+ wallDraftStart: null,
145
+ fenceDraftStart: null,
146
+ roofDraftStart: null,
147
+ polygonDraftType: null,
148
+ polygonDraftPoints: [],
99
149
  },
100
150
  ),
101
151
  }))