diff --git a/packages/editor/src/components/editor/floorplan-panel.tsx b/packages/editor/src/components/editor/floorplan-panel.tsx
index 7e366238..96f08c8c 100644
--- a/packages/editor/src/components/editor/floorplan-panel.tsx
+++ b/packages/editor/src/components/editor/floorplan-panel.tsx
@@ -4715,6 +4715,204 @@ function FloorplanCursorIndicator(
return
}
+// Leaf overlay for the live wall / fence / roof draft segment (the directional
+// draws). It subscribes to the per-move END points in the draft store so a
+// `grid:move` re-renders ONLY this layer, not FloorplanPanel; the per-click
+// START points + render config arrive as props. Owns the draft polygon (wall +
+// roof rect), the fence segment line, and the wall length/angle measurement —
+// the cursor-following pieces the shared `FloorplanDraftLayer` no longer carries.
+function FloorplanLinearDraftLayer({
+ levelId,
+ wallDraftStart,
+ fenceDraftStart,
+ roofDraftStart,
+ isWallBuildActive,
+ isFenceBuildActive,
+ isRoofBuildActive,
+ walls,
+ unit,
+ draftFill,
+ draftStroke,
+ measurementStroke,
+ isDark,
+ unitsPerPixel,
+ sceneRotationDeg,
+}: {
+ levelId: string | null
+ wallDraftStart: WallPlanPoint | null
+ fenceDraftStart: WallPlanPoint | null
+ roofDraftStart: WallPlanPoint | null
+ isWallBuildActive: boolean
+ isFenceBuildActive: boolean
+ isRoofBuildActive: boolean
+ walls: WallNode[]
+ unit: 'metric' | 'imperial'
+ draftFill: string
+ draftStroke: string
+ measurementStroke: string
+ isDark: boolean
+ unitsPerPixel: number
+ sceneRotationDeg: number
+}) {
+ const wallDraftEnd = useFloorplanDraftPreview((s) => s.wallDraftEnd)
+ const fenceDraftEnd = useFloorplanDraftPreview((s) => s.fenceDraftEnd)
+ const roofDraftEnd = useFloorplanDraftPreview((s) => s.roofDraftEnd)
+
+ const draftPolygon = useMemo(() => {
+ if (
+ !(
+ levelId &&
+ wallDraftStart &&
+ wallDraftEnd &&
+ isSegmentLongEnough(wallDraftStart, wallDraftEnd)
+ )
+ ) {
+ return null
+ }
+ const draftWall = getSharedFloorplanWall(buildDraftWall(levelId, wallDraftStart, wallDraftEnd))
+ // Keep the live draft preview cheap; full level-wide mitering here runs on every mouse move.
+ return getWallPlanFootprint(draftWall, EMPTY_WALL_MITER_DATA)
+ }, [levelId, wallDraftStart, wallDraftEnd])
+
+ const draftPolygonPoints = useMemo(() => {
+ if (isRoofBuildActive && roofDraftStart && roofDraftEnd) {
+ const minX = Math.min(roofDraftStart[0], roofDraftEnd[0])
+ const maxX = Math.max(roofDraftStart[0], roofDraftEnd[0])
+ const minY = Math.min(roofDraftStart[1], roofDraftEnd[1])
+ const maxY = Math.max(roofDraftStart[1], roofDraftEnd[1])
+
+ if (Math.abs(maxX - minX) >= 1e-6 || Math.abs(maxY - minY) >= 1e-6) {
+ return formatPolygonPoints([
+ { x: minX, y: minY },
+ { x: maxX, y: minY },
+ { x: maxX, y: maxY },
+ { x: minX, y: maxY },
+ ])
+ }
+ }
+ return draftPolygon ? formatPolygonPoints(draftPolygon) : null
+ }, [draftPolygon, isRoofBuildActive, roofDraftEnd, roofDraftStart])
+
+ const fenceDraftSegment = useMemo(() => {
+ if (!(isFenceBuildActive && fenceDraftStart && fenceDraftEnd)) {
+ return null
+ }
+ if (getPlanPointDistance(toPoint2D(fenceDraftStart), toPoint2D(fenceDraftEnd)) < 1e-6) {
+ return null
+ }
+ return {
+ x1: toSvgX(fenceDraftStart[0]),
+ y1: toSvgY(fenceDraftStart[1]),
+ x2: toSvgX(fenceDraftEnd[0]),
+ y2: toSvgY(fenceDraftEnd[1]),
+ }
+ }, [fenceDraftEnd, fenceDraftStart, isFenceBuildActive])
+
+ // Live length + angle feedback for the wall draft — parity with the 3D
+ // `WallTool`, ported to 2D plan space.
+ const draftWallMeasurement = useMemo(() => {
+ if (
+ !(
+ isWallBuildActive &&
+ wallDraftStart &&
+ wallDraftEnd &&
+ isSegmentLongEnough(wallDraftStart, wallDraftEnd)
+ )
+ ) {
+ return null
+ }
+
+ const dx = wallDraftEnd[0] - wallDraftStart[0]
+ const dy = wallDraftEnd[1] - wallDraftStart[1]
+ const length = Math.hypot(dx, dy)
+
+ const draftFromStart: WallPlanPoint = [dx, dy]
+ const draftFromEnd: WallPlanPoint = [-dx, -dy]
+ const endpoints = [
+ { id: 'start', point: wallDraftStart, draftVector: draftFromStart },
+ { id: 'end', point: wallDraftEnd, draftVector: draftFromEnd },
+ ] as const
+
+ type AngleLabel = {
+ id: string
+ label: string
+ center: WallPlanPoint
+ radius: number
+ startAngle: number
+ endAngle: number
+ midAngle: number
+ }
+
+ const angleLabels: AngleLabel[] = []
+ for (const endpoint of endpoints) {
+ const connectedWall = walls.find((wall) =>
+ Boolean(getSegmentAngleReferenceAtPoint(endpoint.point, wall)),
+ )
+ if (!connectedWall) continue
+ const ref = getSegmentAngleReferenceAtPoint(endpoint.point, connectedWall)
+ if (!ref) continue
+
+ const angle = getAngleToSegmentReference(endpoint.draftVector, ref)
+ if (angle === null) continue
+ const arc = getAngleArcToSegmentReference(endpoint.draftVector, ref)
+ if (!arc || arc.angle < 0.01) continue
+
+ const refLen = Math.hypot(ref.vector[0], ref.vector[1])
+ const radius = Math.max(0.32, Math.min(0.72, Math.min(length, refLen) * 0.28))
+
+ angleLabels.push({
+ id: endpoint.id,
+ label: formatAngleRadians(angle),
+ center: endpoint.point,
+ radius,
+ startAngle: arc.startAngle,
+ endAngle: arc.endAngle,
+ midAngle: arc.midAngle,
+ })
+ }
+
+ return {
+ lengthLabel: formatMeasurement(length, unit),
+ midpoint: [
+ (wallDraftStart[0] + wallDraftEnd[0]) / 2,
+ (wallDraftStart[1] + wallDraftEnd[1]) / 2,
+ ] as WallPlanPoint,
+ direction: [dx / length, dy / length] as WallPlanPoint,
+ angleLabels,
+ }
+ }, [isWallBuildActive, unit, wallDraftEnd, wallDraftStart, walls])
+
+ return (
+ <>
+
+
+ {draftWallMeasurement && (
+
+ )}
+ >
+ )
+}
+
+const EMPTY_DRAFT_ANCHOR_POINTS: Array<{ x: number; y: number; isPrimary: boolean }> = []
+
export function FloorplanPanel({
/**
* Element to portal the compass button into. The 2D/3D navigation poses stay
@@ -4854,12 +5052,34 @@ export function FloorplanPanel({
FLOORPLAN_VIEW_ROTATION_DEG + floorplanUserRotationDeg - buildingRotationDeg
latestFloorplanUserRotationDegRef.current = floorplanUserRotationDeg
+ // Draft START points stay in panel state (set per click). The live END points
+ // are the per-move hot values — they live in `useFloorplanDraftPreview` so a
+ // `grid:move` re-renders only `FloorplanLinearDraftLayer`, not this panel.
+ // Shims keep the `setXDraftEnd(value | prev => …)` call sites unchanged.
const [draftStart, setDraftStart] = useState(null)
- const [draftEnd, setDraftEnd] = useState(null)
+ const setDraftEnd = useCallback(
+ (next: WallPlanPoint | null | ((prev: WallPlanPoint | null) => WallPlanPoint | null)) => {
+ const store = useFloorplanDraftPreview.getState()
+ store.setWallDraftEnd(typeof next === 'function' ? next(store.wallDraftEnd) : next)
+ },
+ [],
+ )
const [fenceDraftStart, setFenceDraftStart] = useState(null)
- const [fenceDraftEnd, setFenceDraftEnd] = useState(null)
+ const setFenceDraftEnd = useCallback(
+ (next: WallPlanPoint | null | ((prev: WallPlanPoint | null) => WallPlanPoint | null)) => {
+ const store = useFloorplanDraftPreview.getState()
+ store.setFenceDraftEnd(typeof next === 'function' ? next(store.fenceDraftEnd) : next)
+ },
+ [],
+ )
const [roofDraftStart, setRoofDraftStart] = useState(null)
- const [roofDraftEnd, setRoofDraftEnd] = useState(null)
+ const setRoofDraftEnd = useCallback(
+ (next: WallPlanPoint | null | ((prev: WallPlanPoint | null) => WallPlanPoint | null)) => {
+ const store = useFloorplanDraftPreview.getState()
+ store.setRoofDraftEnd(typeof next === 'function' ? next(store.roofDraftEnd) : next)
+ },
+ [],
+ )
const [ceilingDraftPoints, setCeilingDraftPoints] = useState([])
const [slabDraftPoints, setSlabDraftPoints] = useState([])
const [zoneDraftPoints, setZoneDraftPoints] = useState([])
@@ -5879,120 +6099,10 @@ export function FloorplanPanel({
})
}, [canUseSiteBoundaryVertexHandles, siteVertexDragState, visibleSitePolygon])
- const draftPolygon = useMemo(() => {
- if (!(levelId && draftStart && draftEnd && isSegmentLongEnough(draftStart, draftEnd))) {
- return null
- }
-
- const draftWall = getSharedFloorplanWall(buildDraftWall(levelId, draftStart, draftEnd))
- // Keep the live draft preview cheap; full level-wide mitering here runs on every mouse move.
- return getWallPlanFootprint(draftWall, EMPTY_WALL_MITER_DATA)
- }, [draftEnd, draftStart, levelId])
- // Live length + angle feedback for the wall draft — parity with the 3D
- // `WallTool` (`packages/nodes/src/wall/tool.tsx`), ported to 2D plan
- // space. Length renders at the segment midpoint; angle arcs sit at
- // each endpoint that meets an existing wall.
- const draftWallMeasurement = useMemo(() => {
- if (
- !(isWallBuildActive && draftStart && draftEnd && isSegmentLongEnough(draftStart, draftEnd))
- ) {
- return null
- }
-
- const dx = draftEnd[0] - draftStart[0]
- const dy = draftEnd[1] - draftStart[1]
- const length = Math.hypot(dx, dy)
-
- const draftFromStart: WallPlanPoint = [dx, dy]
- const draftFromEnd: WallPlanPoint = [-dx, -dy]
- const endpoints = [
- { id: 'start', point: draftStart, draftVector: draftFromStart },
- { id: 'end', point: draftEnd, draftVector: draftFromEnd },
- ] as const
-
- type AngleLabel = {
- id: string
- label: string
- center: WallPlanPoint
- radius: number
- startAngle: number
- endAngle: number
- midAngle: number
- }
-
- const angleLabels: AngleLabel[] = []
- for (const endpoint of endpoints) {
- const connectedWall = walls.find((wall) =>
- Boolean(getSegmentAngleReferenceAtPoint(endpoint.point, wall)),
- )
- if (!connectedWall) continue
- const ref = getSegmentAngleReferenceAtPoint(endpoint.point, connectedWall)
- if (!ref) continue
-
- const angle = getAngleToSegmentReference(endpoint.draftVector, ref)
- if (angle === null) continue
- const arc = getAngleArcToSegmentReference(endpoint.draftVector, ref)
- if (!arc || arc.angle < 0.01) continue
-
- const refLen = Math.hypot(ref.vector[0], ref.vector[1])
- const radius = Math.max(0.32, Math.min(0.72, Math.min(length, refLen) * 0.28))
-
- angleLabels.push({
- id: endpoint.id,
- label: formatAngleRadians(angle),
- center: endpoint.point,
- radius,
- startAngle: arc.startAngle,
- endAngle: arc.endAngle,
- midAngle: arc.midAngle,
- })
- }
-
- return {
- lengthLabel: formatMeasurement(length, unit),
- midpoint: [
- (draftStart[0] + draftEnd[0]) / 2,
- (draftStart[1] + draftEnd[1]) / 2,
- ] as WallPlanPoint,
- direction: [dx / length, dy / length] as WallPlanPoint,
- angleLabels,
- }
- }, [draftEnd, draftStart, isWallBuildActive, unit, walls])
- const draftPolygonPoints = useMemo(() => {
- if (isRoofBuildActive && roofDraftStart && roofDraftEnd) {
- const minX = Math.min(roofDraftStart[0], roofDraftEnd[0])
- const maxX = Math.max(roofDraftStart[0], roofDraftEnd[0])
- const minY = Math.min(roofDraftStart[1], roofDraftEnd[1])
- const maxY = Math.max(roofDraftStart[1], roofDraftEnd[1])
-
- if (Math.abs(maxX - minX) >= 1e-6 || Math.abs(maxY - minY) >= 1e-6) {
- return formatPolygonPoints([
- { x: minX, y: minY },
- { x: maxX, y: minY },
- { x: maxX, y: maxY },
- { x: minX, y: maxY },
- ])
- }
- }
-
- return draftPolygon ? formatPolygonPoints(draftPolygon) : null
- }, [draftPolygon, isRoofBuildActive, roofDraftEnd, roofDraftStart])
- const fenceDraftSegment = useMemo(() => {
- if (!(isFenceBuildActive && fenceDraftStart && fenceDraftEnd)) {
- return null
- }
-
- if (getPlanPointDistance(toPoint2D(fenceDraftStart), toPoint2D(fenceDraftEnd)) < 1e-6) {
- return null
- }
-
- return {
- x1: toSvgX(fenceDraftStart[0]),
- y1: toSvgY(fenceDraftStart[1]),
- x2: toSvgX(fenceDraftEnd[0]),
- y2: toSvgY(fenceDraftEnd[1]),
- }
- }, [fenceDraftEnd, fenceDraftStart, isFenceBuildActive])
+ // The live wall / fence / roof draft preview (polygon + fence segment + wall
+ // measurement) moved into `FloorplanLinearDraftLayer`, which reads the per-
+ // move END points from the draft store so it re-renders per move without
+ // re-rendering this panel.
const activePolygonDraftPoints = useMemo(() => {
if (isCeilingBuildActive) {
return ceilingDraftPoints
@@ -8651,10 +8761,12 @@ export function FloorplanPanel({
}
if (isRoofBuildActive) {
- const bypassSnap = shiftPressed || event.shiftKey
- let snappedPoint = bypassSnap ? planPoint : getSnappedFloorplanPoint(planPoint)
- snappedPoint = alignFloorplanDraftPoint(snappedPoint, {
- bypass: event.altKey || bypassSnap,
+ // Roof is placed as a footprint (no directional draw → polygon context:
+ // grid / lines / off, no angle lock). Mode-driven, matching the chip:
+ // `grid` quantizes via `getSnappedFloorplanPoint` (step 0 in non-grid
+ // modes), `lines` pulls onto alignment, `off` is free. Alt forces.
+ const snappedPoint = alignFloorplanDraftPoint(getSnappedFloorplanPoint(planPoint), {
+ bypass: event.altKey || !isMagneticSnapActive(),
})
emitFloorplanGridEvent('move', snappedPoint, event)
setCursorPoint((previousPoint) =>
@@ -9225,7 +9337,6 @@ export function FloorplanPanel({
setFenceDraftStart,
setRoofDraftEnd,
setRoofDraftStart,
- shiftPressed,
snapPolygonDraftPoint,
snapWallDraftPoint: snapWallDraftPointMagnetic,
toPoint2D,
@@ -10816,6 +10927,13 @@ export function FloorplanPanel({
outlineWidth={FLOORPLAN_MARQUEE_OUTLINE_WIDTH}
/>
+ {/* This shared layer now carries only the per-CLICK draft anchors
+ (reference-scale start + committed polygon vertices). The
+ cursor-following draft geometry moved to the leaves below
+ (`FloorplanLinearDraftLayer` for wall/fence/roof,
+ `FloorplanDraftCursorLayer` for polygon previews), which read
+ the live END points from the draft store so a per-move update
+ never re-renders this panel. */}
- {draftWallMeasurement && (
-
- )}
+
{/* Wall / fence endpoint, wall curve, slab / ceiling /
zone vertex+midpoint+edge handles are all driven by the
diff --git a/packages/editor/src/components/editor/use-floorplan-background-placement.ts b/packages/editor/src/components/editor/use-floorplan-background-placement.ts
index d060067d..a7e326ad 100644
--- a/packages/editor/src/components/editor/use-floorplan-background-placement.ts
+++ b/packages/editor/src/components/editor/use-floorplan-background-placement.ts
@@ -62,7 +62,6 @@ type UseFloorplanBackgroundPlacementArgs = {
setFenceDraftStart: React.Dispatch>
setRoofDraftEnd: React.Dispatch>
setRoofDraftStart: React.Dispatch>
- shiftPressed: boolean
snapWallDraftPoint: (args: {
point: WallPlanPoint
walls: WallNode[]
@@ -122,7 +121,6 @@ export function useFloorplanBackgroundPlacement({
setFenceDraftStart,
setRoofDraftEnd,
setRoofDraftStart,
- shiftPressed,
snapWallDraftPoint,
snapPolygonDraftPoint,
toPoint2D,
@@ -184,11 +182,11 @@ export function useFloorplanBackgroundPlacement({
}
if (isRoofBuildActive) {
- const bypassSnap = shiftPressed || event.shiftKey
- const snappedPoint = alignFloorplanDraftPoint(
- bypassSnap ? planPoint : getSnappedFloorplanPoint(planPoint),
- { bypass: event.altKey || bypassSnap },
- )
+ // Footprint placement (polygon context: grid / lines / off, no angle),
+ // mode-driven to match the chip. Alt forces (skips alignment).
+ const snappedPoint = alignFloorplanDraftPoint(getSnappedFloorplanPoint(planPoint), {
+ bypass: event.altKey || !isMagneticSnapActive(),
+ })
emitFloorplanGridEvent('click', snappedPoint, event)
setCursorPoint(snappedPoint)
@@ -202,34 +200,29 @@ export function useFloorplanBackgroundPlacement({
}
if (isFenceBuildActive) {
- const bypassSnap = shiftPressed || event.shiftKey
- // Fence draft: grid snap (+ existing-wall/fence endpoint snap), then
- // Figma alignment — endpoint snap wins (same precedence as move).
- // While a draft is open the segment locks to 15° rays from its
- // start unless Shift is held; Shift bypasses grid, magnetic,
- // angle, and alignment snap. `gridSnap` keeps the regular snap
- // on the world XZ grid even when the building is rotated.
+ // Fence draft: mode-driven (matches the chip), same as the move
+ // preview. `grid` snaps to the world XZ grid (rotation-safe via the
+ // `gridSnap` callback), `angles` locks 15° rays from the start, `lines`
+ // pulls onto walls / fences / alignment, `off` is free. Alt forces.
const fenceStep = getSegmentGridStep()
- const fenceAngleSnap = fenceDraftStart !== null && !bypassSnap && isAngleSnapActive()
+ const fenceAngleSnap = fenceDraftStart !== null && isAngleSnapActive()
const fenceSnapped = snapFenceDraftPoint({
point: planPoint,
walls,
fences,
start: fenceDraftStart ?? undefined,
angleSnap: fenceAngleSnap,
- bypassSnap,
- magnetic: !bypassSnap && isMagneticSnapActive(),
+ magnetic: isMagneticSnapActive(),
gridSnap: (p) => worldGridSnap(p, fenceStep),
})
- const fenceGridBase = bypassSnap ? planPoint : worldGridSnap(planPoint, fenceStep)
+ const fenceGridBase = worldGridSnap(planPoint, fenceStep)
const fenceLocked =
- !bypassSnap &&
- (fenceSnapped[0] !== fenceGridBase[0] || fenceSnapped[1] !== fenceGridBase[1])
+ fenceSnapped[0] !== fenceGridBase[0] || fenceSnapped[1] !== fenceGridBase[1]
const snappedPoint =
fenceLocked || fenceAngleSnap
? fenceSnapped
: alignFloorplanDraftPoint(fenceSnapped, {
- bypass: event.altKey || bypassSnap || !isMagneticSnapActive(),
+ bypass: event.altKey || !isMagneticSnapActive(),
})
emitFloorplanGridEvent('click', snappedPoint, event)
@@ -310,27 +303,22 @@ export function useFloorplanBackgroundPlacement({
// / draftEnd state in the floor plan would never update, leaving
// the dashed-line draft preview invisible.
if (isWallBuildActive) {
- const bypassSnap = shiftPressed || event.shiftKey
- // Wall draft: grid snap (+ existing-wall endpoint/join snap), then
- // Figma alignment — endpoint/join snap wins (same precedence as the
- // move-preview branch), so committing onto a corner still works.
- // While a draft is open the segment locks to 15° rays from its
- // start unless Shift is held; Shift bypasses grid, magnetic,
- // angle, and alignment snap. `gridSnap` keeps the regular snap
- // on the world XZ grid even when the building is rotated.
+ // Wall draft: mode-driven (matches the chip + the move-preview branch).
+ // `grid` snaps to the world XZ grid (rotation-safe via `gridSnap`),
+ // `angles` locks 15° rays from the start, `lines` pulls the endpoint
+ // onto existing wall corners / edges + alignment, `off` is free.
+ // (Alt = commit a single wall, handled below — not a snap modifier.)
const wallStep = getSegmentGridStep()
- const wallAngleSnap = draftStart !== null && !bypassSnap && isAngleSnapActive()
+ const wallAngleSnap = draftStart !== null && isAngleSnapActive()
const wallSnapped = snapWallDraftPoint({
point: planPoint,
walls,
start: draftStart ?? undefined,
angleSnap: wallAngleSnap,
- bypassSnap,
gridSnap: (p) => worldGridSnap(p, wallStep),
})
- const wallGridBase = bypassSnap ? planPoint : worldGridSnap(planPoint, wallStep)
- const wallLocked =
- !bypassSnap && (wallSnapped[0] !== wallGridBase[0] || wallSnapped[1] !== wallGridBase[1])
+ const wallGridBase = worldGridSnap(planPoint, wallStep)
+ const wallLocked = wallSnapped[0] !== wallGridBase[0] || wallSnapped[1] !== wallGridBase[1]
let snappedPoint = wallSnapped
if (wallLocked) {
useAlignmentGuides.getState().clear()
@@ -340,7 +328,7 @@ export function useFloorplanBackgroundPlacement({
// Figma alignment pulls the endpoint onto existing wall corners /
// edges, so it is a line snap — suppress it whenever magnetic snap
// is off (`'off'` / `'angles'`), matching the wall-geometry snap.
- bypass: event.altKey || bypassSnap || !isMagneticSnapActive(),
+ bypass: event.altKey || !isMagneticSnapActive(),
})
}
@@ -414,7 +402,6 @@ export function useFloorplanBackgroundPlacement({
setFenceDraftStart,
setRoofDraftEnd,
setRoofDraftStart,
- shiftPressed,
snapWallDraftPoint,
snapPolygonDraftPoint,
toPoint2D,
diff --git a/packages/editor/src/store/use-floorplan-draft-preview.ts b/packages/editor/src/store/use-floorplan-draft-preview.ts
index eb4ba391..a54ce08c 100644
--- a/packages/editor/src/store/use-floorplan-draft-preview.ts
+++ b/packages/editor/src/store/use-floorplan-draft-preview.ts
@@ -26,18 +26,45 @@ type FloorplanDraftPreviewState = {
* single hottest 2D update — keeping it out of panel state is what stops the
* panel re-rendering per move. `null` when idle. */
cursorPosition: SvgPoint | null
+ /** Live END point of the open wall / fence / roof draft segment — the per-move
+ * endpoint that drives the 2D draft polygon + measurement. Each is `null`
+ * unless that tool's draft is open. The START points stay in panel state
+ * (set per click, low-frequency). */
+ wallDraftEnd: WallPlanPoint | null
+ fenceDraftEnd: WallPlanPoint | null
+ roofDraftEnd: WallPlanPoint | null
/** Set the snapped cursor point. No-ops (skips the store update, so
* subscribers don't re-render) when unchanged — `grid:move` fires far more
* often than the snapped cell actually changes. */
setCursorPoint(point: WallPlanPoint | null): void
/** Set the screen-space cursor point (deduped on x/y). */
setCursorPosition(point: SvgPoint | null): void
+ setWallDraftEnd(point: WallPlanPoint | null): void
+ setFenceDraftEnd(point: WallPlanPoint | null): void
+ setRoofDraftEnd(point: WallPlanPoint | null): void
reset(): void
}
+function setPlanPointField(
+ field: 'wallDraftEnd' | 'fenceDraftEnd' | 'roofDraftEnd',
+ point: WallPlanPoint | null,
+) {
+ return (
+ state: FloorplanDraftPreviewState,
+ ): Partial | typeof state => {
+ const prev = state[field]
+ if (!point && !prev) return state
+ if (point && prev && prev[0] === point[0] && prev[1] === point[1]) return state
+ return { [field]: point }
+ }
+}
+
export const useFloorplanDraftPreview = create((set) => ({
cursorPoint: null,
cursorPosition: null,
+ wallDraftEnd: null,
+ fenceDraftEnd: null,
+ roofDraftEnd: null,
setCursorPoint: (point) =>
set((state) => {
const prev = state.cursorPoint
@@ -52,10 +79,23 @@ export const useFloorplanDraftPreview = create((set)
if (point && prev && prev.x === point.x && prev.y === point.y) return state
return { cursorPosition: point }
}),
+ setWallDraftEnd: (point) => set(setPlanPointField('wallDraftEnd', point)),
+ setFenceDraftEnd: (point) => set(setPlanPointField('fenceDraftEnd', point)),
+ setRoofDraftEnd: (point) => set(setPlanPointField('roofDraftEnd', point)),
reset: () =>
set((state) =>
- state.cursorPoint === null && state.cursorPosition === null
+ state.cursorPoint === null &&
+ state.cursorPosition === null &&
+ state.wallDraftEnd === null &&
+ state.fenceDraftEnd === null &&
+ state.roofDraftEnd === null
? state
- : { cursorPoint: null, cursorPosition: null },
+ : {
+ cursorPoint: null,
+ cursorPosition: null,
+ wallDraftEnd: null,
+ fenceDraftEnd: null,
+ roofDraftEnd: null,
+ },
),
}))