feat(nodes): mode-aware snapping for wall/fence endpoint moves in 2D
Wall and fence endpoint-move affordances hard-snapped to the grid via a hardcoded WALL_GRID_STEP and always ran Figma line-alignment, ignoring the active snapping mode. Now: - grid step follows getSegmentGridStep() (0 outside grid mode), so lines / angles / off no longer force a grid snap the mode chip says is inactive; - Figma alignment is gated on isMagneticSnapActive() (the lines mode); - angles mode angle-locks the endpoint off the fixed corner (free length), mirroring the draft tool; - fence drops its legacy Shift-bypass to match the wall's unified model (Alt stays as linked-segment detach). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
a80924e011
commit
7512fccdaf
@@ -15,13 +15,13 @@ import {
|
||||
alignFloorplanDraftPoint,
|
||||
type FencePlanPoint,
|
||||
getSegmentGridStep,
|
||||
isAngleSnapActive,
|
||||
isMagneticSnapActive,
|
||||
isSegmentLongEnough,
|
||||
snapBuildingLocalToWorldGrid,
|
||||
snapFenceDraftPoint,
|
||||
snapScalarToGrid,
|
||||
useAlignmentGuides,
|
||||
WALL_GRID_STEP,
|
||||
} from '@pascal-app/editor'
|
||||
|
||||
/**
|
||||
@@ -158,24 +158,30 @@ export const fenceMoveEndpointAffordance: FloorplanAffordance<FenceNode> = {
|
||||
// itself is excluded via `ignoreFenceIds`).
|
||||
const sceneNodes = useScene.getState().nodes
|
||||
const { walls: nextWalls, fences: nextFences } = collectLevel(sceneNodes, parentId)
|
||||
// Endpoint move = grid snap only; the 45°-from-start angle
|
||||
// snap is draft-only. Shift bypasses grid, magnetic, and alignment snap.
|
||||
// The grid step follows the active snapping mode (`getSegmentGridStep()`
|
||||
// is 0 outside grid mode), so `'lines' / 'angles' / 'off'` no longer
|
||||
// force a grid snap the mode chip says is inactive — matching the wall
|
||||
// endpoint affordance. In `'angles'` mode the endpoint angle-locks off
|
||||
// the fixed corner (free length); the angle path ignores `gridSnap`.
|
||||
const angleLocked = isAngleSnapActive()
|
||||
const snapped = snapFenceDraftPoint({
|
||||
point: planPoint as FencePlanPoint,
|
||||
walls: nextWalls,
|
||||
fences: nextFences,
|
||||
ignoreFenceIds: [node.id],
|
||||
bypassSnap: modifiers.shiftKey,
|
||||
magnetic: !modifiers.shiftKey && isMagneticSnapActive(),
|
||||
gridSnap: (p) => snapBuildingLocalToWorldGrid(p, WALL_GRID_STEP) as FencePlanPoint,
|
||||
start: angleLocked ? fixedPoint : undefined,
|
||||
angleSnap: angleLocked,
|
||||
magnetic: isMagneticSnapActive(),
|
||||
gridSnap: (p) => snapBuildingLocalToWorldGrid(p, getSegmentGridStep()) as FencePlanPoint,
|
||||
})
|
||||
// Figma-style alignment on the dragged endpoint — snaps it onto
|
||||
// another object's edge / wall face and publishes a guide, matching
|
||||
// the 3D fence endpoint action. The dragged fence and its linked
|
||||
// siblings (which cascade with the endpoint) are excluded from the
|
||||
// candidate pool. Alt is reserved for detach here, NOT bypass.
|
||||
// the 3D fence endpoint action. It is a line snap, so gate it on the
|
||||
// magnetic (`'lines'`) mode. The dragged fence and its linked siblings
|
||||
// (which cascade with the endpoint) are excluded from the candidate
|
||||
// pool. Alt is reserved for detach here, NOT bypass.
|
||||
const aligned = alignFloorplanDraftPoint(snapped, {
|
||||
bypass: modifiers.shiftKey,
|
||||
bypass: !isMagneticSnapActive(),
|
||||
excludeIds: [node.id, ...linkedOriginals.map((l) => l.id)],
|
||||
}) as FencePlanPoint
|
||||
const nextStart = endpoint === 'start' ? aligned : fixedPoint
|
||||
|
||||
@@ -13,13 +13,13 @@ import {
|
||||
import {
|
||||
alignFloorplanDraftPoint,
|
||||
getSegmentGridStep,
|
||||
isAngleSnapActive,
|
||||
isMagneticSnapActive,
|
||||
isSegmentLongEnough,
|
||||
snapBuildingLocalToWorldGrid,
|
||||
snapScalarToGrid,
|
||||
snapWallDraftPoint,
|
||||
useAlignmentGuides,
|
||||
WALL_GRID_STEP,
|
||||
type WallPlanPoint,
|
||||
} from '@pascal-app/editor'
|
||||
|
||||
@@ -183,19 +183,28 @@ export const wallMoveEndpointAffordance: FloorplanAffordance<WallNode> = {
|
||||
// the legacy flow.
|
||||
const sceneNodes = useScene.getState().nodes
|
||||
const walls = collectLevelWalls(sceneNodes, node.id)
|
||||
// Endpoint move = grid snap, never 45° from the fixed corner.
|
||||
// The grid step follows the active snapping mode (`getSegmentGridStep()`
|
||||
// is 0 outside grid mode), so `'lines' / 'angles' / 'off'` no longer
|
||||
// force a grid snap the mode chip says is inactive. In `'angles'` mode
|
||||
// the endpoint angle-locks off the fixed corner (free length), matching
|
||||
// the draft tool — the angle path ignores the `gridSnap` override.
|
||||
const angleLocked = isAngleSnapActive()
|
||||
const snapped = snapWallDraftPoint({
|
||||
point: planPoint as WallPlanPoint,
|
||||
walls,
|
||||
ignoreWallIds: [node.id],
|
||||
start: angleLocked ? fixedPoint : undefined,
|
||||
angleSnap: angleLocked,
|
||||
magnetic: isMagneticSnapActive(),
|
||||
gridSnap: (p) => snapBuildingLocalToWorldGrid(p, WALL_GRID_STEP),
|
||||
gridSnap: (p) => snapBuildingLocalToWorldGrid(p, getSegmentGridStep()),
|
||||
})
|
||||
// Figma-style alignment on the dragged corner — snaps it onto another
|
||||
// object's edge / wall face and publishes a guide. The dragged wall
|
||||
// and its linked siblings (which cascade with the corner) are excluded
|
||||
// from the candidate pool. Alt is reserved for detach, NOT bypass.
|
||||
// object's edge / wall face and publishes a guide. It is a line snap,
|
||||
// so gate it on the magnetic (`'lines'`) mode like the draft tool does.
|
||||
// The dragged wall and its linked siblings (which cascade with the
|
||||
// corner) are excluded from the candidate pool. Alt is detach, NOT bypass.
|
||||
const aligned = alignFloorplanDraftPoint(snapped, {
|
||||
bypass: !isMagneticSnapActive(),
|
||||
excludeIds: [node.id, ...linkedWalls.map((w) => w.id)],
|
||||
}) as WallPlanPoint
|
||||
|
||||
|
||||
Reference in New Issue
Block a user