fix(editor): 2D slab/zone/ceiling drafting honors the active snapping mode

The polygon-draft snap path used the legacy model: bypassSnap = shiftPressed and
angleSnap = pointCount > 0 && !bypassSnap, so the 15deg angle lock engaged after
the first vertex regardless of mode — hijacking grid/lines/off into angle-snap
even though the HUD chip showed the right mode.

Migrate all three placement paths (move preview, single-click vertex, double-
click close) to the unified model: angleSnap = isAngleSnapActive(); grid flows
through snapToHalf (step 0 in non-grid modes); wall-snap/alignment already gates
on isMagneticSnapActive(). Behavior now matches the chip — grid quantizes,
angles locks 15deg rays, lines snaps to walls/alignment, off is free. Drop the
now-dead bypassSnap param from snapPolygonDraftPoint.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Wassim SAMAD
2026-06-25 13:45:30 -04:00
co-authored by Claude Opus 4.8
parent a769b45756
commit bc9e075b2d
2 changed files with 25 additions and 38 deletions
@@ -2167,15 +2167,14 @@ function snapPolygonDraftPoint({
point, point,
start, start,
angleSnap, angleSnap,
bypassSnap,
}: { }: {
point: WallPlanPoint point: WallPlanPoint
start?: WallPlanPoint start?: WallPlanPoint
angleSnap: boolean angleSnap: boolean
bypassSnap?: boolean
}): WallPlanPoint { }): WallPlanPoint {
if (bypassSnap) return point // `snapToHalf`'s default step is 0 in any non-`grid` mode, so the grid branch
// passes the raw point through for `lines` / `off` (where wall-snap /
// alignment, run by the caller, takes over) — no explicit bypass needed.
if (!(start && angleSnap)) { if (!(start && angleSnap)) {
return [snapToHalf(point[0]), snapToHalf(point[1])] return [snapToHalf(point[0]), snapToHalf(point[1])]
} }
@@ -8624,24 +8623,23 @@ export function FloorplanPanel({
} }
if (isCeilingBuildActive) { if (isCeilingBuildActive) {
const bypassSnap = shiftPressed || event.shiftKey // Polygon vertex snapping is governed by the active snapping mode (the
// Polygon vertex: grid (snapToHalf) or 15° angle snap from the // chip on the right): `grid` quantizes via `snapToHalf` (whose step is
// previous vertex. Wall magnetic snap may still win, while // 0 — i.e. off — in any non-grid mode), `angles` locks to 15° rays from
// generic alignment runs only when angle snap is OFF (first vertex, // the previous vertex, `lines` pulls onto wall corners / alignment
// or Shift held) so it does not pull a locked angle sideways. // guides, `off` is free. No Shift hold-to-bypass; Alt forces (skips
const angleSnap = ceilingDraftPoints.length > 0 && !bypassSnap // alignment).
const angleSnap = ceilingDraftPoints.length > 0 && isAngleSnapActive()
const fallbackPoint = snapPolygonDraftPoint({ const fallbackPoint = snapPolygonDraftPoint({
point: planPoint, point: planPoint,
start: ceilingDraftPoints[ceilingDraftPoints.length - 1], start: ceilingDraftPoints[ceilingDraftPoints.length - 1],
angleSnap, angleSnap,
bypassSnap,
}) })
const snappedPoint = resolveCeilingPlanPointSnap({ const snappedPoint = resolveCeilingPlanPointSnap({
rawPoint: planPoint, rawPoint: planPoint,
fallbackPoint, fallbackPoint,
levelId, levelId,
altKey: event.altKey, altKey: event.altKey,
shiftKey: bypassSnap,
align: !angleSnap, align: !angleSnap,
}).point }).point
@@ -8719,13 +8717,15 @@ export function FloorplanPanel({
// the local polygon-draft state actually updates as the cursor // the local polygon-draft state actually updates as the cursor
// moves (the catch-all would otherwise swallow the move event). // moves (the catch-all would otherwise swallow the move event).
if (isPolygonBuildActive) { if (isPolygonBuildActive) {
const bypassSnap = shiftPressed || event.shiftKey // Mode-driven (matches the chip): `grid` quantizes (`snapToHalf`'s step
const angleSnap = activePolygonDraftPoints.length > 0 && !bypassSnap // is 0 in non-grid modes), `angles` locks 15° rays from the previous
// vertex, `lines` snaps onto wall corners / alignment guides, `off` is
// free. No Shift bypass; Alt forces (skips alignment).
const angleSnap = activePolygonDraftPoints.length > 0 && isAngleSnapActive()
const fallbackPoint = snapPolygonDraftPoint({ const fallbackPoint = snapPolygonDraftPoint({
point: planPoint, point: planPoint,
start: activePolygonDraftPoints[activePolygonDraftPoints.length - 1], start: activePolygonDraftPoints[activePolygonDraftPoints.length - 1],
angleSnap, angleSnap,
bypassSnap,
}) })
let snappedPoint = fallbackPoint let snappedPoint = fallbackPoint
if (isSlabBuildActive) { if (isSlabBuildActive) {
@@ -8734,14 +8734,13 @@ export function FloorplanPanel({
fallbackPoint, fallbackPoint,
levelId, levelId,
altKey: event.altKey, altKey: event.altKey,
shiftKey: bypassSnap,
align: !angleSnap, align: !angleSnap,
}).point }).point
} else if (angleSnap) { } else if (angleSnap) {
useAlignmentGuides.getState().clear() useAlignmentGuides.getState().clear()
} else { } else {
snappedPoint = alignFloorplanDraftPoint(fallbackPoint, { snappedPoint = alignFloorplanDraftPoint(fallbackPoint, {
bypass: event.altKey || bypassSnap, bypass: event.altKey || !isMagneticSnapActive(),
}) })
} }
@@ -8751,7 +8750,7 @@ export function FloorplanPanel({
setCursorPoint((previousPoint) => { setCursorPoint((previousPoint) => {
const hasChanged = !(previousPoint && pointsEqual(previousPoint, snappedPoint)) const hasChanged = !(previousPoint && pointsEqual(previousPoint, snappedPoint))
if (!bypassSnap && hasChanged && activePolygonDraftPoints.length > 0) { if (hasChanged && activePolygonDraftPoints.length > 0) {
sfxEmitter.emit('sfx:grid-snap') sfxEmitter.emit('sfx:grid-snap')
} }
return snappedPoint return snappedPoint
@@ -9396,13 +9395,11 @@ export function FloorplanPanel({
return return
} }
const bypassSnap = shiftPressed || event.shiftKey const angleSnap = activePolygonDraftPoints.length > 0 && isAngleSnapActive()
const angleSnap = activePolygonDraftPoints.length > 0 && !bypassSnap
const fallbackPoint = snapPolygonDraftPoint({ const fallbackPoint = snapPolygonDraftPoint({
point: planPoint, point: planPoint,
start: activePolygonDraftPoints[activePolygonDraftPoints.length - 1], start: activePolygonDraftPoints[activePolygonDraftPoints.length - 1],
angleSnap, angleSnap,
bypassSnap,
}) })
if (isCeilingBuildActive) { if (isCeilingBuildActive) {
@@ -9411,7 +9408,6 @@ export function FloorplanPanel({
fallbackPoint, fallbackPoint,
levelId, levelId,
altKey: event.altKey, altKey: event.altKey,
shiftKey: bypassSnap,
align: !angleSnap, align: !angleSnap,
}).point }).point
emitFloorplanGridEvent('double-click', snappedPoint, event) emitFloorplanGridEvent('double-click', snappedPoint, event)
@@ -9427,7 +9423,6 @@ export function FloorplanPanel({
fallbackPoint, fallbackPoint,
levelId, levelId,
altKey: event.altKey, altKey: event.altKey,
shiftKey: bypassSnap,
align: !angleSnap, align: !angleSnap,
}).point }).point
// Slab is registry-driven: forward the double-click so the 3D tool // Slab is registry-driven: forward the double-click so the 3D tool
@@ -9448,7 +9443,6 @@ export function FloorplanPanel({
isRoofBuildActive, isRoofBuildActive,
isZoneBuildActive, isZoneBuildActive,
levelId, levelId,
shiftPressed,
], ],
) )
@@ -76,7 +76,6 @@ type UseFloorplanBackgroundPlacementArgs = {
point: WallPlanPoint point: WallPlanPoint
start?: WallPlanPoint start?: WallPlanPoint
angleSnap: boolean angleSnap: boolean
bypassSnap?: boolean
}) => WallPlanPoint }) => WallPlanPoint
toPoint2D: (point: WallPlanPoint) => { x: number; y: number } toPoint2D: (point: WallPlanPoint) => { x: number; y: number }
walls: WallNode[] walls: WallNode[]
@@ -161,24 +160,21 @@ export function useFloorplanBackgroundPlacement({
} }
if (isCeilingBuildActive) { if (isCeilingBuildActive) {
const bypassSnap = shiftPressed || event.shiftKey // Align the committed vertex the same way the move-preview did, so the
// Align the committed vertex the same way the move-preview did, so // placed point matches what the user saw — mode-driven (the chip):
// the placed point matches what the user saw. Wall magnetic snap may // `grid` quantizes, `angles` locks 15° rays, `lines` snaps onto walls /
// still win; generic alignment is skipped when angle snap owns the // alignment, `off` is free. Alt forces (skips alignment).
// vertex (matches the move branch). const angleSnap = ceilingDraftPoints.length > 0 && isAngleSnapActive()
const angleSnap = ceilingDraftPoints.length > 0 && !bypassSnap
const fallbackPoint = snapPolygonDraftPoint({ const fallbackPoint = snapPolygonDraftPoint({
point: planPoint, point: planPoint,
start: ceilingDraftPoints[ceilingDraftPoints.length - 1], start: ceilingDraftPoints[ceilingDraftPoints.length - 1],
angleSnap, angleSnap,
bypassSnap,
}) })
const snappedPoint = resolveCeilingPlanPointSnap({ const snappedPoint = resolveCeilingPlanPointSnap({
rawPoint: planPoint, rawPoint: planPoint,
fallbackPoint, fallbackPoint,
levelId, levelId,
altKey: event.altKey, altKey: event.altKey,
shiftKey: bypassSnap,
align: !angleSnap, align: !angleSnap,
}).point }).point
@@ -272,13 +268,11 @@ export function useFloorplanBackgroundPlacement({
// swallow the click and skip local draft state updates — leaving // swallow the click and skip local draft state updates — leaving
// the 2D draft polygon invisible while the 3D tool builds fine). // the 2D draft polygon invisible while the 3D tool builds fine).
if (isPolygonBuildActive) { if (isPolygonBuildActive) {
const bypassSnap = shiftPressed || event.shiftKey const angleSnap = activePolygonDraftPoints.length > 0 && isAngleSnapActive()
const angleSnap = activePolygonDraftPoints.length > 0 && !bypassSnap
const fallbackPoint = snapPolygonDraftPoint({ const fallbackPoint = snapPolygonDraftPoint({
point: planPoint, point: planPoint,
start: activePolygonDraftPoints[activePolygonDraftPoints.length - 1], start: activePolygonDraftPoints[activePolygonDraftPoints.length - 1],
angleSnap, angleSnap,
bypassSnap,
}) })
let snappedPoint = fallbackPoint let snappedPoint = fallbackPoint
if (isSlabBuildActive) { if (isSlabBuildActive) {
@@ -287,12 +281,11 @@ export function useFloorplanBackgroundPlacement({
fallbackPoint, fallbackPoint,
levelId, levelId,
altKey: event.altKey, altKey: event.altKey,
shiftKey: bypassSnap,
align: !angleSnap, align: !angleSnap,
}).point }).point
} else if (!angleSnap) { } else if (!angleSnap) {
snappedPoint = alignFloorplanDraftPoint(fallbackPoint, { snappedPoint = alignFloorplanDraftPoint(fallbackPoint, {
bypass: event.altKey || bypassSnap, bypass: event.altKey || !isMagneticSnapActive(),
}) })
} }