@pascal-app/editor 1.0.7 → 1.1.0
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 +7 -7
- package/src/components/editor/custom-camera-controls.tsx +90 -34
- package/src/components/editor/duplicate-node.ts +2 -1
- package/src/components/editor/first-person-controls.tsx +5 -7
- package/src/components/editor/fit-scene-framing.test.ts +318 -0
- package/src/components/editor/fit-scene-framing.ts +142 -0
- package/src/components/editor/floating-action-menu.tsx +4 -2
- package/src/components/editor/floor-openings-overlay.tsx +1 -1
- package/src/components/editor/floor-region-controls.tsx +2 -2
- package/src/components/editor/floorplan-panel.tsx +173 -8
- package/src/components/editor/floorplan-wall-typing-keys.test.tsx +152 -0
- package/src/components/editor/floorplan-wall-typing-keys.ts +79 -0
- package/src/components/editor/footprint-height-handle.tsx +1 -1
- package/src/components/editor/group-transform-shared.ts +3 -1
- package/src/components/editor/index.tsx +20 -0
- package/src/components/editor/measurement-pill.tsx +1 -1
- package/src/components/editor/node-arrow-handles.tsx +1 -1
- package/src/components/editor/open-wall-ends-3d-layer.tsx +1 -2
- package/src/components/editor/open-wall-ends-hint.tsx +1 -1
- package/src/components/editor/quick-measurement-card.tsx +1 -1
- package/src/components/editor/room-controls.tsx +3 -2
- package/src/components/editor/room-handles-2d.tsx +2 -1
- package/src/components/editor/room-handles.tsx +3 -3
- package/src/components/editor/room-labels.tsx +4 -2
- package/src/components/editor/site-edge-labels.tsx +6 -2
- package/src/components/editor/wall-measurement-label.tsx +30 -14
- package/src/components/editor/wall-move-side-handles.tsx +10 -6
- package/src/components/editor/wall-region-handles.tsx +2 -2
- package/src/components/editor/wall-snap-beacon-layer.tsx +53 -32
- package/src/components/editor-2d/floorplan-group-move.tsx +1 -1
- package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +12 -12
- package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +1 -2
- package/src/components/editor-2d/floorplan-registry-action-menu.tsx +2 -1
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +22 -4
- package/src/components/editor-2d/renderers/floorplan-draft-measurement.tsx +7 -1
- package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +13 -1
- package/src/components/editor-2d/renderers/floorplan-placement-typing-hud.test.tsx +35 -0
- package/src/components/editor-2d/renderers/floorplan-placement-typing-hud.tsx +65 -0
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +22 -0
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +14 -2
- package/src/components/systems/roof/roof-edit-system.test.tsx +15 -1
- package/src/components/systems/roof/roof-edit-system.tsx +30 -19
- package/src/components/tools/distribution/distribution-run-tool.test.tsx +371 -3
- package/src/components/tools/distribution/distribution-run-tool.tsx +150 -32
- package/src/components/tools/item/move-tool.tsx +1 -6
- package/src/components/tools/item/use-placement-coordinator.tsx +1 -1
- package/src/components/tools/paint-region/wall-paint-region-tool.tsx +1 -1
- package/src/components/tools/registry/move-registry-node-tool.test.ts +42 -1
- package/src/components/tools/registry/move-registry-node-tool.tsx +38 -14
- package/src/components/tools/shared/placement-box.tsx +1 -5
- package/src/components/tools/shared/placement-coordinate-input.tsx +94 -0
- package/src/components/tools/shared/segment-angle.ts +0 -4
- package/src/components/tools/tool-manager.tsx +1 -24
- package/src/components/tools/wall/wall-typed-entry.test.ts +99 -0
- package/src/components/tools/wall/wall-typed-entry.ts +70 -0
- package/src/components/ui/controls/slider-control.tsx +18 -6
- package/src/components/ui/controls/tool-options-panel.tsx +74 -44
- package/src/components/ui/helpers/helper-manager.test.tsx +53 -0
- package/src/components/ui/helpers/helper-manager.tsx +54 -0
- package/src/components/ui/panels/floor-foundation-panel.tsx +1 -1
- package/src/components/ui/panels/floor-opening-panel.tsx +1 -1
- package/src/components/ui/panels/mobile-selection-bar.tsx +28 -24
- package/src/components/ui/panels/multi-height-mode.tsx +3 -3
- package/src/components/ui/panels/multi-selection-panel.tsx +1 -1
- package/src/components/ui/panels/panel-manager.tsx +4 -2
- package/src/components/ui/panels/panel-wrapper.tsx +41 -18
- package/src/components/ui/panels/room-construction-rows.tsx +1 -1
- package/src/components/ui/panels/room-openings-field.tsx +1 -1
- package/src/components/ui/panels/room-panel.tsx +1 -1
- package/src/components/ui/right-stack.test.ts +13 -6
- package/src/components/ui/right-stack.tsx +21 -10
- package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +1 -8
- package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +1 -1
- package/src/components/ui/wall-type/wall-type-editor-dialog.tsx +1 -1
- package/src/components/viewer/viewer-rooms.tsx +1 -1
- package/src/i18n/locales/en/messages.po +856 -124
- package/src/i18n/locales/en/messages.ts +1 -1
- package/src/i18n/locales/en-XA/messages.ts +1 -1
- package/src/i18n/locales/es/messages.po +2087 -257
- package/src/i18n/locales/fr/messages.po +2087 -257
- package/src/i18n/locales/ja/messages.po +2087 -257
- package/src/i18n/locales/zh-CN/messages.po +2087 -257
- package/src/index.tsx +74 -47
- package/src/lib/contextual-help.ts +6 -0
- package/src/lib/elevation-guides.ts +21 -6
- package/src/lib/elevator-support.ts +1 -72
- package/src/lib/floor-footprints.test.ts +1 -1
- package/src/lib/floor-footprints.ts +1 -9
- package/src/lib/floor-foundation-support.test.ts +8 -3
- package/src/lib/floor-opening-draft.ts +1 -1
- package/src/lib/floor-region-session.ts +1 -1
- package/src/lib/floorplan/open-wall-ends.ts +2 -1
- package/src/lib/gesture-lifecycle.ts +3 -1
- package/src/lib/interaction/scope.ts +1 -0
- package/src/lib/layer-thickness.ts +1 -1
- package/src/lib/level-selection.test.ts +18 -2
- package/src/lib/level-selection.ts +31 -8
- package/src/lib/linear-display.ts +1 -1
- package/src/lib/live-gesture-round-trip.test.ts +1 -1
- package/src/lib/measurement-label.test.ts +1 -1
- package/src/lib/measurements.test.ts +8 -7
- package/src/lib/measurements.ts +1 -218
- package/src/lib/node-action-movement.test.tsx +115 -0
- package/src/lib/node-action-movement.ts +2 -0
- package/src/lib/placement-metadata.ts +4 -0
- package/src/lib/point-ask/describe-target.ts +6 -8
- package/src/lib/point-ask/region.ts +1 -1
- package/src/lib/print-shell-compiler.ts +12 -1
- package/src/lib/print-wall-solids.test.ts +11 -0
- package/src/lib/print-wall-solids.ts +9 -0
- package/src/lib/room-assembly-overlay.ts +1 -1
- package/src/lib/room-built-on.test.ts +2 -5
- package/src/lib/room-built-on.ts +1 -15
- package/src/lib/room-construction-commands.ts +3 -2
- package/src/lib/room-controls-phase6.test.ts +1 -1
- package/src/lib/room-handle-drag.ts +6 -25
- package/src/lib/room-push-dimensions.test.ts +1 -1
- package/src/lib/scene-bounds.test.ts +144 -0
- package/src/lib/scene-bounds.ts +79 -15
- package/src/lib/scene-clipboard.ts +2 -1
- package/src/lib/snapping-mode.test.ts +23 -1
- package/src/lib/viewer-selection.ts +1 -1
- package/src/lib/wall-region-list.ts +3 -1
- package/src/lib/window-interaction.test.ts +4 -2
- package/src/store/use-editor.tsx +5 -0
- package/src/store/use-placement-typing.test.ts +165 -0
- package/src/store/use-placement-typing.ts +186 -0
- package/src/components/tools/elevator/elevator-defaults.ts +0 -8
- package/src/components/tools/elevator/elevator-tool.tsx +0 -372
- package/src/components/tools/elevator/move-elevator-tool.tsx +0 -262
- package/src/lib/door-interaction.ts +0 -102
- package/src/lib/measurement-label.ts +0 -68
- package/src/lib/window-interaction.ts +0 -142
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
export type FitSceneBounds = {
|
|
2
|
+
center: [number, number]
|
|
3
|
+
size: [number, number]
|
|
4
|
+
} | null
|
|
5
|
+
|
|
6
|
+
export type FitSceneLookAt = {
|
|
7
|
+
eyeX: number
|
|
8
|
+
eyeY: number
|
|
9
|
+
eyeZ: number
|
|
10
|
+
targetX: number
|
|
11
|
+
targetY: number
|
|
12
|
+
targetZ: number
|
|
13
|
+
/** The eye's height above `targetY`, before any keep-over-ground lift. */
|
|
14
|
+
eyeHeight: number
|
|
15
|
+
/**
|
|
16
|
+
* What an orthographic camera fits its zoom to: `setLookAt` leaves the
|
|
17
|
+
* previous project's zoom in place. Centred on the target, so fitting it
|
|
18
|
+
* does not move the pivot.
|
|
19
|
+
*/
|
|
20
|
+
fitSphere: { center: [number, number, number]; radius: number }
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// The empty-editor pose shows the default ±15 m site.
|
|
24
|
+
const DEFAULT_FIT_RADIUS = 15
|
|
25
|
+
// Margin around the footprint, as the frame-node path uses for orthographic fits.
|
|
26
|
+
const FIT_SPHERE_MARGIN = 1.3
|
|
27
|
+
// Keeps a lone wall from filling the viewport, like the perspective 15 m floor.
|
|
28
|
+
const MIN_FIT_RADIUS = 5
|
|
29
|
+
|
|
30
|
+
export type PendingFitScene = {
|
|
31
|
+
bounds: FitSceneBounds
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export type FitSceneEventPlan =
|
|
35
|
+
| { action: 'ignore' }
|
|
36
|
+
| { action: 'apply'; lookAt: FitSceneLookAt }
|
|
37
|
+
| { action: 'queue'; pending: PendingFitScene }
|
|
38
|
+
|
|
39
|
+
export type FitSceneResumePlan = { action: 'noop' } | { action: 'apply'; lookAt: FitSceneLookAt }
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Compute the orbit look-at used by `camera-controls:fit-scene`, resting the
|
|
43
|
+
* pivot at `groundY` (the selected level's orbit height).
|
|
44
|
+
* Null bounds restore the empty-editor default pose.
|
|
45
|
+
*/
|
|
46
|
+
export function computeFitSceneLookAt(bounds: FitSceneBounds, groundY = 0): FitSceneLookAt {
|
|
47
|
+
if (!bounds) {
|
|
48
|
+
return {
|
|
49
|
+
eyeX: 20,
|
|
50
|
+
eyeY: 20 + groundY,
|
|
51
|
+
eyeZ: 20,
|
|
52
|
+
targetX: 0,
|
|
53
|
+
targetY: groundY,
|
|
54
|
+
targetZ: 0,
|
|
55
|
+
eyeHeight: 20,
|
|
56
|
+
fitSphere: { center: [0, groundY, 0], radius: DEFAULT_FIT_RADIUS },
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const [cx, cz] = bounds.center
|
|
61
|
+
const [w, d] = bounds.size
|
|
62
|
+
// Use the longer horizontal extent to size the orbit radius so the whole
|
|
63
|
+
// footprint sits in view regardless of aspect ratio.
|
|
64
|
+
const maxExtent = Math.max(w, d)
|
|
65
|
+
const distance = Math.max(maxExtent * 1.4, 15)
|
|
66
|
+
const height = Math.max(maxExtent * 0.8, 10)
|
|
67
|
+
return {
|
|
68
|
+
eyeX: cx + distance * 0.7,
|
|
69
|
+
eyeY: height + groundY,
|
|
70
|
+
eyeZ: cz + distance * 0.7,
|
|
71
|
+
targetX: cx,
|
|
72
|
+
targetY: groundY,
|
|
73
|
+
targetZ: cz,
|
|
74
|
+
eyeHeight: height,
|
|
75
|
+
fitSphere: {
|
|
76
|
+
center: [cx, groundY, cz],
|
|
77
|
+
radius: Math.max((Math.hypot(w, d) / 2) * FIT_SPHERE_MARGIN, MIN_FIT_RADIUS),
|
|
78
|
+
},
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Decide what to do when a fit-scene event arrives.
|
|
84
|
+
* First-person (and briefly-unmounted orbit controls) must queue the frame
|
|
85
|
+
* so it can apply once orbit is back.
|
|
86
|
+
*/
|
|
87
|
+
export function planFitSceneOnEvent(input: {
|
|
88
|
+
isPreviewMode: boolean
|
|
89
|
+
isFirstPersonMode: boolean
|
|
90
|
+
hasControls: boolean
|
|
91
|
+
bounds: FitSceneBounds
|
|
92
|
+
groundY?: number
|
|
93
|
+
}): FitSceneEventPlan {
|
|
94
|
+
if (input.isPreviewMode) return { action: 'ignore' }
|
|
95
|
+
|
|
96
|
+
const pending: PendingFitScene = { bounds: input.bounds }
|
|
97
|
+
if (input.isFirstPersonMode || !input.hasControls) {
|
|
98
|
+
return { action: 'queue', pending }
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return { action: 'apply', lookAt: computeFitSceneLookAt(input.bounds, input.groundY) }
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Flush a queued load frame once orbit controls can apply it. The ground is
|
|
106
|
+
* read at flush time: the selected level may have changed while it waited.
|
|
107
|
+
*/
|
|
108
|
+
export function planFitSceneOnOrbitResume(input: {
|
|
109
|
+
isPreviewMode: boolean
|
|
110
|
+
isFirstPersonMode: boolean
|
|
111
|
+
hasControls: boolean
|
|
112
|
+
pending: PendingFitScene | null
|
|
113
|
+
groundY?: number
|
|
114
|
+
}): FitSceneResumePlan {
|
|
115
|
+
if (!input.pending || input.isPreviewMode || input.isFirstPersonMode || !input.hasControls) {
|
|
116
|
+
return { action: 'noop' }
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return { action: 'apply', lookAt: computeFitSceneLookAt(input.pending.bounds, input.groundY) }
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export type LevelFollowPlan = 'none' | 'default-pose' | 'rest-pivot'
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* What the level-follow effect does with the orbit pivot.
|
|
126
|
+
*
|
|
127
|
+
* Only a scene-less first run takes the default pose. A first run over a
|
|
128
|
+
* loaded scene is a fresh controls instance (mount, leaving XR, error-boundary
|
|
129
|
+
* recovery) whose pivot sits at the origin, so it is rested on the level like
|
|
130
|
+
* a camera change; on a real load the later scene-ready fit still wins.
|
|
131
|
+
* Afterwards the pivot follows level, level-mode and camera changes only.
|
|
132
|
+
*/
|
|
133
|
+
export function planLevelFollow(input: {
|
|
134
|
+
firstLoad: boolean
|
|
135
|
+
sceneEmpty: boolean
|
|
136
|
+
levelChanged: boolean
|
|
137
|
+
modeChanged: boolean
|
|
138
|
+
cameraChanged: boolean
|
|
139
|
+
}): LevelFollowPlan {
|
|
140
|
+
if (input.firstLoad) return input.sceneEmpty ? 'default-pose' : 'rest-pivot'
|
|
141
|
+
return input.levelChanged || input.modeChanged || input.cameraChanged ? 'rest-pivot' : 'none'
|
|
142
|
+
}
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
getWallThickness,
|
|
15
15
|
isCurvedWall,
|
|
16
16
|
isDerivedNode,
|
|
17
|
+
isNodeDuplicable,
|
|
17
18
|
isRegistryMovable,
|
|
18
19
|
isRegistrySelectable,
|
|
19
20
|
isSplineFence,
|
|
@@ -284,7 +285,7 @@ function getHeightPillDimensions(node: WallNode | FenceNode): {
|
|
|
284
285
|
} {
|
|
285
286
|
if (node.type === 'wall') {
|
|
286
287
|
return {
|
|
287
|
-
height: getWallEffectiveHeightForNodes(node, useScene.getState().nodes),
|
|
288
|
+
height: getWallEffectiveHeightForNodes(node, useScene.getState().nodes, 0),
|
|
288
289
|
length: getWallCurveLength(node),
|
|
289
290
|
thickness: getWallThickness(node),
|
|
290
291
|
}
|
|
@@ -423,7 +424,7 @@ export function FloatingActionMenu() {
|
|
|
423
424
|
| undefined
|
|
424
425
|
const fallbackHeight =
|
|
425
426
|
node.type === 'wall'
|
|
426
|
-
? getWallEffectiveHeightForNodes(node, useScene.getState().nodes)
|
|
427
|
+
? getWallEffectiveHeightForNodes(node, useScene.getState().nodes, 0)
|
|
427
428
|
: FENCE_DEFAULT_HEIGHT
|
|
428
429
|
const liveHeight = override?.height ?? node.height ?? fallbackHeight
|
|
429
430
|
pillHeightRef.current.textContent = `H ${formatMeasurement(liveHeight, unit, metricNotation)}`
|
|
@@ -681,6 +682,7 @@ export function FloatingActionMenu() {
|
|
|
681
682
|
onDelete={handleDelete}
|
|
682
683
|
onDuplicate={
|
|
683
684
|
node &&
|
|
685
|
+
isNodeDuplicable(node as AnyNode) &&
|
|
684
686
|
node.type !== 'spawn' &&
|
|
685
687
|
!DELETE_ONLY_TYPES.includes(node.type) &&
|
|
686
688
|
!HOLE_TYPES.includes(node.type)
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
type AnyNodeId,
|
|
6
6
|
type FloorOpeningNode,
|
|
7
7
|
resolveCeilingHeight,
|
|
8
|
+
roomFloorElevation,
|
|
8
9
|
sceneRegistry,
|
|
9
10
|
useScene,
|
|
10
11
|
} from '@pascal-app/core'
|
|
@@ -24,7 +25,6 @@ import { useCeilingEditSession } from '../../lib/ceiling-edit-session'
|
|
|
24
25
|
import { EDITOR_LAYER } from '../../lib/constants'
|
|
25
26
|
import { openingRoom, roomSurfaceOpenings, useFloorEditSession } from '../../lib/floor-edit-session'
|
|
26
27
|
import { useOpeningDraft } from '../../lib/floor-opening-draft'
|
|
27
|
-
import { roomFloorElevation } from '../../lib/room-handle-drag'
|
|
28
28
|
import { PolygonEditor } from '../tools/shared/polygon-editor'
|
|
29
29
|
|
|
30
30
|
// The openings of the room being edited ("Edit floor", "Edit ceiling", or a
|
|
@@ -3,8 +3,10 @@
|
|
|
3
3
|
import {
|
|
4
4
|
containsPoint,
|
|
5
5
|
emitter,
|
|
6
|
+
formatAreaLabel,
|
|
6
7
|
type MultiPolygon,
|
|
7
8
|
resolveCeilingHeight,
|
|
9
|
+
roomFloorElevation,
|
|
8
10
|
sceneRegistry,
|
|
9
11
|
useScene,
|
|
10
12
|
} from '@pascal-app/core'
|
|
@@ -60,14 +62,12 @@ import {
|
|
|
60
62
|
} from '../../lib/floor-region-session'
|
|
61
63
|
import { type FloorRegionPoint, snapFloorRegionPoint } from '../../lib/floor-region-snap'
|
|
62
64
|
import { clientToPlan } from '../../lib/floorplan/plan-coords'
|
|
63
|
-
import { formatAreaLabel } from '../../lib/measurements'
|
|
64
65
|
import {
|
|
65
66
|
cancelMezzanineDraft,
|
|
66
67
|
mezzanineDraftActive,
|
|
67
68
|
useMezzanineDraft,
|
|
68
69
|
} from '../../lib/mezzanine-draft'
|
|
69
70
|
import { usePaintRegionMode } from '../../lib/paint-region-mode'
|
|
70
|
-
import { roomFloorElevation } from '../../lib/room-handle-drag'
|
|
71
71
|
import {
|
|
72
72
|
cancelTerraceDraft,
|
|
73
73
|
terraceDraftActive,
|
|
@@ -19,6 +19,8 @@ import {
|
|
|
19
19
|
emitter,
|
|
20
20
|
type FenceNode,
|
|
21
21
|
type FloorplanGeometry,
|
|
22
|
+
formatAngleRadians,
|
|
23
|
+
formatLinearMeasurement,
|
|
22
24
|
formatNumber as formatLocalizedNumber,
|
|
23
25
|
type GeometryContext,
|
|
24
26
|
type GuideNode,
|
|
@@ -29,6 +31,7 @@ import {
|
|
|
29
31
|
type ItemNode,
|
|
30
32
|
isCurvedWall,
|
|
31
33
|
type LevelNode,
|
|
34
|
+
linearUnitToMeters,
|
|
32
35
|
loadAssetUrl,
|
|
33
36
|
newZone,
|
|
34
37
|
nodeRegistry,
|
|
@@ -124,7 +127,6 @@ import {
|
|
|
124
127
|
syncKeyboardPanMotion,
|
|
125
128
|
} from '../../lib/keyboard-pan'
|
|
126
129
|
import { measurementHint, parseMeasurement } from '../../lib/measurement-parser'
|
|
127
|
-
import { formatLinearMeasurement, linearUnitToMeters } from '../../lib/measurements'
|
|
128
130
|
import {
|
|
129
131
|
createGuideScaleReference,
|
|
130
132
|
formatReferenceScaleLabel,
|
|
@@ -193,6 +195,7 @@ import useInteractionScope, {
|
|
|
193
195
|
useReshapingNode,
|
|
194
196
|
} from '../../store/use-interaction-scope'
|
|
195
197
|
import usePlacementPreview from '../../store/use-placement-preview'
|
|
198
|
+
import { placementTypingLengthOptions, usePlacementTyping } from '../../store/use-placement-typing'
|
|
196
199
|
import { expandSessionSelectionForNode } from '../../store/use-session-groups'
|
|
197
200
|
import { useStairBuildPreview } from '../../store/use-stair-build-preview'
|
|
198
201
|
import {
|
|
@@ -223,6 +226,7 @@ import { FloorplanDraftMeasurement } from '../editor-2d/renderers/floorplan-draf
|
|
|
223
226
|
import { FloorplanGeometryRenderer } from '../editor-2d/renderers/floorplan-geometry-renderer'
|
|
224
227
|
import { FloorplanMarqueeLayer } from '../editor-2d/renderers/floorplan-marquee-layer'
|
|
225
228
|
import { FloorplanPlacementPreviewLayer } from '../editor-2d/renderers/floorplan-placement-preview-layer'
|
|
229
|
+
import { FloorplanPlacementTypingHud } from '../editor-2d/renderers/floorplan-placement-typing-hud'
|
|
226
230
|
import {
|
|
227
231
|
FloorplanRegistryLayer,
|
|
228
232
|
RotationAngleOverlay,
|
|
@@ -253,7 +257,6 @@ import {
|
|
|
253
257
|
} from '../tools/select/screen-rectangle-selection'
|
|
254
258
|
import { collectSelectableCandidateIds } from '../tools/select/select-candidates'
|
|
255
259
|
import {
|
|
256
|
-
formatAngleRadians,
|
|
257
260
|
getAngleArcToSegmentReference,
|
|
258
261
|
getAngleToSegmentReference,
|
|
259
262
|
getSegmentAngleReferenceAtPoint,
|
|
@@ -262,6 +265,7 @@ import {
|
|
|
262
265
|
chainEndJoinsExistingWall,
|
|
263
266
|
createInvisibleWallOnCurrentLevel,
|
|
264
267
|
createWallOnCurrentLevel,
|
|
268
|
+
getSegmentGridStep,
|
|
265
269
|
isSegmentLongEnough,
|
|
266
270
|
snapWallDraftPoint,
|
|
267
271
|
snapWallDraftPointDetailed,
|
|
@@ -270,6 +274,7 @@ import {
|
|
|
270
274
|
WALL_JOIN_SNAP_RADIUS,
|
|
271
275
|
type WallPlanPoint,
|
|
272
276
|
} from '../tools/wall/wall-drafting'
|
|
277
|
+
import { formatWallDraftHeading, resolveTypedWallEnd } from '../tools/wall/wall-typed-entry'
|
|
273
278
|
import { FloorplanCompassButton } from '../viewer/floorplan-compass-button'
|
|
274
279
|
import { resolveFloorplanBackgroundSelection } from './floorplan-background-selection'
|
|
275
280
|
import {
|
|
@@ -289,6 +294,10 @@ import {
|
|
|
289
294
|
resolveFloorplanPresentationViewBox,
|
|
290
295
|
setFloorplanCompassRotation,
|
|
291
296
|
} from './floorplan-navigation-presentation'
|
|
297
|
+
import {
|
|
298
|
+
endFloorplanWallTypingOnCancel,
|
|
299
|
+
installFloorplanWallTypingKeys,
|
|
300
|
+
} from './floorplan-wall-typing-keys'
|
|
292
301
|
import { swallowNextClick } from './handles/use-handle-drag'
|
|
293
302
|
import { useFloorplanBackgroundPlacement } from './use-floorplan-background-placement'
|
|
294
303
|
import { useFloorplanHitTesting } from './use-floorplan-hit-testing'
|
|
@@ -4503,6 +4512,7 @@ function FloorplanLinearDraftLayer({
|
|
|
4503
4512
|
// The thickness the wall commits at, as the 3D draft shows it.
|
|
4504
4513
|
const wallDraftThickness = useWallDraftThickness(DEFAULT_WALL_THICKNESS)
|
|
4505
4514
|
const wallDraftEnd = useFloorplanDraftPreview((s) => s.wallDraftEnd)
|
|
4515
|
+
const wallTypingActive = usePlacementTyping((s) => s.isActive)
|
|
4506
4516
|
const fenceDraftEnd = useFloorplanDraftPreview((s) => s.fenceDraftEnd)
|
|
4507
4517
|
const roofDraftEnd = useFloorplanDraftPreview((s) => s.roofDraftEnd)
|
|
4508
4518
|
const roofDraftQuarterTurn = useFloorplanDraftPreview((s) => s.roofDraftQuarterTurn)
|
|
@@ -4828,6 +4838,7 @@ function FloorplanLinearDraftLayer({
|
|
|
4828
4838
|
<FloorplanDraftMeasurement
|
|
4829
4839
|
labelBackground={isDark ? '#0f172a' : '#ffffff'}
|
|
4830
4840
|
labelText={isDark ? '#e2e8f0' : '#171717'}
|
|
4841
|
+
lengthHidden={wallTypingActive}
|
|
4831
4842
|
measurement={draftWallMeasurement}
|
|
4832
4843
|
measurementStroke={measurementStroke}
|
|
4833
4844
|
sceneRotationDeg={sceneRotationDeg}
|
|
@@ -4835,6 +4846,19 @@ function FloorplanLinearDraftLayer({
|
|
|
4835
4846
|
/>
|
|
4836
4847
|
)}
|
|
4837
4848
|
|
|
4849
|
+
{wallTypingActive && isWallBuildActive && wallDraftStart ? (
|
|
4850
|
+
<FloorplanPlacementTypingHud
|
|
4851
|
+
placeholders={[
|
|
4852
|
+
draftWallMeasurement?.lengthLabel ?? '',
|
|
4853
|
+
wallDraftEnd ? formatWallDraftHeading(wallDraftStart, wallDraftEnd) : '',
|
|
4854
|
+
]}
|
|
4855
|
+
sceneRotationDeg={sceneRotationDeg}
|
|
4856
|
+
unitsPerPixel={unitsPerPixel}
|
|
4857
|
+
x={(draftWallMeasurement?.midpoint ?? wallDraftStart)[0]}
|
|
4858
|
+
y={(draftWallMeasurement?.midpoint ?? wallDraftStart)[1]}
|
|
4859
|
+
/>
|
|
4860
|
+
) : null}
|
|
4861
|
+
|
|
4838
4862
|
{polygonRoomDraft?.map((points, index) => (
|
|
4839
4863
|
<FloorplanDraftLayer
|
|
4840
4864
|
anchorFill={draftStroke}
|
|
@@ -4912,6 +4936,9 @@ export function FloorplanPanel({
|
|
|
4912
4936
|
const floorplanSpacePanPressedRef = useRef(false)
|
|
4913
4937
|
const floorplanNavigationClickSuppressedRef = useRef(false)
|
|
4914
4938
|
const guideInteractionRef = useRef<GuideInteractionState | null>(null)
|
|
4939
|
+
// Late-bound so the window keydown effect can commit the wall draft at the
|
|
4940
|
+
// typed length without an ordering dependency on `handleWallPlacementPoint`.
|
|
4941
|
+
const wallPlacementPointRef = useRef<((point: WallPlanPoint) => void) | null>(null)
|
|
4915
4942
|
const guideTransformDraftRef = useRef<GuideTransformDraft | null>(null)
|
|
4916
4943
|
const pendingFenceDragRef = useRef<PendingFenceDragState | null>(null)
|
|
4917
4944
|
const wallEndpointDragRef = useRef<WallEndpointDragState | null>(null)
|
|
@@ -5105,6 +5132,25 @@ export function FloorplanPanel({
|
|
|
5105
5132
|
// Walls committed by the current 2D-only chain — exclusion set for the
|
|
5106
5133
|
// T-junction chain-termination test (mirrors the 3D tool's `chainWallIds`).
|
|
5107
5134
|
const wallChainWallIdsRef = useRef<string[]>([])
|
|
5135
|
+
// Split view: the 3D wall tool owns the commit and publishes each segment's
|
|
5136
|
+
// resolved end as the chain start. Re-base this panel's draft onto it so a
|
|
5137
|
+
// typed Enter in the plan projects from the segment the 3D chain is
|
|
5138
|
+
// actually drafting — without this, `draftStart` stays on the last point
|
|
5139
|
+
// the panel itself placed and the typed commit uses a stale origin. In
|
|
5140
|
+
// 2D-only the panel is the committer and never publishes, so this
|
|
5141
|
+
// subscription stays dormant there.
|
|
5142
|
+
useEffect(() => {
|
|
5143
|
+
if (!(mode === 'build' && tool === 'wall')) return
|
|
5144
|
+
return useSegmentDraftChain.subscribe((state, previousState) => {
|
|
5145
|
+
const next = state.wall
|
|
5146
|
+
if (next === previousState.wall) return
|
|
5147
|
+
if (!next) return
|
|
5148
|
+
setDraftStart((current) =>
|
|
5149
|
+
current && current[0] === next[0] && current[1] === next[1] ? current : next,
|
|
5150
|
+
)
|
|
5151
|
+
setWallChainFirstVertex((current) => current ?? next)
|
|
5152
|
+
})
|
|
5153
|
+
}, [mode, tool])
|
|
5108
5154
|
// Direction of the run's last wall (a pulled run's source first): the next
|
|
5109
5155
|
// holds square to it. Null before the run has a wall.
|
|
5110
5156
|
const wallRunReferenceRef = useRef<WallPlanPoint | null>(null)
|
|
@@ -5115,6 +5161,8 @@ export function FloorplanPanel({
|
|
|
5115
5161
|
},
|
|
5116
5162
|
[],
|
|
5117
5163
|
)
|
|
5164
|
+
// The pointer's own (snapped) wall draft end; a typed session re-projects it.
|
|
5165
|
+
const wallDraftPointerRef = useRef<WallPlanPoint | null>(null)
|
|
5118
5166
|
const [fenceDraftStart, setFenceDraftStart] = useState<WallPlanPoint | null>(null)
|
|
5119
5167
|
const setFenceDraftEnd = useCallback(
|
|
5120
5168
|
(next: WallPlanPoint | null | ((prev: WallPlanPoint | null) => WallPlanPoint | null)) => {
|
|
@@ -7916,6 +7964,26 @@ export function FloorplanPanel({
|
|
|
7916
7964
|
}
|
|
7917
7965
|
}, [isFloorplanOpen, stopFloorplanViewAnimation])
|
|
7918
7966
|
|
|
7967
|
+
// Typing reshapes the plan's own draft without a pointer move.
|
|
7968
|
+
useEffect(() => {
|
|
7969
|
+
if (!draftStart) return
|
|
7970
|
+
return usePlacementTyping.subscribe((state, previous) => {
|
|
7971
|
+
if (state.fields === previous.fields && state.isActive === previous.isActive) return
|
|
7972
|
+
const pointer = wallDraftPointerRef.current
|
|
7973
|
+
if (!pointer) return
|
|
7974
|
+
setDraftEnd(
|
|
7975
|
+
(state.isActive &&
|
|
7976
|
+
resolveTypedWallEnd(
|
|
7977
|
+
draftStart,
|
|
7978
|
+
pointer,
|
|
7979
|
+
state.fields,
|
|
7980
|
+
placementTypingLengthOptions(unit, metricNotation),
|
|
7981
|
+
getSegmentGridStep(),
|
|
7982
|
+
)) ||
|
|
7983
|
+
pointer,
|
|
7984
|
+
)
|
|
7985
|
+
})
|
|
7986
|
+
}, [draftStart, metricNotation, setDraftEnd, unit])
|
|
7919
7987
|
// Clearing an open wall draft drops a Polygon room in progress (nothing of
|
|
7920
7988
|
// it was written); paths that keep it commit first.
|
|
7921
7989
|
const clearWallPlacementDraft = useCallback(() => {
|
|
@@ -7926,6 +7994,8 @@ export function FloorplanPanel({
|
|
|
7926
7994
|
wallChainWallIdsRef.current = []
|
|
7927
7995
|
wallRunReferenceRef.current = null
|
|
7928
7996
|
setDraftEnd(null)
|
|
7997
|
+
wallDraftPointerRef.current = null
|
|
7998
|
+
usePlacementTyping.getState().clear()
|
|
7929
7999
|
useSegmentDraftChain.getState().clear('wall')
|
|
7930
8000
|
// A run pulled from a wall end ends with the plan's draft: back to Select.
|
|
7931
8001
|
endWallPullRun()
|
|
@@ -8033,6 +8103,7 @@ export function FloorplanPanel({
|
|
|
8033
8103
|
|
|
8034
8104
|
useEffect(() => {
|
|
8035
8105
|
const handleCancel = () => {
|
|
8106
|
+
if (endFloorplanWallTypingOnCancel()) return
|
|
8036
8107
|
clearDraft()
|
|
8037
8108
|
}
|
|
8038
8109
|
|
|
@@ -8042,6 +8113,20 @@ export function FloorplanPanel({
|
|
|
8042
8113
|
}
|
|
8043
8114
|
}, [clearDraft])
|
|
8044
8115
|
|
|
8116
|
+
// Split view: when the 3D wall tool stops drafting (auto-close,
|
|
8117
|
+
// T-junction, single-segment, cancel) it clears the published chain start.
|
|
8118
|
+
// That null clear must tear this panel's draft down too, or the 2D side
|
|
8119
|
+
// keeps a live draft that swallows typing keys against a chain the 3D
|
|
8120
|
+
// view already ended. Lives here (below `clearDraft`)
|
|
8121
|
+
// rather than beside the re-base subscription so the teardown stays
|
|
8122
|
+
// reachable.
|
|
8123
|
+
useEffect(() => {
|
|
8124
|
+
if (!(mode === 'build' && tool === 'wall')) return
|
|
8125
|
+
return useSegmentDraftChain.subscribe((state, previousState) => {
|
|
8126
|
+
if (state.wall === null && previousState.wall !== null) clearDraft()
|
|
8127
|
+
})
|
|
8128
|
+
}, [mode, tool, clearDraft])
|
|
8129
|
+
|
|
8045
8130
|
// A wall end's pull grip pressed in the plan opens the Walls run at that
|
|
8046
8131
|
// end: start the plan's draft there, as if the end had been clicked. A pull
|
|
8047
8132
|
// from the 3D view drafts there (the plan beside it follows its moves).
|
|
@@ -8315,6 +8400,31 @@ export function FloorplanPanel({
|
|
|
8315
8400
|
return unsubscribe
|
|
8316
8401
|
}, [movingNode, scheduleMovingFloorplanNodeRefresh])
|
|
8317
8402
|
|
|
8403
|
+
// Read by the typed-entry listener, registered once so it stays ahead of
|
|
8404
|
+
// the global shortcuts.
|
|
8405
|
+
const wallTypingSourceRef = useRef({
|
|
8406
|
+
draftStart: null as WallPlanPoint | null,
|
|
8407
|
+
lengthOptions: placementTypingLengthOptions(unit, metricNotation),
|
|
8408
|
+
})
|
|
8409
|
+
wallTypingSourceRef.current = {
|
|
8410
|
+
draftStart: isWallBuildActive ? draftStart : null,
|
|
8411
|
+
lengthOptions: placementTypingLengthOptions(unit, metricNotation),
|
|
8412
|
+
}
|
|
8413
|
+
useEffect(
|
|
8414
|
+
() =>
|
|
8415
|
+
installFloorplanWallTypingKeys({
|
|
8416
|
+
getDraft: () => {
|
|
8417
|
+
const { draftStart: start, lengthOptions } = wallTypingSourceRef.current
|
|
8418
|
+
if (!start) return null
|
|
8419
|
+
const pointer =
|
|
8420
|
+
wallDraftPointerRef.current ?? useFloorplanDraftPreview.getState().wallDraftEnd ?? start
|
|
8421
|
+
return { start, pointer, lengthOptions }
|
|
8422
|
+
},
|
|
8423
|
+
commit: (pointer) => wallPlacementPointRef.current?.(pointer),
|
|
8424
|
+
}),
|
|
8425
|
+
[],
|
|
8426
|
+
)
|
|
8427
|
+
|
|
8318
8428
|
useEffect(() => {
|
|
8319
8429
|
const handleKeyDown = (event: KeyboardEvent) => {
|
|
8320
8430
|
const target = event.target as HTMLElement | null
|
|
@@ -8393,7 +8503,16 @@ export function FloorplanPanel({
|
|
|
8393
8503
|
window.removeEventListener('keyup', handleKeyUp)
|
|
8394
8504
|
window.removeEventListener('blur', handleBlur)
|
|
8395
8505
|
}
|
|
8396
|
-
}, [
|
|
8506
|
+
}, [
|
|
8507
|
+
isFloorplanOpen,
|
|
8508
|
+
isStairBuildActive,
|
|
8509
|
+
movingNode,
|
|
8510
|
+
isWallBuildActive,
|
|
8511
|
+
draftStart,
|
|
8512
|
+
unit,
|
|
8513
|
+
metricNotation,
|
|
8514
|
+
setDraftEnd,
|
|
8515
|
+
])
|
|
8397
8516
|
|
|
8398
8517
|
useEffect(() => {
|
|
8399
8518
|
const handleWindowPointerMove = (event: PointerEvent) => {
|
|
@@ -9653,8 +9772,23 @@ export function FloorplanPanel({
|
|
|
9653
9772
|
// Emit `grid:move` so the registry-driven wall tool's 3D preview
|
|
9654
9773
|
// tracks the cursor. The local draftEnd update below is what
|
|
9655
9774
|
// drives the 2D draft polygon — both views update in parallel.
|
|
9775
|
+
// An open typed session re-projects the pointer onto what was typed;
|
|
9776
|
+
// the 3D tool gets the pointer itself and projects it the same way.
|
|
9777
|
+
wallDraftPointerRef.current = draftStart ? snappedPoint : null
|
|
9778
|
+
const wallTyping = usePlacementTyping.getState()
|
|
9779
|
+
const draftEndPoint =
|
|
9780
|
+
(draftStart &&
|
|
9781
|
+
wallTyping.isActive &&
|
|
9782
|
+
resolveTypedWallEnd(
|
|
9783
|
+
draftStart,
|
|
9784
|
+
snappedPoint,
|
|
9785
|
+
wallTyping.fields,
|
|
9786
|
+
placementTypingLengthOptions(unit, metricNotation),
|
|
9787
|
+
getSegmentGridStep(),
|
|
9788
|
+
)) ||
|
|
9789
|
+
snappedPoint
|
|
9656
9790
|
emitFloorplanGridEvent('move', snappedPoint, event)
|
|
9657
|
-
setCursorPoint(
|
|
9791
|
+
setCursorPoint(draftEndPoint)
|
|
9658
9792
|
|
|
9659
9793
|
if (!draftStart) {
|
|
9660
9794
|
return
|
|
@@ -9663,13 +9797,13 @@ export function FloorplanPanel({
|
|
|
9663
9797
|
setDraftEnd((previousEnd) => {
|
|
9664
9798
|
if (
|
|
9665
9799
|
!previousEnd ||
|
|
9666
|
-
previousEnd[0] !==
|
|
9667
|
-
previousEnd[1] !==
|
|
9800
|
+
previousEnd[0] !== draftEndPoint[0] ||
|
|
9801
|
+
previousEnd[1] !== draftEndPoint[1]
|
|
9668
9802
|
) {
|
|
9669
9803
|
sfxEmitter.emit('sfx:grid-snap')
|
|
9670
9804
|
}
|
|
9671
9805
|
|
|
9672
|
-
return
|
|
9806
|
+
return draftEndPoint
|
|
9673
9807
|
})
|
|
9674
9808
|
},
|
|
9675
9809
|
[
|
|
@@ -9704,6 +9838,11 @@ export function FloorplanPanel({
|
|
|
9704
9838
|
isRoofBuildActive,
|
|
9705
9839
|
isWallBuildActive,
|
|
9706
9840
|
levelId,
|
|
9841
|
+
// `unit` / `metricNotation` feed the typed-length projection helpers
|
|
9842
|
+
// shared with the commit path; listed so a unit change re-binds the
|
|
9843
|
+
// move preview to the new bare-unit parse.
|
|
9844
|
+
metricNotation,
|
|
9845
|
+
unit,
|
|
9707
9846
|
publishFloorplanNavigationPose,
|
|
9708
9847
|
referenceScaleDraft,
|
|
9709
9848
|
siteVertexDragState,
|
|
@@ -9889,7 +10028,21 @@ export function FloorplanPanel({
|
|
|
9889
10028
|
)
|
|
9890
10029
|
|
|
9891
10030
|
const handleWallPlacementPoint = useCallback(
|
|
9892
|
-
(
|
|
10031
|
+
(rawPoint: WallPlanPoint) => {
|
|
10032
|
+
// An open typed session commits what its preview shows: the pointer
|
|
10033
|
+
// re-projected onto what was typed, never re-snapped.
|
|
10034
|
+
const typing = usePlacementTyping.getState()
|
|
10035
|
+
const typedEnd =
|
|
10036
|
+
draftStart && typing.isActive
|
|
10037
|
+
? resolveTypedWallEnd(
|
|
10038
|
+
draftStart,
|
|
10039
|
+
rawPoint,
|
|
10040
|
+
typing.fields,
|
|
10041
|
+
placementTypingLengthOptions(unit, metricNotation),
|
|
10042
|
+
getSegmentGridStep(),
|
|
10043
|
+
)
|
|
10044
|
+
: null
|
|
10045
|
+
const point: WallPlanPoint = typedEnd ?? rawPoint
|
|
9893
10046
|
if (!draftStart) {
|
|
9894
10047
|
wallConstructionOptionsRef.current = levelId
|
|
9895
10048
|
? resolveTerrainWallConstructionOptions(
|
|
@@ -9950,6 +10103,7 @@ export function FloorplanPanel({
|
|
|
9950
10103
|
setDraftStart(point)
|
|
9951
10104
|
setDraftEnd(point)
|
|
9952
10105
|
setCursorPoint(point)
|
|
10106
|
+
usePlacementTyping.getState().clear()
|
|
9953
10107
|
return
|
|
9954
10108
|
}
|
|
9955
10109
|
if (viewIs2DOnly) {
|
|
@@ -10015,16 +10169,26 @@ export function FloorplanPanel({
|
|
|
10015
10169
|
setDraftStart(nextStart)
|
|
10016
10170
|
setDraftEnd(nextStart)
|
|
10017
10171
|
setCursorPoint(nextStart)
|
|
10172
|
+
// What was typed belongs to this segment; the next starts afresh.
|
|
10173
|
+
usePlacementTyping.getState().clear()
|
|
10018
10174
|
},
|
|
10019
10175
|
[
|
|
10020
10176
|
clearWallPlacementDraft,
|
|
10021
10177
|
draftStart,
|
|
10022
10178
|
levelId,
|
|
10179
|
+
metricNotation,
|
|
10180
|
+
unit,
|
|
10023
10181
|
wallChainFirstVertex,
|
|
10024
10182
|
setDraftEnd,
|
|
10025
10183
|
setCursorPoint,
|
|
10026
10184
|
],
|
|
10027
10185
|
)
|
|
10186
|
+
// Latest-ref for the Enter-commit path: assigning the ref inside the
|
|
10187
|
+
// callback itself froze the first-render closure (draftStart === null), so
|
|
10188
|
+
// a typed Enter re-started the draft instead of committing the wall.
|
|
10189
|
+
useEffect(() => {
|
|
10190
|
+
wallPlacementPointRef.current = handleWallPlacementPoint
|
|
10191
|
+
}, [handleWallPlacementPoint])
|
|
10028
10192
|
const { getFloorplanHitIdAtPoint, getFloorplanSelectionIdsInBounds } = useFloorplanHitTesting({
|
|
10029
10193
|
sceneRef: floorplanSceneRef,
|
|
10030
10194
|
})
|
|
@@ -10254,6 +10418,7 @@ export function FloorplanPanel({
|
|
|
10254
10418
|
unit,
|
|
10255
10419
|
emitFloorplanGridEvent,
|
|
10256
10420
|
setCursorPoint,
|
|
10421
|
+
locale,
|
|
10257
10422
|
],
|
|
10258
10423
|
)
|
|
10259
10424
|
const handleSvgClick = useCallback(
|