@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.
- package/package.json +12 -9
- package/src/components/editor/alignment-3d-guide-layer.tsx +10 -2
- package/src/components/editor/bake-exporter.tsx +47 -0
- package/src/components/editor/custom-camera-controls.tsx +63 -25
- package/src/components/editor/editor-layout-mobile.tsx +5 -0
- package/src/components/editor/editor-layout-v2.tsx +18 -0
- package/src/components/editor/export-manager.tsx +71 -63
- package/src/components/editor/fence-tangent-lines-3d.tsx +87 -0
- package/src/components/editor/first-person/build-collider-world.test.ts +36 -3
- package/src/components/editor/first-person/build-collider-world.ts +32 -5
- package/src/components/editor/first-person-controls.tsx +118 -7
- package/src/components/editor/floating-action-menu.tsx +410 -33
- package/src/components/editor/floating-building-action-menu.tsx +1 -1
- package/src/components/editor/floorplan-background-selection.test.ts +58 -0
- package/src/components/editor/floorplan-background-selection.ts +3 -2
- package/src/components/editor/floorplan-panel.tsx +1804 -849
- package/src/components/editor/grid.tsx +166 -39
- package/src/components/editor/group-actions.ts +457 -0
- package/src/components/editor/group-floating-action-menu.tsx +130 -0
- package/src/components/editor/group-move-3d.ts +397 -0
- package/src/components/editor/group-rotate-handle.tsx +102 -64
- package/src/components/editor/group-selection-box-3d.tsx +173 -0
- package/src/components/editor/group-transform-shared.test.ts +192 -1
- package/src/components/editor/group-transform-shared.ts +240 -7
- package/src/components/editor/handles/handle-arrow.tsx +81 -23
- package/src/components/editor/handles/use-handle-drag.ts +37 -4
- package/src/components/editor/index.tsx +248 -27
- package/src/components/editor/measurement-pill.tsx +62 -22
- package/src/components/editor/node-action-menu.tsx +14 -1
- package/src/components/editor/node-arrow-handles.tsx +366 -173
- package/src/components/editor/opening-guides-3d-layer.tsx +144 -0
- package/src/components/editor/quick-measurement-card.tsx +70 -0
- package/src/components/editor/quick-measurement-hud.tsx +28 -0
- package/src/components/editor/riser-diagram-panel.tsx +137 -0
- package/src/components/editor/selection-manager.tsx +812 -466
- package/src/components/editor/site-edge-labels.tsx +4 -4
- package/src/components/editor/slab-hole-highlights.tsx +13 -5
- package/src/components/editor/snapshot-capture-overlay.tsx +255 -115
- package/src/components/editor/three-context-bridge.ts +22 -0
- package/src/components/editor/thumbnail-generator.tsx +136 -43
- package/src/components/editor/use-floorplan-background-placement.ts +113 -45
- package/src/components/editor/use-floorplan-scene-data.ts +5 -5
- package/src/components/editor/use-mesh-settle-epoch.ts +26 -0
- package/src/components/editor/wall-measurement-label.tsx +3 -13
- package/src/components/editor/wall-move-side-handles.tsx +36 -21
- package/src/components/editor/wall-snap-beacon-layer.tsx +160 -6
- package/src/components/editor-2d/floorplan-action-menu-layer.tsx +9 -5
- package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +1 -1
- package/src/components/editor-2d/floorplan-group-action-menu.tsx +87 -0
- package/src/components/editor-2d/floorplan-group-move.tsx +701 -0
- package/src/components/editor-2d/floorplan-measurement-tool-layer.test.ts +113 -0
- package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +1656 -0
- package/src/components/editor-2d/floorplan-quick-measure-layer.tsx +212 -0
- package/src/components/editor-2d/floorplan-registry-action-menu.tsx +169 -4
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +300 -59
- package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +5 -3
- package/src/components/editor-2d/renderers/floorplan-label-angle.test.ts +15 -0
- package/src/components/editor-2d/renderers/floorplan-label-angle.ts +14 -0
- package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +4 -1
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +308 -0
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +1704 -342
- package/src/components/editor-2d/renderers/floorplan-voronoi-layer.tsx +128 -0
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +419 -60
- package/src/components/systems/ceiling/ceiling-system.tsx +97 -4
- package/src/components/systems/roof/roof-edit-system.tsx +1768 -13
- package/src/components/systems/selection-affordance-manager.tsx +38 -0
- package/src/components/systems/zone/zone-label-editor-system.tsx +14 -1
- package/src/components/systems/zone/zone-system.tsx +42 -13
- package/src/components/tools/elevator/elevator-tool.tsx +34 -8
- package/src/components/tools/elevator/move-elevator-tool.tsx +13 -2
- package/src/components/tools/fence/fence-drafting.ts +80 -8
- package/src/components/tools/item/move-tool.tsx +17 -13
- package/src/components/tools/item/placement-math.test.ts +13 -1
- package/src/components/tools/item/placement-math.ts +28 -2
- package/src/components/tools/item/placement-strategies.ts +246 -5
- package/src/components/tools/item/placement-types.ts +13 -1
- package/src/components/tools/item/use-draft-node.ts +31 -1
- package/src/components/tools/item/use-placement-coordinator.tsx +761 -121
- package/src/components/tools/registry/move-registry-node-tool.tsx +561 -99
- package/src/components/tools/roof/roof-tool.tsx +320 -94
- package/src/components/tools/select/box-select-state.ts +18 -7
- package/src/components/tools/select/box-select-tool.tsx +176 -40
- package/src/components/tools/select/marquee-geometry.test.ts +161 -0
- package/src/components/tools/select/marquee-geometry.ts +155 -0
- package/src/components/tools/select/plane-box-select-tool.tsx +1 -8
- package/src/components/tools/select/select-candidates.ts +1 -1
- package/src/components/tools/shared/cursor-sphere.tsx +62 -26
- package/src/components/tools/shared/drag-bounding-box.tsx +28 -4
- package/src/components/tools/shared/facing-indicator.tsx +87 -0
- package/src/components/tools/shared/facing-pose-indicator.tsx +54 -0
- package/src/components/tools/shared/placement-box.tsx +183 -1
- package/src/components/tools/shared/polygon-editor.tsx +276 -35
- package/src/components/tools/site/site-boundary-editor.tsx +88 -36
- package/src/components/tools/stair/stair-defaults.ts +1 -0
- package/src/components/tools/stair/stair-tool.tsx +82 -11
- package/src/components/tools/tool-manager.tsx +86 -25
- package/src/components/tools/wall/wall-drafting.test.ts +330 -0
- package/src/components/tools/wall/wall-drafting.ts +180 -103
- package/src/components/tools/wall/wall-snap-geometry.test.ts +44 -0
- package/src/components/tools/wall/wall-snap-geometry.ts +67 -6
- package/src/components/tools/zone/zone-boundary-editor.tsx +5 -1
- package/src/components/tools/zone/zone-tool.tsx +82 -88
- package/src/components/ui/action-menu/camera-actions.tsx +24 -17
- package/src/components/ui/action-menu/control-modes.tsx +40 -93
- package/src/components/ui/action-menu/furnish-tools.tsx +5 -5
- package/src/components/ui/action-menu/index.tsx +2 -4
- package/src/components/ui/action-menu/measurement-control.tsx +188 -0
- package/src/components/ui/action-menu/structure-tools.tsx +22 -13
- package/src/components/ui/action-menu/view-toggles.tsx +42 -90
- package/src/components/ui/command-palette/editor-commands.tsx +2 -2
- package/src/components/ui/command-palette/index.tsx +4 -3
- package/src/components/ui/controls/material-paint-panel.tsx +86 -17
- package/src/components/ui/controls/material-picker.tsx +83 -152
- package/src/components/ui/controls/material-properties-editor.tsx +108 -0
- package/src/components/ui/controls/metric-control.tsx +118 -44
- package/src/components/ui/controls/scene-material-list.tsx +247 -0
- package/src/components/ui/controls/slider-control.tsx +88 -38
- package/src/components/ui/floating-level-selector.tsx +1 -1
- package/src/components/ui/helpers/building-helper.tsx +10 -23
- package/src/components/ui/helpers/contextual-helper-panel.tsx +428 -0
- package/src/components/ui/helpers/helper-manager.tsx +227 -14
- package/src/components/ui/helpers/item-helper.tsx +28 -32
- package/src/components/ui/helpers/registered-tool-helper.tsx +45 -11
- package/src/components/ui/helpers/roof-helper.tsx +10 -12
- package/src/components/ui/icon-ref.tsx +47 -0
- package/src/components/ui/item-catalog/catalog-items.tsx +39 -1
- package/src/components/ui/item-catalog/item-catalog.tsx +13 -36
- package/src/components/ui/level-duplicate-dialog.tsx +1 -1
- package/src/components/ui/panels/node-display.ts +17 -17
- package/src/components/ui/panels/panel-manager.tsx +22 -13
- package/src/components/ui/panels/panel-wrapper.tsx +24 -3
- package/src/components/ui/panels/parametric-inspector.tsx +15 -2
- package/src/components/ui/panels/reference-panel.tsx +54 -19
- package/src/components/ui/primitives/shortcut-token.tsx +39 -3
- package/src/components/ui/primitives/sidebar.tsx +1 -0
- package/src/components/ui/sidebar/app-sidebar.tsx +5 -1
- package/src/components/ui/sidebar/icon-rail.tsx +50 -31
- package/src/components/ui/sidebar/panels/plugins-panel.tsx +218 -0
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +71 -40
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +60 -6
- package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +12 -4
- package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -3
- package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +10 -7
- package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +7 -2
- package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +10 -7
- package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +10 -7
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +40 -17
- package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +31 -17
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +72 -25
- package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +7 -3
- package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +12 -8
- package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +4 -3
- package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +10 -7
- package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +2 -2
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +44 -3
- package/src/components/ui/sidebar/panels/site-panel/tree-structure.ts +36 -0
- package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +7 -2
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +3 -3
- package/src/components/ui/sidebar/tab-bar.tsx +42 -28
- package/src/components/ui/sidebar/use-plugin-panels.tsx +132 -0
- package/src/components/ui/snap-target-badge.test.tsx +40 -0
- package/src/components/ui/snap-target-badge.tsx +88 -0
- package/src/components/viewer-overlay.tsx +85 -10
- package/src/components/viewer-zone-system.tsx +6 -1
- package/src/hooks/use-auto-save.test.ts +14 -0
- package/src/hooks/use-auto-save.ts +54 -7
- package/src/hooks/use-ceiling-events.ts +3 -2
- package/src/hooks/use-drag-action.ts +4 -1
- package/src/hooks/use-keyboard.ts +385 -40
- package/src/index.tsx +219 -6
- package/src/lib/active-placement-surface.ts +35 -0
- package/src/lib/ceiling-plan-snap.ts +24 -0
- package/src/lib/contextual-help.test.ts +112 -0
- package/src/lib/contextual-help.ts +144 -0
- package/src/lib/continuation.ts +64 -0
- package/src/lib/direct-manipulation.test.ts +192 -0
- package/src/lib/direct-manipulation.ts +109 -0
- package/src/lib/editor-api.ts +23 -35
- package/src/lib/floorplan/apply-alignment.test.ts +25 -0
- package/src/lib/floorplan/apply-alignment.ts +18 -12
- package/src/lib/floorplan/floorplan-export.tsx +364 -0
- package/src/lib/floorplan/geometry.ts +53 -0
- package/src/lib/floorplan/index.ts +3 -0
- package/src/lib/floorplan/items.ts +5 -2
- package/src/lib/floorplan/plan-coords.ts +21 -0
- package/src/lib/fresh-planar-placement.test.ts +240 -3
- package/src/lib/fresh-planar-placement.ts +68 -1
- package/src/lib/glb-export.test.ts +441 -0
- package/src/lib/glb-export.ts +874 -0
- package/src/lib/history.ts +9 -0
- package/src/lib/interaction/hot-set.test.ts +133 -0
- package/src/lib/interaction/hot-set.ts +67 -0
- package/src/lib/interaction/overlay-policy.test.ts +51 -0
- package/src/lib/interaction/overlay-policy.ts +59 -0
- package/src/lib/interaction/scope.ts +211 -0
- package/src/lib/level-selection.ts +2 -2
- package/src/lib/material-paint.ts +36 -49
- package/src/lib/measurement-kind.test.ts +30 -0
- package/src/lib/measurement-kind.ts +19 -0
- package/src/lib/measurement-label.test.ts +47 -0
- package/src/lib/measurement-label.ts +68 -0
- package/src/lib/measurement-parser.ts +116 -0
- package/src/lib/measurements.test.ts +183 -0
- package/src/lib/measurements.ts +199 -0
- package/src/lib/paint-scope.test.ts +454 -0
- package/src/lib/paint-scope.ts +461 -0
- package/src/lib/placement-drag-release.ts +23 -0
- package/src/lib/planar-cursor-placement.test.ts +57 -0
- package/src/lib/plugin-panels.test.ts +19 -0
- package/src/lib/plugin-panels.ts +91 -0
- package/src/lib/quick-measurement.test.ts +77 -0
- package/src/lib/quick-measurement.ts +109 -0
- package/src/lib/roof-duplication.ts +6 -6
- package/src/lib/roof-hover-outline-proxy.ts +22 -0
- package/src/lib/roof-wall-hit.ts +164 -0
- package/src/lib/scene-clipboard.test.ts +196 -0
- package/src/lib/scene-clipboard.ts +73 -8
- package/src/lib/scene.ts +28 -5
- package/src/lib/selection-routing.test.ts +298 -0
- package/src/lib/selection-routing.ts +175 -0
- package/src/lib/sfx/index.ts +1 -0
- package/src/lib/sfx/movement-tick.test.ts +65 -0
- package/src/lib/sfx/movement-tick.ts +18 -0
- package/src/lib/sfx-bus.ts +50 -13
- package/src/lib/sfx-player.test.ts +124 -0
- package/src/lib/sfx-player.ts +111 -66
- package/src/lib/slab-plan-snap.test.ts +93 -0
- package/src/lib/slab-plan-snap.ts +108 -0
- package/src/lib/snapping-mode.test.ts +138 -0
- package/src/lib/snapping-mode.ts +179 -0
- package/src/lib/stair-duplication.ts +4 -4
- package/src/lib/surface-plan-snap.test.ts +71 -0
- package/src/lib/surface-plan-snap.ts +256 -0
- package/src/lib/use-linear-display.ts +40 -0
- package/src/lib/world-grid-snap.ts +37 -5
- package/src/store/use-direct-manipulation-feedback.ts +20 -0
- package/src/store/use-editor.tsx +484 -114
- package/src/store/use-facing-pose.ts +44 -0
- package/src/store/use-fence-curve-draft.ts +21 -0
- package/src/store/use-floorplan-draft-preview.ts +101 -0
- package/src/store/use-floorplan-marquee.ts +50 -0
- package/src/store/use-interaction-scope.test.ts +186 -0
- package/src/store/use-interaction-scope.ts +132 -0
- package/src/store/use-measurement-draft.test.ts +530 -0
- package/src/store/use-measurement-draft.ts +543 -0
- package/src/store/use-opening-guides.ts +41 -0
- package/src/store/use-placement-preview.ts +11 -3
- package/src/store/use-quick-measurement-hud.test.ts +53 -0
- package/src/store/use-quick-measurement-hud.ts +77 -0
- package/src/store/use-segment-draft-chain.ts +28 -0
- package/src/store/use-stair-build-preview.ts +43 -0
- package/src/store/use-wall-snap-indicator.ts +2 -0
- package/src/components/editor/first-person/bvh-ecctrl.tsx +0 -860
- package/src/components/editor/group-move-handle.tsx +0 -316
- package/src/components/ui/panels/paint-panel.tsx +0 -163
- package/src/lib/level-name.ts +0 -11
package/src/store/use-editor.tsx
CHANGED
|
@@ -5,6 +5,8 @@ import {
|
|
|
5
5
|
type AnyNode,
|
|
6
6
|
type AnyNodeId,
|
|
7
7
|
type BuildingNode,
|
|
8
|
+
type CabinetModuleNode,
|
|
9
|
+
type CabinetNode,
|
|
8
10
|
type CeilingNode,
|
|
9
11
|
type ChimneyMaterialRole,
|
|
10
12
|
type ChimneyNode,
|
|
@@ -16,6 +18,7 @@ import {
|
|
|
16
18
|
type FenceNode,
|
|
17
19
|
type ItemNode,
|
|
18
20
|
type LevelNode,
|
|
21
|
+
nodeRegistry,
|
|
19
22
|
type RoofNode,
|
|
20
23
|
type RoofSegmentNode,
|
|
21
24
|
type RoofSurfaceMaterialRole,
|
|
@@ -33,6 +36,13 @@ import {
|
|
|
33
36
|
import { useViewer } from '@pascal-app/viewer'
|
|
34
37
|
import { create } from 'zustand'
|
|
35
38
|
import { persist } from 'zustand/middleware'
|
|
39
|
+
import {
|
|
40
|
+
CONTINUATION_PROFILES,
|
|
41
|
+
type ContinuationContext,
|
|
42
|
+
type ContinuationMode,
|
|
43
|
+
continuationContextOf,
|
|
44
|
+
nextContinuation,
|
|
45
|
+
} from '../lib/continuation'
|
|
36
46
|
import {
|
|
37
47
|
type ActivePaintMaterial,
|
|
38
48
|
type PaintableMaterialTarget,
|
|
@@ -40,12 +50,57 @@ import {
|
|
|
40
50
|
resolvePaintTargetFromSelection,
|
|
41
51
|
type SingleSurfaceMaterialRole,
|
|
42
52
|
} from '../lib/material-paint'
|
|
43
|
-
|
|
44
|
-
|
|
53
|
+
import {
|
|
54
|
+
type CreatableMeasurementKind,
|
|
55
|
+
DEFAULT_CREATABLE_MEASUREMENT_KIND,
|
|
56
|
+
normalizeCreatableMeasurementKind,
|
|
57
|
+
} from '../lib/measurement-kind'
|
|
58
|
+
import {
|
|
59
|
+
cyclePaintScope as cyclePaintScopeValue,
|
|
60
|
+
type PaintHoverInfo,
|
|
61
|
+
type PaintScope,
|
|
62
|
+
} from '../lib/paint-scope'
|
|
63
|
+
import {
|
|
64
|
+
cycleSnappingModeIn,
|
|
65
|
+
defaultSnappingModeFor,
|
|
66
|
+
resolveSnapFlags,
|
|
67
|
+
type SnapContext,
|
|
68
|
+
type SnappingMode,
|
|
69
|
+
snapContextOf,
|
|
70
|
+
snappingModesFor,
|
|
71
|
+
} from '../lib/snapping-mode'
|
|
72
|
+
import useInteractionScope from './use-interaction-scope'
|
|
73
|
+
|
|
74
|
+
const DEFAULT_ACTIVE_SIDEBAR_PANEL = 'build'
|
|
45
75
|
const DEFAULT_FLOORPLAN_PANE_RATIO = 0.5
|
|
46
76
|
const MIN_FLOORPLAN_PANE_RATIO = 0.15
|
|
47
77
|
const MAX_FLOORPLAN_PANE_RATIO = 0.85
|
|
48
78
|
|
|
79
|
+
function resolveMovingNodeTarget(
|
|
80
|
+
node:
|
|
81
|
+
| ItemNode
|
|
82
|
+
| WindowNode
|
|
83
|
+
| DoorNode
|
|
84
|
+
| ElevatorNode
|
|
85
|
+
| CeilingNode
|
|
86
|
+
| ChimneyNode
|
|
87
|
+
| ColumnNode
|
|
88
|
+
| DormerNode
|
|
89
|
+
| SlabNode
|
|
90
|
+
| WallNode
|
|
91
|
+
| FenceNode
|
|
92
|
+
| RoofNode
|
|
93
|
+
| RoofSegmentNode
|
|
94
|
+
| SpawnNode
|
|
95
|
+
| StairNode
|
|
96
|
+
| StairSegmentNode
|
|
97
|
+
| BuildingNode
|
|
98
|
+
| CabinetNode
|
|
99
|
+
| CabinetModuleNode,
|
|
100
|
+
) {
|
|
101
|
+
return node
|
|
102
|
+
}
|
|
103
|
+
|
|
49
104
|
export type ViewMode = '3d' | '2d' | 'split'
|
|
50
105
|
export type SplitOrientation = 'horizontal' | 'vertical'
|
|
51
106
|
export type WorkspaceMode = 'edit' | 'studio'
|
|
@@ -59,9 +114,16 @@ export type WorkspaceMode = 'edit' | 'studio'
|
|
|
59
114
|
// — `ThumbnailGenerator` consults `captureMode.mode === 'preset'` and
|
|
60
115
|
// applies those constraints. Keeping it a discriminated union lets us
|
|
61
116
|
// add future modes without surfacing the choice to end users.
|
|
117
|
+
// How the captured pixels are cropped: full-frame 16:9, raw canvas viewport,
|
|
118
|
+
// or user-dragged area. Hosts (e.g. the studio capture bar) can preselect it
|
|
119
|
+
// when entering capture mode.
|
|
120
|
+
export type SnapshotCropMode = 'standard' | 'viewport' | 'area'
|
|
121
|
+
/** Aspect presets available to `standard` crops. */
|
|
122
|
+
export type SnapshotStandardAspect = '16:9' | '9:16' | '4:3' | '3:4' | '1:1'
|
|
123
|
+
|
|
62
124
|
export type CaptureMode =
|
|
63
125
|
| { mode: 'idle' }
|
|
64
|
-
| { mode: 'standard' }
|
|
126
|
+
| { mode: 'standard'; crop?: SnapshotCropMode; standardAspect?: SnapshotStandardAspect }
|
|
65
127
|
| {
|
|
66
128
|
mode: 'preset'
|
|
67
129
|
isolated: AnyNodeId[]
|
|
@@ -106,9 +168,18 @@ export type StructureTool =
|
|
|
106
168
|
| 'dormer'
|
|
107
169
|
| 'gutter'
|
|
108
170
|
| 'downspout'
|
|
171
|
+
| 'duct-segment'
|
|
172
|
+
| 'duct-fitting'
|
|
173
|
+
| 'duct-terminal'
|
|
174
|
+
| 'hvac-equipment'
|
|
175
|
+
| 'lineset'
|
|
176
|
+
| 'liquid-line'
|
|
177
|
+
| 'pipe-segment'
|
|
178
|
+
| 'pipe-fitting'
|
|
179
|
+
| 'pipe-trap'
|
|
109
180
|
|
|
110
181
|
// Furnish mode tools (items and decoration)
|
|
111
|
-
export type FurnishTool = 'item'
|
|
182
|
+
export type FurnishTool = 'item' | 'cabinet'
|
|
112
183
|
|
|
113
184
|
// Site mode tools
|
|
114
185
|
export type SiteTool = 'property-line'
|
|
@@ -140,8 +211,14 @@ export type NavigationSyncPose = {
|
|
|
140
211
|
|
|
141
212
|
export type NavigationSyncPoseInput = Omit<NavigationSyncPose, 'revision'>
|
|
142
213
|
|
|
143
|
-
// Combined tool type
|
|
144
|
-
|
|
214
|
+
// Combined tool type. Known literals keep autocomplete; the `(string & {})`
|
|
215
|
+
// arm lets plugin-contributed tool ids (e.g. `'trees:tree'`) typecheck without
|
|
216
|
+
// the host enumerating every plugin kind. The runtime dispatch is already
|
|
217
|
+
// registry-first — `tool-manager` resolves `nodeRegistry.get(tool)?.tool` — so
|
|
218
|
+
// an unknown-to-the-host tool string flows straight through to the plugin's
|
|
219
|
+
// placement component.
|
|
220
|
+
export type KnownTool = SiteTool | StructureTool | FurnishTool
|
|
221
|
+
export type Tool = KnownTool | (string & {})
|
|
145
222
|
|
|
146
223
|
/**
|
|
147
224
|
* Starting parameters seeded into a draw tool before it mints a node.
|
|
@@ -151,16 +228,6 @@ export type Tool = SiteTool | StructureTool | FurnishTool
|
|
|
151
228
|
*/
|
|
152
229
|
export type ToolDefaults = Record<string, unknown>
|
|
153
230
|
|
|
154
|
-
export type MovingWallEndpoint = {
|
|
155
|
-
wall: WallNode
|
|
156
|
-
endpoint: 'start' | 'end'
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
export type MovingFenceEndpoint = {
|
|
160
|
-
fence: FenceNode
|
|
161
|
-
endpoint: 'start' | 'end'
|
|
162
|
-
}
|
|
163
|
-
|
|
164
231
|
export type MaterialTargetRole =
|
|
165
232
|
| WallSurfaceSide
|
|
166
233
|
| StairSurfaceMaterialRole
|
|
@@ -168,6 +235,7 @@ export type MaterialTargetRole =
|
|
|
168
235
|
| ChimneyMaterialRole
|
|
169
236
|
| DormerSurfaceMaterialRole
|
|
170
237
|
| SingleSurfaceMaterialRole
|
|
238
|
+
| string
|
|
171
239
|
|
|
172
240
|
export type SelectedMaterialTarget = {
|
|
173
241
|
nodeId: AnyNodeId
|
|
@@ -203,31 +271,14 @@ type EditorState = {
|
|
|
203
271
|
*/
|
|
204
272
|
toolDefaults: Partial<Record<Tool, ToolDefaults>>
|
|
205
273
|
setToolDefaults: (tool: Tool, defaults: ToolDefaults | null) => void
|
|
274
|
+
lastMeasurementKind: CreatableMeasurementKind
|
|
275
|
+
setLastMeasurementKind: (kind: CreatableMeasurementKind) => void
|
|
206
276
|
structureLayer: StructureLayer
|
|
207
277
|
setStructureLayer: (layer: StructureLayer) => void
|
|
208
278
|
catalogCategory: CatalogCategory | null
|
|
209
279
|
setCatalogCategory: (category: CatalogCategory | null) => void
|
|
210
280
|
selectedItem: AssetInput | null
|
|
211
281
|
setSelectedItem: (item: AssetInput) => void
|
|
212
|
-
movingNode:
|
|
213
|
-
| ItemNode
|
|
214
|
-
| WindowNode
|
|
215
|
-
| DoorNode
|
|
216
|
-
| ElevatorNode
|
|
217
|
-
| CeilingNode
|
|
218
|
-
| ChimneyNode
|
|
219
|
-
| ColumnNode
|
|
220
|
-
| DormerNode
|
|
221
|
-
| SlabNode
|
|
222
|
-
| WallNode
|
|
223
|
-
| FenceNode
|
|
224
|
-
| RoofNode
|
|
225
|
-
| RoofSegmentNode
|
|
226
|
-
| SpawnNode
|
|
227
|
-
| StairNode
|
|
228
|
-
| StairSegmentNode
|
|
229
|
-
| BuildingNode
|
|
230
|
-
| null
|
|
231
282
|
/**
|
|
232
283
|
* True while a move was engaged by a press-drag gizmo (the on-canvas move
|
|
233
284
|
* cross) rather than a click-to-place flow. The placement coordinator reads
|
|
@@ -235,6 +286,8 @@ type EditorState = {
|
|
|
235
286
|
*/
|
|
236
287
|
placementDragMode: boolean
|
|
237
288
|
setPlacementDragMode: (dragMode: boolean) => void
|
|
289
|
+
roofHostDragArmedId: AnyNodeId | null
|
|
290
|
+
setRoofHostDragArmedId: (nodeId: AnyNodeId | null) => void
|
|
238
291
|
setMovingNode: (
|
|
239
292
|
node:
|
|
240
293
|
| ItemNode
|
|
@@ -254,6 +307,8 @@ type EditorState = {
|
|
|
254
307
|
| StairNode
|
|
255
308
|
| StairSegmentNode
|
|
256
309
|
| BuildingNode
|
|
310
|
+
| CabinetNode
|
|
311
|
+
| CabinetModuleNode
|
|
257
312
|
| null,
|
|
258
313
|
) => void
|
|
259
314
|
/**
|
|
@@ -275,43 +330,57 @@ type EditorState = {
|
|
|
275
330
|
*/
|
|
276
331
|
movingNodeOrigin: '2d' | '3d' | null
|
|
277
332
|
setMovingNodeOrigin: (origin: '2d' | '3d' | null) => void
|
|
278
|
-
movingWallEndpoint: MovingWallEndpoint | null
|
|
279
|
-
setMovingWallEndpoint: (value: MovingWallEndpoint | null) => void
|
|
280
|
-
movingFenceEndpoint: MovingFenceEndpoint | null
|
|
281
|
-
setMovingFenceEndpoint: (value: MovingFenceEndpoint | null) => void
|
|
282
333
|
/**
|
|
283
|
-
*
|
|
284
|
-
* (
|
|
285
|
-
*
|
|
286
|
-
*
|
|
287
|
-
* overlays read it to render the right caption; the camera controls
|
|
288
|
-
* use the truthy value to suppress one-finger pan-rotate. Replaces
|
|
289
|
-
* the previous per-kind `resizing*` fields so adding a new resize
|
|
290
|
-
* handle doesn't require a new store field.
|
|
334
|
+
* World axis the R/T keyboard rotation turns around, for kinds with
|
|
335
|
+
* full 3D orientation (duct fittings). Alt cycles it Y → X → Z; the
|
|
336
|
+
* kind's tool / keyboard actions read it, and the floating action
|
|
337
|
+
* menu surfaces it in a pill above the selected node.
|
|
291
338
|
*/
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
curvingWall: WallNode | null
|
|
295
|
-
setCurvingWall: (wall: WallNode | null) => void
|
|
296
|
-
curvingFence: FenceNode | null
|
|
297
|
-
setCurvingFence: (fence: FenceNode | null) => void
|
|
339
|
+
rotationAxis: 'x' | 'y' | 'z'
|
|
340
|
+
cycleRotationAxis: () => 'x' | 'y' | 'z'
|
|
298
341
|
selectedMaterialTarget: SelectedMaterialTarget | null
|
|
299
342
|
setSelectedMaterialTarget: (target: SelectedMaterialTarget | null) => void
|
|
300
343
|
activePaintMaterial: ActivePaintMaterial | null
|
|
301
344
|
setActivePaintMaterial: (material: ActivePaintMaterial | null) => void
|
|
302
345
|
activePaintTarget: PaintableMaterialTarget
|
|
303
346
|
setActivePaintTarget: (target: PaintableMaterialTarget) => void
|
|
347
|
+
// Live vertex count of an in-progress polygon draft (slab / ceiling), so the
|
|
348
|
+
// contextual HUD can gate hints on it (e.g. "Finish" only once ≥ 3 points).
|
|
349
|
+
// 0 when not drafting. Not persisted.
|
|
350
|
+
draftVertexCount: number
|
|
351
|
+
setDraftVertexCount: (count: number) => void
|
|
352
|
+
// Painter application scope — how far one paint click spreads (this surface /
|
|
353
|
+
// whole item / all matching / room). One global mode, target-aware in the HUD
|
|
354
|
+
// (see `lib/paint-scope.ts`), defaulting to the narrowest `'single'`. Not
|
|
355
|
+
// persisted: a "paint everything" scope should reset each session.
|
|
356
|
+
paintScope: PaintScope
|
|
357
|
+
setPaintScope: (scope: PaintScope) => void
|
|
358
|
+
// Cycle the scope within the hovered node's available set and return the new
|
|
359
|
+
// value. Bound to Shift while in paint mode.
|
|
360
|
+
cyclePaintScope: () => PaintScope
|
|
304
361
|
// When true, clicking a surface in paint mode clears it back to its
|
|
305
362
|
// default material instead of applying `activePaintMaterial`.
|
|
306
363
|
paintEraser: boolean
|
|
307
364
|
setPaintEraser: (eraser: boolean) => void
|
|
308
365
|
primeMaterialPaintFromSelection: () => MaterialPaintSelectionSnapshot
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
366
|
+
// What the cursor is over in paint mode: the scopes it offers + labels for the
|
|
367
|
+
// HUD chip. `null` when not over a paintable surface (drives the "hover a
|
|
368
|
+
// surface" hint). Set by the selection-manager paint hover; not persisted.
|
|
369
|
+
paintHover: PaintHoverInfo | null
|
|
370
|
+
setPaintHover: (info: PaintHoverInfo | null) => void
|
|
371
|
+
// Embedder capability: true when a host (e.g. community) can locate a selected
|
|
372
|
+
// node in its catalog browser. Gates the node action menu's "Find" button; the
|
|
373
|
+
// editor itself emits `selection:find-node` and lets the host fulfil it. Not
|
|
374
|
+
// persisted — it's a per-mount capability the host registers.
|
|
375
|
+
canFindNode: boolean
|
|
376
|
+
setCanFindNode: (canFind: boolean) => void
|
|
313
377
|
selectedReferenceId: string | null
|
|
314
378
|
setSelectedReferenceId: (id: string | null) => void
|
|
379
|
+
// Guide id with an in-flight reference-scale measurement (line drawing or
|
|
380
|
+
// length dialog). Owned by the floorplan panel; mirrored here so the
|
|
381
|
+
// reference panel can flip its Set Scale button into a Cancel.
|
|
382
|
+
referenceScaleActiveGuideId: string | null
|
|
383
|
+
setReferenceScaleActiveGuideId: (id: string | null) => void
|
|
315
384
|
guideUi: Record<string, GuideUiState>
|
|
316
385
|
setGuideLocked: (guideId: string, locked: boolean) => void
|
|
317
386
|
setGuideScaleReferenceVisible: (guideId: string, visible: boolean) => void
|
|
@@ -319,9 +388,6 @@ type EditorState = {
|
|
|
319
388
|
// Space detection for cutaway mode
|
|
320
389
|
spaces: Record<string, Space>
|
|
321
390
|
setSpaces: (spaces: Record<string, Space>) => void
|
|
322
|
-
// Generic hole editing (works for slabs, ceilings, and any future polygon nodes)
|
|
323
|
-
editingHole: SurfaceHoleTarget | null
|
|
324
|
-
setEditingHole: (hole: SurfaceHoleTarget | null) => void
|
|
325
391
|
hoveredHole: SurfaceHoleTarget | null
|
|
326
392
|
setHoveredHole: (hole: SurfaceHoleTarget | null) => void
|
|
327
393
|
// Preview mode (viewer-like experience inside the editor)
|
|
@@ -348,17 +414,38 @@ type EditorState = {
|
|
|
348
414
|
toggleFloorplanOpen: () => void
|
|
349
415
|
isFloorplanHovered: boolean
|
|
350
416
|
setFloorplanHovered: (hovered: boolean) => void
|
|
417
|
+
// Toggleable DWV riser-diagram (plumbing isometric) overlay.
|
|
418
|
+
isRiserOpen: boolean
|
|
419
|
+
setRiserOpen: (open: boolean) => void
|
|
420
|
+
toggleRiserOpen: () => void
|
|
351
421
|
navigationSyncPose: NavigationSyncPose | null
|
|
352
422
|
publishNavigationSyncPose: (pose: NavigationSyncPoseInput) => void
|
|
353
423
|
floorplanSelectionTool: FloorplanSelectionTool
|
|
354
424
|
setFloorplanSelectionTool: (tool: FloorplanSelectionTool) => void
|
|
355
425
|
gridSnapStep: GridSnapStep
|
|
356
426
|
setGridSnapStep: (step: GridSnapStep) => void
|
|
427
|
+
// Cycles the grid step through GRID_SNAP_STEPS (0.5 → 0.25 → 0.1 → 0.05 →
|
|
428
|
+
// 0.5) and returns the new value. Bound to the measurement-step shortcut.
|
|
429
|
+
cycleGridSnapStep: () => GridSnapStep
|
|
357
430
|
// Magnetic snapping while drafting — snaps wall endpoints onto existing
|
|
358
431
|
// wall corners / wall bodies (the "magnetic" beacon). Independent of grid
|
|
359
432
|
// snap. On by default; toggled from the Display menu.
|
|
360
433
|
magneticSnap: boolean
|
|
361
434
|
setMagneticSnap: (enabled: boolean) => void
|
|
435
|
+
// Per-context, user-cyclable snapping mode (see `lib/snapping-mode.ts`). Each
|
|
436
|
+
// activity (wall / item / polygon) keeps its own mode + default, because they
|
|
437
|
+
// want different snapping — drawing a wall wants grid + angle, nudging an item
|
|
438
|
+
// wants free movement that only catches alignment lines. Resolved to the live
|
|
439
|
+
// context via `getActiveSnappingMode()`; maps onto `gridSnapStep`/`magneticSnap`
|
|
440
|
+
// via `resolveSnapFlags`. Persisted per context.
|
|
441
|
+
snappingModeByContext: Record<SnapContext, SnappingMode>
|
|
442
|
+
setSnappingMode: (context: SnapContext, mode: SnappingMode) => void
|
|
443
|
+
// Cycle the *active* context's mode within its own set; returns the new value.
|
|
444
|
+
cycleSnappingMode: () => SnappingMode
|
|
445
|
+
continuationByContext: Record<ContinuationContext, ContinuationMode>
|
|
446
|
+
setContinuation: (context: ContinuationContext, mode: ContinuationMode) => void
|
|
447
|
+
cycleContinuation: (context: ContinuationContext) => ContinuationMode
|
|
448
|
+
getContinuation: (context: ContinuationContext) => ContinuationMode
|
|
362
449
|
showReferenceFloor: boolean
|
|
363
450
|
toggleReferenceFloor: () => void
|
|
364
451
|
setShowReferenceFloor: (show: boolean) => void
|
|
@@ -369,6 +456,11 @@ type EditorState = {
|
|
|
369
456
|
// Development-only camera debug flag for inspecting underside geometry
|
|
370
457
|
allowUndergroundCamera: boolean
|
|
371
458
|
setAllowUndergroundCamera: (enabled: boolean) => void
|
|
459
|
+
// Development-only debug overlay: draw each wall's opening-snap hit area
|
|
460
|
+
// (the capsule of points within the snap radius of its centerline). Lets us
|
|
461
|
+
// see why a door/window snaps where it does.
|
|
462
|
+
show2dVoronoi: boolean
|
|
463
|
+
setShow2dVoronoi: (enabled: boolean) => void
|
|
372
464
|
// First-person walkthrough mode (street view)
|
|
373
465
|
isFirstPersonMode: boolean
|
|
374
466
|
_viewModeBeforeFirstPerson: ViewMode | null
|
|
@@ -402,6 +494,9 @@ type PersistedEditorLayoutState = Pick<
|
|
|
402
494
|
| 'floorplanSelectionTool'
|
|
403
495
|
| 'gridSnapStep'
|
|
404
496
|
| 'magneticSnap'
|
|
497
|
+
| 'lastMeasurementKind'
|
|
498
|
+
| 'snappingModeByContext'
|
|
499
|
+
| 'continuationByContext'
|
|
405
500
|
| 'showReferenceFloor'
|
|
406
501
|
| 'referenceFloorOffset'
|
|
407
502
|
| 'referenceFloorOpacity'
|
|
@@ -425,6 +520,18 @@ export const DEFAULT_PERSISTED_EDITOR_LAYOUT_STATE: PersistedEditorLayoutState =
|
|
|
425
520
|
floorplanSelectionTool: 'click',
|
|
426
521
|
gridSnapStep: 0.5,
|
|
427
522
|
magneticSnap: true,
|
|
523
|
+
lastMeasurementKind: DEFAULT_CREATABLE_MEASUREMENT_KIND,
|
|
524
|
+
snappingModeByContext: {
|
|
525
|
+
wall: defaultSnappingModeFor('wall'),
|
|
526
|
+
item: defaultSnappingModeFor('item'),
|
|
527
|
+
polygon: defaultSnappingModeFor('polygon'),
|
|
528
|
+
},
|
|
529
|
+
continuationByContext: {
|
|
530
|
+
wall: CONTINUATION_PROFILES.wall.default,
|
|
531
|
+
fence: CONTINUATION_PROFILES.fence.default,
|
|
532
|
+
point: CONTINUATION_PROFILES.point.default,
|
|
533
|
+
cabinet: CONTINUATION_PROFILES.cabinet.default,
|
|
534
|
+
},
|
|
428
535
|
showReferenceFloor: false,
|
|
429
536
|
referenceFloorOffset: 1,
|
|
430
537
|
referenceFloorOpacity: 0.35,
|
|
@@ -527,8 +634,51 @@ export function normalizePersistedEditorUiState(
|
|
|
527
634
|
}
|
|
528
635
|
}
|
|
529
636
|
|
|
637
|
+
// Validate a persisted per-context mode against that context's allowed set
|
|
638
|
+
// (so e.g. a stale `angles` for items resets), falling back to its default.
|
|
639
|
+
function migrateSnappingMode(value: unknown, context: SnapContext): SnappingMode {
|
|
640
|
+
return snappingModesFor(context).includes(value as SnappingMode)
|
|
641
|
+
? (value as SnappingMode)
|
|
642
|
+
: defaultSnappingModeFor(context)
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
type LegacyContinuationState = {
|
|
646
|
+
continuationByContext?: Partial<Record<ContinuationContext, unknown>>
|
|
647
|
+
wallChainMode?: unknown
|
|
648
|
+
fenceChainMode?: unknown
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
function migrateContinuationMode(
|
|
652
|
+
value: unknown,
|
|
653
|
+
context: ContinuationContext,
|
|
654
|
+
): ContinuationMode | null {
|
|
655
|
+
const profile = CONTINUATION_PROFILES[context]
|
|
656
|
+
return profile.options.includes(value as ContinuationMode) ? (value as ContinuationMode) : null
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
function normalizeContinuationByContext(
|
|
660
|
+
state: LegacyContinuationState | null | undefined,
|
|
661
|
+
): Record<ContinuationContext, ContinuationMode> {
|
|
662
|
+
return {
|
|
663
|
+
wall:
|
|
664
|
+
migrateContinuationMode(state?.continuationByContext?.wall, 'wall') ??
|
|
665
|
+
migrateContinuationMode(state?.wallChainMode, 'wall') ??
|
|
666
|
+
CONTINUATION_PROFILES.wall.default,
|
|
667
|
+
fence:
|
|
668
|
+
migrateContinuationMode(state?.continuationByContext?.fence, 'fence') ??
|
|
669
|
+
migrateContinuationMode(state?.fenceChainMode, 'fence') ??
|
|
670
|
+
CONTINUATION_PROFILES.fence.default,
|
|
671
|
+
point:
|
|
672
|
+
migrateContinuationMode(state?.continuationByContext?.point, 'point') ??
|
|
673
|
+
CONTINUATION_PROFILES.point.default,
|
|
674
|
+
cabinet:
|
|
675
|
+
migrateContinuationMode(state?.continuationByContext?.cabinet, 'cabinet') ??
|
|
676
|
+
CONTINUATION_PROFILES.cabinet.default,
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
530
680
|
function normalizePersistedEditorLayoutState(
|
|
531
|
-
state: Partial<PersistedEditorLayoutState> | null | undefined,
|
|
681
|
+
state: (Partial<PersistedEditorLayoutState> & LegacyContinuationState) | null | undefined,
|
|
532
682
|
): PersistedEditorLayoutState {
|
|
533
683
|
return {
|
|
534
684
|
activeSidebarPanel:
|
|
@@ -543,6 +693,13 @@ function normalizePersistedEditorLayoutState(
|
|
|
543
693
|
: DEFAULT_PERSISTED_EDITOR_LAYOUT_STATE.gridSnapStep,
|
|
544
694
|
// Default on: only an explicit persisted `false` disables it.
|
|
545
695
|
magneticSnap: state?.magneticSnap !== false,
|
|
696
|
+
lastMeasurementKind: normalizeCreatableMeasurementKind(state?.lastMeasurementKind),
|
|
697
|
+
snappingModeByContext: {
|
|
698
|
+
wall: migrateSnappingMode(state?.snappingModeByContext?.wall, 'wall'),
|
|
699
|
+
item: migrateSnappingMode(state?.snappingModeByContext?.item, 'item'),
|
|
700
|
+
polygon: migrateSnappingMode(state?.snappingModeByContext?.polygon, 'polygon'),
|
|
701
|
+
},
|
|
702
|
+
continuationByContext: normalizeContinuationByContext(state),
|
|
546
703
|
showReferenceFloor: state?.showReferenceFloor === true,
|
|
547
704
|
referenceFloorOffset:
|
|
548
705
|
typeof state?.referenceFloorOffset === 'number' && state.referenceFloorOffset >= 1
|
|
@@ -662,6 +819,11 @@ export function selectSiteFloorplanContext() {
|
|
|
662
819
|
})
|
|
663
820
|
}
|
|
664
821
|
|
|
822
|
+
// Stashes the view mode the user was in before entering capture, so we can
|
|
823
|
+
// restore it on exit. Snapshot capture always frames in 3D — the 2D/split
|
|
824
|
+
// floorplan panes render nothing meaningful for a thumbnail.
|
|
825
|
+
let viewModeBeforeCapture: ViewMode | null = null
|
|
826
|
+
|
|
665
827
|
const useEditor = create<EditorState>()(
|
|
666
828
|
persist(
|
|
667
829
|
(set, get) => ({
|
|
@@ -730,6 +892,10 @@ const useEditor = create<EditorState>()(
|
|
|
730
892
|
else if (tool) {
|
|
731
893
|
set({ tool: null })
|
|
732
894
|
}
|
|
895
|
+
|
|
896
|
+
const scope = useInteractionScope.getState()
|
|
897
|
+
if (mode === 'material-paint') scope.begin({ kind: 'painting' })
|
|
898
|
+
else scope.endIf((s) => s.kind === 'painting')
|
|
733
899
|
},
|
|
734
900
|
tool: DEFAULT_PERSISTED_EDITOR_UI_STATE.tool,
|
|
735
901
|
setTool: (tool) => set({ tool }),
|
|
@@ -744,6 +910,8 @@ const useEditor = create<EditorState>()(
|
|
|
744
910
|
}
|
|
745
911
|
return { toolDefaults: next }
|
|
746
912
|
}),
|
|
913
|
+
lastMeasurementKind: DEFAULT_PERSISTED_EDITOR_LAYOUT_STATE.lastMeasurementKind,
|
|
914
|
+
setLastMeasurementKind: (kind) => set({ lastMeasurementKind: kind }),
|
|
747
915
|
structureLayer: DEFAULT_PERSISTED_EDITOR_UI_STATE.structureLayer,
|
|
748
916
|
setStructureLayer: (layer) => {
|
|
749
917
|
const { mode } = get()
|
|
@@ -765,48 +933,57 @@ const useEditor = create<EditorState>()(
|
|
|
765
933
|
setCatalogCategory: (category) => set({ catalogCategory: category }),
|
|
766
934
|
selectedItem: null,
|
|
767
935
|
setSelectedItem: (item) => set({ selectedItem: item }),
|
|
768
|
-
movingNode: null as
|
|
769
|
-
| ItemNode
|
|
770
|
-
| WindowNode
|
|
771
|
-
| DoorNode
|
|
772
|
-
| ElevatorNode
|
|
773
|
-
| CeilingNode
|
|
774
|
-
| ColumnNode
|
|
775
|
-
| SlabNode
|
|
776
|
-
| WallNode
|
|
777
|
-
| FenceNode
|
|
778
|
-
| RoofNode
|
|
779
|
-
| RoofSegmentNode
|
|
780
|
-
| SpawnNode
|
|
781
|
-
| StairNode
|
|
782
|
-
| StairSegmentNode
|
|
783
|
-
| BuildingNode
|
|
784
|
-
| null,
|
|
785
936
|
placementDragMode: false,
|
|
786
937
|
setPlacementDragMode: (dragMode) => set({ placementDragMode: dragMode }),
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
938
|
+
roofHostDragArmedId: null,
|
|
939
|
+
setRoofHostDragArmedId: (nodeId) => set({ roofHostDragArmedId: nodeId }),
|
|
940
|
+
// The node being placed/moved now lives inside the interaction scope
|
|
941
|
+
// (`useMovingNode` / `getMovingNode`), not a `useEditor` flag. This setter
|
|
942
|
+
// remains the single entry point: it drives the scope and still touches
|
|
943
|
+
// `movingNodeOrigin` / `placementDragMode` so cross-store subscribers that
|
|
944
|
+
// watch this store (community placement) keep firing on move start/end.
|
|
945
|
+
setMovingNode: (node) => {
|
|
946
|
+
const scope = useInteractionScope.getState()
|
|
947
|
+
if (node === null) {
|
|
948
|
+
scope.endIf((s) => s.kind === 'placing' || s.kind === 'moving')
|
|
949
|
+
// Preserve `movingNodeOrigin` across the clear so the non-owning
|
|
950
|
+
// side's effect cleanup — which fires after `setMovingNode(null)`
|
|
951
|
+
// propagates — can still read who finalised. The next non-null
|
|
952
|
+
// `setMovingNode` resets it. Always clear the press-drag flag.
|
|
953
|
+
set({ placementDragMode: false })
|
|
954
|
+
return
|
|
955
|
+
}
|
|
956
|
+
const targetNode = resolveMovingNodeTarget(node)
|
|
957
|
+
const isNew = Boolean((targetNode as { metadata?: { isNew?: boolean } }).metadata?.isNew)
|
|
958
|
+
if (isNew) {
|
|
959
|
+
scope.begin({
|
|
960
|
+
kind: 'placing',
|
|
961
|
+
node: targetNode,
|
|
962
|
+
nodeId: targetNode.id,
|
|
963
|
+
nodeType: targetNode.type,
|
|
964
|
+
view: '3d',
|
|
965
|
+
pressDrag: get().placementDragMode,
|
|
966
|
+
})
|
|
967
|
+
} else {
|
|
968
|
+
scope.begin({
|
|
969
|
+
kind: 'moving',
|
|
970
|
+
node: targetNode,
|
|
971
|
+
nodeId: targetNode.id,
|
|
972
|
+
nodeType: targetNode.type,
|
|
973
|
+
view: '3d',
|
|
974
|
+
})
|
|
975
|
+
}
|
|
976
|
+
set({ movingNodeOrigin: null })
|
|
977
|
+
},
|
|
798
978
|
movingNodeOrigin: null as '2d' | '3d' | null,
|
|
799
979
|
setMovingNodeOrigin: (origin) => set({ movingNodeOrigin: origin }),
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
setCurvingWall: (wall) => set({ curvingWall: wall }),
|
|
808
|
-
curvingFence: null,
|
|
809
|
-
setCurvingFence: (fence) => set({ curvingFence: fence }),
|
|
980
|
+
rotationAxis: 'y',
|
|
981
|
+
cycleRotationAxis: () => {
|
|
982
|
+
const order = ['y', 'x', 'z'] as const
|
|
983
|
+
const next = order[(order.indexOf(get().rotationAxis as 'y' | 'x' | 'z') + 1) % 3]!
|
|
984
|
+
set({ rotationAxis: next })
|
|
985
|
+
return next
|
|
986
|
+
},
|
|
810
987
|
selectedMaterialTarget: null,
|
|
811
988
|
setSelectedMaterialTarget: (target) => set({ selectedMaterialTarget: target }),
|
|
812
989
|
activePaintMaterial: null,
|
|
@@ -819,6 +996,19 @@ const useEditor = create<EditorState>()(
|
|
|
819
996
|
set((state) =>
|
|
820
997
|
state.activePaintTarget === target ? state : { activePaintTarget: target },
|
|
821
998
|
),
|
|
999
|
+
draftVertexCount: 0,
|
|
1000
|
+
setDraftVertexCount: (count) =>
|
|
1001
|
+
set((state) => (state.draftVertexCount === count ? state : { draftVertexCount: count })),
|
|
1002
|
+
paintScope: 'single',
|
|
1003
|
+
setPaintScope: (scope) => set({ paintScope: scope }),
|
|
1004
|
+
cyclePaintScope: () => {
|
|
1005
|
+
// Cycle within the hovered node's available scopes (what the click will
|
|
1006
|
+
// actually hit). With nothing paintable hovered there's only `single`.
|
|
1007
|
+
const scopes = get().paintHover?.scopes ?? (['single'] as PaintScope[])
|
|
1008
|
+
const next = cyclePaintScopeValue(get().paintScope, scopes)
|
|
1009
|
+
set({ paintScope: next })
|
|
1010
|
+
return next
|
|
1011
|
+
},
|
|
822
1012
|
paintEraser: false,
|
|
823
1013
|
setPaintEraser: (eraser) => set({ paintEraser: eraser }),
|
|
824
1014
|
primeMaterialPaintFromSelection: () => {
|
|
@@ -848,15 +1038,14 @@ const useEditor = create<EditorState>()(
|
|
|
848
1038
|
activePaintMaterial: activePaintMaterial ?? get().activePaintMaterial,
|
|
849
1039
|
}
|
|
850
1040
|
},
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
),
|
|
856
|
-
isPaintPanelOpen: false,
|
|
857
|
-
setPaintPanelOpen: (open) => set({ isPaintPanelOpen: open }),
|
|
1041
|
+
paintHover: null,
|
|
1042
|
+
setPaintHover: (info) => set({ paintHover: info }),
|
|
1043
|
+
canFindNode: false,
|
|
1044
|
+
setCanFindNode: (canFind) => set({ canFindNode: canFind }),
|
|
858
1045
|
selectedReferenceId: null,
|
|
859
1046
|
setSelectedReferenceId: (id) => set({ selectedReferenceId: id }),
|
|
1047
|
+
referenceScaleActiveGuideId: null,
|
|
1048
|
+
setReferenceScaleActiveGuideId: (id) => set({ referenceScaleActiveGuideId: id }),
|
|
860
1049
|
guideUi: {},
|
|
861
1050
|
setGuideLocked: (guideId, locked) =>
|
|
862
1051
|
set((state) => ({
|
|
@@ -889,8 +1078,6 @@ const useEditor = create<EditorState>()(
|
|
|
889
1078
|
}),
|
|
890
1079
|
spaces: {},
|
|
891
1080
|
setSpaces: (spaces) => set({ spaces }),
|
|
892
|
-
editingHole: null,
|
|
893
|
-
setEditingHole: (hole) => set({ editingHole: hole }),
|
|
894
1081
|
hoveredHole: null,
|
|
895
1082
|
setHoveredHole: (hole) =>
|
|
896
1083
|
set((state) =>
|
|
@@ -914,7 +1101,35 @@ const useEditor = create<EditorState>()(
|
|
|
914
1101
|
setCaptureMode: (next) => {
|
|
915
1102
|
const resolved: CaptureMode =
|
|
916
1103
|
typeof next === 'boolean' ? { mode: next ? 'standard' : 'idle' } : next
|
|
917
|
-
|
|
1104
|
+
const entering = resolved.mode !== 'idle'
|
|
1105
|
+
set((state) => {
|
|
1106
|
+
if (entering) {
|
|
1107
|
+
// Force 3D for the shot. Remember the prior mode only on the first
|
|
1108
|
+
// entry (viewMode is already '3d' on re-entry), so we restore the
|
|
1109
|
+
// user's real choice — not the forced '3d' — when capture ends.
|
|
1110
|
+
if (state.viewMode !== '3d') {
|
|
1111
|
+
viewModeBeforeCapture = state.viewMode
|
|
1112
|
+
return {
|
|
1113
|
+
captureMode: resolved,
|
|
1114
|
+
isCaptureMode: true,
|
|
1115
|
+
viewMode: '3d',
|
|
1116
|
+
isFloorplanOpen: false,
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
return { captureMode: resolved, isCaptureMode: true }
|
|
1120
|
+
}
|
|
1121
|
+
const restore = viewModeBeforeCapture
|
|
1122
|
+
viewModeBeforeCapture = null
|
|
1123
|
+
if (restore && restore !== '3d') {
|
|
1124
|
+
return {
|
|
1125
|
+
captureMode: resolved,
|
|
1126
|
+
isCaptureMode: false,
|
|
1127
|
+
viewMode: restore,
|
|
1128
|
+
isFloorplanOpen: true,
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
return { captureMode: resolved, isCaptureMode: false }
|
|
1132
|
+
})
|
|
918
1133
|
},
|
|
919
1134
|
viewMode: DEFAULT_PERSISTED_EDITOR_UI_STATE.viewMode,
|
|
920
1135
|
setViewMode: (mode) => set({ viewMode: mode, isFloorplanOpen: mode !== '3d' }),
|
|
@@ -929,6 +1144,9 @@ const useEditor = create<EditorState>()(
|
|
|
929
1144
|
}),
|
|
930
1145
|
isFloorplanHovered: false,
|
|
931
1146
|
setFloorplanHovered: (hovered) => set({ isFloorplanHovered: hovered }),
|
|
1147
|
+
isRiserOpen: false,
|
|
1148
|
+
setRiserOpen: (open) => set({ isRiserOpen: open }),
|
|
1149
|
+
toggleRiserOpen: () => set((state) => ({ isRiserOpen: !state.isRiserOpen })),
|
|
932
1150
|
navigationSyncPose: null,
|
|
933
1151
|
publishNavigationSyncPose: (pose) =>
|
|
934
1152
|
set((state) => ({
|
|
@@ -941,8 +1159,48 @@ const useEditor = create<EditorState>()(
|
|
|
941
1159
|
setFloorplanSelectionTool: (tool) => set({ floorplanSelectionTool: tool }),
|
|
942
1160
|
gridSnapStep: DEFAULT_PERSISTED_EDITOR_LAYOUT_STATE.gridSnapStep,
|
|
943
1161
|
setGridSnapStep: (step) => set({ gridSnapStep: step }),
|
|
1162
|
+
cycleGridSnapStep: () => {
|
|
1163
|
+
const current = get().gridSnapStep
|
|
1164
|
+
const index = GRID_SNAP_STEPS.indexOf(current)
|
|
1165
|
+
const next = GRID_SNAP_STEPS[(index + 1) % GRID_SNAP_STEPS.length] ?? GRID_SNAP_STEPS[0]!
|
|
1166
|
+
set({ gridSnapStep: next })
|
|
1167
|
+
return next
|
|
1168
|
+
},
|
|
944
1169
|
magneticSnap: DEFAULT_PERSISTED_EDITOR_LAYOUT_STATE.magneticSnap,
|
|
945
1170
|
setMagneticSnap: (enabled) => set({ magneticSnap: enabled }),
|
|
1171
|
+
snappingModeByContext: DEFAULT_PERSISTED_EDITOR_LAYOUT_STATE.snappingModeByContext,
|
|
1172
|
+
setSnappingMode: (context, mode) =>
|
|
1173
|
+
set((state) => ({
|
|
1174
|
+
snappingModeByContext: { ...state.snappingModeByContext, [context]: mode },
|
|
1175
|
+
})),
|
|
1176
|
+
cycleSnappingMode: () => {
|
|
1177
|
+
const context = getActiveSnapContext() ?? 'item'
|
|
1178
|
+
const current = get().snappingModeByContext[context]
|
|
1179
|
+
const next = cycleSnappingModeIn(context, current)
|
|
1180
|
+
set((state) => ({
|
|
1181
|
+
snappingModeByContext: { ...state.snappingModeByContext, [context]: next },
|
|
1182
|
+
}))
|
|
1183
|
+
return next
|
|
1184
|
+
},
|
|
1185
|
+
continuationByContext: DEFAULT_PERSISTED_EDITOR_LAYOUT_STATE.continuationByContext,
|
|
1186
|
+
setContinuation: (context, mode) => {
|
|
1187
|
+
const next =
|
|
1188
|
+
migrateContinuationMode(mode, context) ?? CONTINUATION_PROFILES[context].default
|
|
1189
|
+
set((state) => ({
|
|
1190
|
+
continuationByContext: { ...state.continuationByContext, [context]: next },
|
|
1191
|
+
}))
|
|
1192
|
+
},
|
|
1193
|
+
cycleContinuation: (context) => {
|
|
1194
|
+
const next = nextContinuation(context, get().getContinuation(context))
|
|
1195
|
+
set((state) => ({
|
|
1196
|
+
continuationByContext: { ...state.continuationByContext, [context]: next },
|
|
1197
|
+
}))
|
|
1198
|
+
return next
|
|
1199
|
+
},
|
|
1200
|
+
getContinuation: (context) => {
|
|
1201
|
+
const current = get().continuationByContext[context]
|
|
1202
|
+
return migrateContinuationMode(current, context) ?? CONTINUATION_PROFILES[context].default
|
|
1203
|
+
},
|
|
946
1204
|
showReferenceFloor: DEFAULT_PERSISTED_EDITOR_LAYOUT_STATE.showReferenceFloor,
|
|
947
1205
|
toggleReferenceFloor: () =>
|
|
948
1206
|
set((state) => ({ showReferenceFloor: !state.showReferenceFloor })),
|
|
@@ -955,6 +1213,8 @@ const useEditor = create<EditorState>()(
|
|
|
955
1213
|
set({ referenceFloorOpacity: Math.min(0.8, Math.max(0.1, opacity)) }),
|
|
956
1214
|
allowUndergroundCamera: false,
|
|
957
1215
|
setAllowUndergroundCamera: (enabled) => set({ allowUndergroundCamera: enabled }),
|
|
1216
|
+
show2dVoronoi: false,
|
|
1217
|
+
setShow2dVoronoi: (enabled) => set({ show2dVoronoi: enabled }),
|
|
958
1218
|
isFirstPersonMode: false,
|
|
959
1219
|
_viewModeBeforeFirstPerson: null as ViewMode | null,
|
|
960
1220
|
setFirstPersonMode: (enabled) => {
|
|
@@ -1014,11 +1274,28 @@ const useEditor = create<EditorState>()(
|
|
|
1014
1274
|
}),
|
|
1015
1275
|
{
|
|
1016
1276
|
name: 'pascal-editor-ui-preferences',
|
|
1017
|
-
merge: (persistedState, currentState) =>
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1277
|
+
merge: (persistedState, currentState) => {
|
|
1278
|
+
const uiState = normalizePersistedEditorUiState(
|
|
1279
|
+
persistedState as Partial<PersistedEditorState>,
|
|
1280
|
+
)
|
|
1281
|
+
const layoutState = normalizePersistedEditorLayoutState(
|
|
1282
|
+
persistedState as Partial<PersistedEditorState>,
|
|
1283
|
+
)
|
|
1284
|
+
|
|
1285
|
+
return {
|
|
1286
|
+
...currentState,
|
|
1287
|
+
...uiState,
|
|
1288
|
+
...layoutState,
|
|
1289
|
+
...(uiState.mode === 'build' && uiState.tool === 'measurement'
|
|
1290
|
+
? {
|
|
1291
|
+
toolDefaults: {
|
|
1292
|
+
...currentState.toolDefaults,
|
|
1293
|
+
measurement: { kind: layoutState.lastMeasurementKind },
|
|
1294
|
+
},
|
|
1295
|
+
}
|
|
1296
|
+
: {}),
|
|
1297
|
+
}
|
|
1298
|
+
},
|
|
1022
1299
|
partialize: (state) => ({
|
|
1023
1300
|
phase: state.phase,
|
|
1024
1301
|
mode: state.mode,
|
|
@@ -1033,12 +1310,105 @@ const useEditor = create<EditorState>()(
|
|
|
1033
1310
|
floorplanSelectionTool: state.floorplanSelectionTool,
|
|
1034
1311
|
gridSnapStep: state.gridSnapStep,
|
|
1035
1312
|
magneticSnap: state.magneticSnap,
|
|
1313
|
+
lastMeasurementKind: state.lastMeasurementKind,
|
|
1314
|
+
snappingModeByContext: state.snappingModeByContext,
|
|
1315
|
+
continuationByContext: state.continuationByContext,
|
|
1036
1316
|
showReferenceFloor: state.showReferenceFloor,
|
|
1037
1317
|
referenceFloorOffset: state.referenceFloorOffset,
|
|
1038
1318
|
referenceFloorOpacity: state.referenceFloorOpacity,
|
|
1039
1319
|
}),
|
|
1320
|
+
skipHydration: true,
|
|
1040
1321
|
},
|
|
1041
1322
|
),
|
|
1042
1323
|
)
|
|
1043
1324
|
|
|
1325
|
+
/**
|
|
1326
|
+
* Effective magnetic-snap state: the legacy `magneticSnap` flag AND the active
|
|
1327
|
+
* context's snapping mode. With exclusive modes, magnetic (alignment axes + wall
|
|
1328
|
+
* corner-join) is on only in `'lines'`. Read from the smallest magnetic choke
|
|
1329
|
+
* points so the mode is honoured without retuning any snap math.
|
|
1330
|
+
*/
|
|
1331
|
+
export function isMagneticSnapActive(): boolean {
|
|
1332
|
+
const state = useEditor.getState()
|
|
1333
|
+
return state.magneticSnap && resolveSnapFlags(getActiveSnappingMode()).magnetic
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
/**
|
|
1337
|
+
* Effective angle-lock state: the active context's snapping mode. With exclusive
|
|
1338
|
+
* modes the 15°/45° lock is on only in `'angles'`. Read from the smallest
|
|
1339
|
+
* angle-lock choke points (wall / fence draft call sites).
|
|
1340
|
+
*/
|
|
1341
|
+
export function isAngleSnapActive(): boolean {
|
|
1342
|
+
return resolveSnapFlags(getActiveSnappingMode()).angles
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
/**
|
|
1346
|
+
* Effective grid-lattice state: the active context's snapping mode. With
|
|
1347
|
+
* exclusive modes the grid quantize is on only in `'grid'`.
|
|
1348
|
+
*/
|
|
1349
|
+
export function isGridSnapActive(): boolean {
|
|
1350
|
+
return resolveSnapFlags(getActiveSnappingMode()).grid
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
/**
|
|
1354
|
+
* Whether alignment "lines" should be DISPLAYED for the active context.
|
|
1355
|
+
*
|
|
1356
|
+
* True whenever a snappable context is active — in EVERY snapping mode,
|
|
1357
|
+
* including `'off'`. The guides are passive reference feedback; this is
|
|
1358
|
+
* decoupled from the magnetic *pull*: a producer publishes guides whenever this
|
|
1359
|
+
* is true, but only applies the alignment delta when `isMagneticSnapActive()`
|
|
1360
|
+
* (i.e. `'lines'`). So the user always sees the same alignment lines while
|
|
1361
|
+
* snapping to grid / angles / off, and only snaps to them in `'lines'` mode.
|
|
1362
|
+
*/
|
|
1363
|
+
export function isAlignmentGuideActive(): boolean {
|
|
1364
|
+
return getActiveSnapContext() !== null
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
/**
|
|
1368
|
+
* The snapping context for what the user is currently doing (wall / item /
|
|
1369
|
+
* polygon), or null when nothing snappable is active. Derived from the
|
|
1370
|
+
* authoritative interaction scope, falling back to the armed build tool (the
|
|
1371
|
+
* `drafting` scope isn't wired). The single source every snap reader + the HUD
|
|
1372
|
+
* resolve their mode through.
|
|
1373
|
+
*/
|
|
1374
|
+
export function getActiveSnapContext(): SnapContext | null {
|
|
1375
|
+
const editor = useEditor.getState()
|
|
1376
|
+
return snapContextOf({
|
|
1377
|
+
scope: useInteractionScope.getState().scope,
|
|
1378
|
+
mode: editor.mode,
|
|
1379
|
+
tool: editor.tool,
|
|
1380
|
+
profileOf: (typeOrTool) => nodeRegistry.get(typeOrTool)?.snapProfile,
|
|
1381
|
+
draftDirectionalOf: (typeOrTool) => nodeRegistry.get(typeOrTool)?.snapDraftDirectional ?? true,
|
|
1382
|
+
})
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
export function getActiveContinuationContext(): ContinuationContext | null {
|
|
1386
|
+
const scope = useInteractionScope.getState().scope
|
|
1387
|
+
if (scope.kind === 'drafting') return continuationContextOf(scope.tool)
|
|
1388
|
+
if (scope.kind === 'placing') return continuationContextOf(scope.nodeType)
|
|
1389
|
+
if (scope.kind !== 'idle') return null
|
|
1390
|
+
|
|
1391
|
+
const editor = useEditor.getState()
|
|
1392
|
+
if (editor.mode !== 'build' || !editor.tool) return null
|
|
1393
|
+
return continuationContextOf(editor.tool)
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
export function getContinuation(context: ContinuationContext): ContinuationMode {
|
|
1397
|
+
return useEditor.getState().getContinuation(context)
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
/**
|
|
1401
|
+
* The effective snapping mode for the active context. Falls back to `'off'` when
|
|
1402
|
+
* no snappable context is active (select / idle, no armed tool) so grid /
|
|
1403
|
+
* magnetic / angle readers — including the snap-grid overlay — stay inert
|
|
1404
|
+
* outside an interaction. Per-context defaults (item is `'grid'`) only take
|
|
1405
|
+
* effect once a tool is armed or an interaction begins; otherwise the item
|
|
1406
|
+
* default would light up the snap grid at idle.
|
|
1407
|
+
*/
|
|
1408
|
+
export function getActiveSnappingMode(): SnappingMode {
|
|
1409
|
+
const context = getActiveSnapContext()
|
|
1410
|
+
if (!context) return 'off'
|
|
1411
|
+
return useEditor.getState().snappingModeByContext[context]
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1044
1414
|
export default useEditor
|