@pascal-app/editor 0.9.2 → 1.0.0-beta.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.
- package/package.json +9 -7
- package/src/components/editor/bake-exporter.tsx +3 -1
- package/src/components/editor/bake-thumbnail.tsx +95 -0
- package/src/components/editor/camera-dragging-lifecycle.test.ts +49 -0
- package/src/components/editor/camera-dragging-lifecycle.ts +41 -0
- package/src/components/editor/custom-camera-controls.tsx +307 -212
- package/src/components/editor/delete-confirmation-dialog.tsx +54 -0
- package/src/components/editor/elevation-3d-guide-layer.tsx +120 -0
- package/src/components/editor/first-person/build-collider-world.test.ts +66 -2
- package/src/components/editor/first-person/build-collider-world.ts +20 -0
- package/src/components/editor/first-person/terrain-collider.test.ts +132 -0
- package/src/components/editor/first-person/terrain-collider.ts +212 -0
- package/src/components/editor/first-person-controls.tsx +295 -132
- package/src/components/editor/floating-action-menu.tsx +59 -65
- package/src/components/editor/floorplan-background-selection.test.ts +16 -2
- package/src/components/editor/floorplan-background-selection.ts +4 -18
- package/src/components/editor/floorplan-camera-sync.test.ts +249 -0
- package/src/components/editor/floorplan-camera-sync.ts +213 -0
- package/src/components/editor/floorplan-mode-coordinator.tsx +82 -0
- package/src/components/editor/floorplan-navigation-presentation.test.ts +142 -0
- package/src/components/editor/floorplan-navigation-presentation.ts +156 -0
- package/src/components/editor/floorplan-panel.tsx +931 -861
- package/src/components/editor/group-actions.ts +155 -15
- package/src/components/editor/group-floating-action-menu.tsx +1 -1
- package/src/components/editor/group-rotate-handle.tsx +1 -1
- package/src/components/editor/group-selection-box-3d.tsx +1 -1
- package/src/components/editor/handles/handle-drag-history.test.ts +96 -0
- package/src/components/editor/handles/handle-drag-history.ts +14 -0
- package/src/components/editor/handles/preview-overrides.test.ts +26 -0
- package/src/components/editor/handles/preview-overrides.ts +13 -0
- package/src/components/editor/handles/resize-snap.test.ts +52 -0
- package/src/components/editor/handles/resize-snap.ts +23 -0
- package/src/components/editor/handles/use-handle-drag.ts +17 -7
- package/src/components/editor/index.tsx +42 -9
- package/src/components/editor/measurement-pill.tsx +6 -1
- package/src/components/editor/node-arrow-handles.tsx +51 -15
- package/src/components/editor/quick-measurement-card.tsx +9 -3
- package/src/components/editor/quick-measurement-hud.tsx +7 -1
- package/src/components/editor/selection-manager.tsx +3 -1
- package/src/components/editor/site-edge-labels.tsx +24 -2
- package/src/components/editor/snapshot-capture-overlay.tsx +4 -1
- package/src/components/editor/thumbnail-generator.tsx +66 -294
- package/src/components/editor/use-floorplan-hit-testing.test.ts +82 -0
- package/src/components/editor/use-floorplan-hit-testing.ts +144 -161
- package/src/components/editor/use-mesh-settle-epoch.ts +1 -0
- package/src/components/editor/wall-measurement-label.tsx +8 -16
- package/src/components/editor/wall-move-side-handles.tsx +193 -18
- package/src/components/editor/wall-opening-highlights.tsx +63 -32
- package/src/components/editor/wall-snap-beacon-layer.tsx +6 -4
- package/src/components/editor-2d/floorplan-alignment-guide-layer.tsx +2 -1
- package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +39 -8
- package/src/components/editor-2d/floorplan-cursor-indicator-position.test.ts +31 -0
- package/src/components/editor-2d/floorplan-cursor-indicator-position.ts +39 -0
- package/src/components/editor-2d/floorplan-group-move.tsx +47 -4
- package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +21 -5
- package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +72 -0
- package/src/components/editor-2d/floorplan-registry-action-menu.tsx +93 -59
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +27 -0
- package/src/components/editor-2d/floorplan-render-context.test.ts +27 -0
- package/src/components/editor-2d/floorplan-render-context.tsx +113 -8
- package/src/components/editor-2d/renderers/floorplan-annotation-layout.test.ts +285 -0
- package/src/components/editor-2d/renderers/floorplan-annotation-layout.ts +516 -0
- package/src/components/editor-2d/renderers/floorplan-dimension-renderer.test.tsx +236 -0
- package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +618 -0
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.test.tsx +369 -0
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +431 -32
- package/src/components/editor-2d/renderers/floorplan-label-angle.test.ts +75 -1
- package/src/components/editor-2d/renderers/floorplan-label-angle.ts +92 -0
- package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +93 -28
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +279 -0
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +621 -281
- package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +20 -10
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +12 -7
- package/src/components/tools/fence/fence-drafting.ts +27 -0
- package/src/components/tools/item/use-draft-node.ts +131 -72
- package/src/components/tools/item/use-placement-coordinator.tsx +52 -4
- package/src/components/tools/registry/move-registry-node-tool.tsx +75 -4
- package/src/components/tools/roof/roof-tool.tsx +16 -0
- package/src/components/tools/shared/floor-stack-preview.ts +4 -0
- package/src/components/tools/shared/horizontal-construction-plane.ts +123 -0
- package/src/components/tools/shared/pointer-support-cap.test.ts +98 -0
- package/src/components/tools/shared/pointer-support-cap.ts +273 -0
- package/src/components/tools/site/site-boundary-editor.tsx +13 -6
- package/src/components/tools/site/stroke-pointer-ownership.test.ts +91 -0
- package/src/components/tools/site/stroke-pointer-ownership.ts +57 -0
- package/src/components/tools/site/terrain-brush-cursor.tsx +247 -0
- package/src/components/tools/site/terrain-sculpt-grid.tsx +192 -0
- package/src/components/tools/site/terrain-sculpt-tool.tsx +356 -0
- package/src/components/tools/stair/stair-click-guard.test.ts +58 -0
- package/src/components/tools/stair/stair-click-guard.ts +72 -0
- package/src/components/tools/stair/stair-tool.tsx +98 -47
- package/src/components/tools/tool-manager.tsx +67 -9
- package/src/components/tools/wall/wall-drafting.test.ts +118 -1
- package/src/components/tools/wall/wall-drafting.ts +128 -9
- package/src/components/tools/wall/wall-snap-geometry.test.ts +8 -0
- package/src/components/tools/wall/wall-snap-geometry.ts +70 -3
- package/src/components/tools/zone/zone-tool.tsx +16 -0
- package/src/components/ui/action-menu/control-modes.tsx +4 -1
- package/src/components/ui/action-menu/measurement-control.tsx +107 -28
- package/src/components/ui/command-palette/editor-commands.tsx +14 -1
- package/src/components/ui/controls/material-paint-panel.tsx +7 -1
- package/src/components/ui/controls/material-picker.tsx +86 -7
- package/src/components/ui/controls/terrain-sculpt-panel.tsx +193 -0
- package/src/components/ui/floating-level-selector.tsx +73 -19
- package/src/components/ui/helpers/helper-manager.tsx +46 -0
- package/src/components/ui/panels/multi-selection-panel.tsx +57 -0
- package/src/components/ui/panels/panel-manager.tsx +14 -1
- package/src/components/ui/panels/parametric-inspector.tsx +4 -2
- package/src/components/ui/panels/selection-breakdown.test.ts +24 -0
- package/src/components/ui/panels/selection-breakdown.ts +20 -0
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +14 -4
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +16 -0
- package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +2 -1
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +2 -0
- package/src/components/viewer/viewer-controls-bar.tsx +466 -0
- package/src/components/viewer/viewer-scene-header.tsx +235 -0
- package/src/components/viewer-overlay.tsx +20 -655
- package/src/components/walkthrough-hud.tsx +111 -0
- package/src/hooks/use-grid-events.ts +24 -0
- package/src/hooks/use-keyboard.ts +53 -30
- package/src/index.tsx +137 -4
- package/src/lib/active-placement-surface.test.ts +24 -0
- package/src/lib/active-placement-surface.ts +8 -1
- package/src/lib/camera-pose.test.ts +237 -0
- package/src/lib/camera-pose.ts +172 -0
- package/src/lib/direct-manipulation.test.ts +83 -0
- package/src/lib/direct-manipulation.ts +9 -0
- package/src/lib/door-interaction.ts +1 -1
- package/src/lib/elevation-guides.test.ts +107 -0
- package/src/lib/elevation-guides.ts +236 -0
- package/src/lib/floorplan/annotation-visibility.test.ts +327 -0
- package/src/lib/floorplan/annotation-visibility.ts +133 -0
- package/src/lib/floorplan/drawing-coordination.test.ts +46 -0
- package/src/lib/floorplan/drawing-coordination.ts +13 -0
- package/src/lib/floorplan/floorplan-export.test.ts +319 -0
- package/src/lib/floorplan/floorplan-export.tsx +745 -131
- package/src/lib/floorplan/floorplan-extension.test.ts +43 -0
- package/src/lib/floorplan/floorplan-extension.ts +181 -0
- package/src/lib/floorplan/floorplan-mode.test.ts +120 -0
- package/src/lib/floorplan/floorplan-mode.ts +73 -0
- package/src/lib/floorplan/floorplan-pdfkit-document.ts +194 -0
- package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +107 -0
- package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +680 -0
- package/src/lib/floorplan/geometry.ts +2 -3
- package/src/lib/fresh-planar-placement.test.ts +44 -1
- package/src/lib/fresh-planar-placement.ts +21 -4
- package/src/lib/glb-export.test.ts +116 -1
- package/src/lib/glb-export.ts +204 -7
- package/src/lib/ground-surface.ts +95 -0
- package/src/lib/history.test.ts +96 -0
- package/src/lib/history.ts +22 -0
- package/src/lib/interaction/overlay-policy.test.ts +2 -1
- package/src/lib/interaction/scope.ts +40 -8
- package/src/lib/level-duplication.test.ts +2 -1
- package/src/lib/measurements.test.ts +5 -0
- package/src/lib/measurements.ts +12 -1
- package/src/lib/quick-action-feedback.ts +33 -0
- package/src/lib/quick-action-nodes.test.ts +94 -0
- package/src/lib/quick-action-nodes.ts +47 -0
- package/src/lib/scene-clipboard.test.ts +130 -1
- package/src/lib/scene-clipboard.ts +237 -13
- package/src/lib/selection-routing.test.ts +16 -1
- package/src/lib/selection-routing.ts +5 -0
- package/src/lib/sfx-bus.ts +33 -2
- package/src/lib/sfx-player.test.ts +148 -6
- package/src/lib/sfx-player.ts +156 -8
- package/src/lib/site-boundary.test.ts +53 -0
- package/src/lib/site-boundary.ts +27 -0
- package/src/lib/snapping-mode.test.ts +29 -2
- package/src/lib/snapping-mode.ts +15 -3
- package/src/lib/stair-duplication.test.ts +103 -0
- package/src/lib/stair-duplication.ts +57 -85
- package/src/lib/stair-levels.ts +2 -0
- package/src/lib/terrain-sculpt.test.ts +429 -0
- package/src/lib/terrain-sculpt.ts +301 -0
- package/src/lib/terrain-verb-color.test.ts +51 -0
- package/src/lib/terrain-verb-color.ts +58 -0
- package/src/lib/touch-gesture-priority.test.ts +45 -0
- package/src/lib/touch-gesture-priority.ts +30 -0
- package/src/lib/window-interaction.ts +1 -1
- package/src/store/camera-pose-store.test.ts +29 -0
- package/src/store/camera-pose-store.ts +27 -0
- package/src/store/live-draft-preview-stores.test.ts +136 -0
- package/src/store/navigation-sync-pose-store.test.ts +34 -0
- package/src/store/navigation-sync-pose-store.ts +27 -0
- package/src/store/terrain-sculpt-mode.test.ts +223 -0
- package/src/store/use-delete-confirmation.ts +27 -0
- package/src/store/use-drawing-view.test.ts +23 -0
- package/src/store/use-drawing-view.ts +84 -0
- package/src/store/use-editor.tsx +184 -15
- package/src/store/use-elevation-guides.ts +40 -0
- package/src/store/use-fence-curve-draft.ts +38 -8
- package/src/store/use-first-person-hud.ts +40 -0
- package/src/store/use-floorplan-annotation-visibility.ts +60 -0
- package/src/store/use-floorplan-draft-preview.ts +54 -4
- package/src/store/use-floorplan-mode.ts +110 -0
- package/src/store/use-interaction-scope.test.ts +23 -3
- package/src/store/use-interaction-scope.ts +8 -0
- package/src/store/use-measurement-draft.test.ts +29 -0
- package/src/store/use-measurement-draft.ts +33 -6
- package/src/store/use-path-draft-preview.ts +90 -0
- package/src/store/use-stair-build-preview.ts +12 -2
- package/src/lib/floorplan/selection-tool.ts +0 -271
package/src/store/use-editor.tsx
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
897
|
-
|
|
898
|
-
|
|
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) =>
|
|
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
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
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
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
18
|
-
|
|
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
|
|
32
|
-
*
|
|
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:
|
|
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
|
}))
|