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,
|
alignFloorplanDraftPoint,
|
||||||
type FencePlanPoint,
|
type FencePlanPoint,
|
||||||
getSegmentGridStep,
|
getSegmentGridStep,
|
||||||
|
isAngleSnapActive,
|
||||||
isMagneticSnapActive,
|
isMagneticSnapActive,
|
||||||
isSegmentLongEnough,
|
isSegmentLongEnough,
|
||||||
snapBuildingLocalToWorldGrid,
|
snapBuildingLocalToWorldGrid,
|
||||||
snapFenceDraftPoint,
|
snapFenceDraftPoint,
|
||||||
snapScalarToGrid,
|
snapScalarToGrid,
|
||||||
useAlignmentGuides,
|
useAlignmentGuides,
|
||||||
WALL_GRID_STEP,
|
|
||||||
} from '@pascal-app/editor'
|
} from '@pascal-app/editor'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -158,24 +158,30 @@ export const fenceMoveEndpointAffordance: FloorplanAffordance<FenceNode> = {
|
|||||||
// itself is excluded via `ignoreFenceIds`).
|
// itself is excluded via `ignoreFenceIds`).
|
||||||
const sceneNodes = useScene.getState().nodes
|
const sceneNodes = useScene.getState().nodes
|
||||||
const { walls: nextWalls, fences: nextFences } = collectLevel(sceneNodes, parentId)
|
const { walls: nextWalls, fences: nextFences } = collectLevel(sceneNodes, parentId)
|
||||||
// Endpoint move = grid snap only; the 45°-from-start angle
|
// The grid step follows the active snapping mode (`getSegmentGridStep()`
|
||||||
// snap is draft-only. Shift bypasses grid, magnetic, and alignment snap.
|
// 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({
|
const snapped = snapFenceDraftPoint({
|
||||||
point: planPoint as FencePlanPoint,
|
point: planPoint as FencePlanPoint,
|
||||||
walls: nextWalls,
|
walls: nextWalls,
|
||||||
fences: nextFences,
|
fences: nextFences,
|
||||||
ignoreFenceIds: [node.id],
|
ignoreFenceIds: [node.id],
|
||||||
bypassSnap: modifiers.shiftKey,
|
start: angleLocked ? fixedPoint : undefined,
|
||||||
magnetic: !modifiers.shiftKey && isMagneticSnapActive(),
|
angleSnap: angleLocked,
|
||||||
gridSnap: (p) => snapBuildingLocalToWorldGrid(p, WALL_GRID_STEP) as FencePlanPoint,
|
magnetic: isMagneticSnapActive(),
|
||||||
|
gridSnap: (p) => snapBuildingLocalToWorldGrid(p, getSegmentGridStep()) as FencePlanPoint,
|
||||||
})
|
})
|
||||||
// Figma-style alignment on the dragged endpoint — snaps it onto
|
// Figma-style alignment on the dragged endpoint — snaps it onto
|
||||||
// another object's edge / wall face and publishes a guide, matching
|
// another object's edge / wall face and publishes a guide, matching
|
||||||
// the 3D fence endpoint action. The dragged fence and its linked
|
// the 3D fence endpoint action. It is a line snap, so gate it on the
|
||||||
// siblings (which cascade with the endpoint) are excluded from the
|
// magnetic (`'lines'`) mode. The dragged fence and its linked siblings
|
||||||
// candidate pool. Alt is reserved for detach here, NOT bypass.
|
// (which cascade with the endpoint) are excluded from the candidate
|
||||||
|
// pool. Alt is reserved for detach here, NOT bypass.
|
||||||
const aligned = alignFloorplanDraftPoint(snapped, {
|
const aligned = alignFloorplanDraftPoint(snapped, {
|
||||||
bypass: modifiers.shiftKey,
|
bypass: !isMagneticSnapActive(),
|
||||||
excludeIds: [node.id, ...linkedOriginals.map((l) => l.id)],
|
excludeIds: [node.id, ...linkedOriginals.map((l) => l.id)],
|
||||||
}) as FencePlanPoint
|
}) as FencePlanPoint
|
||||||
const nextStart = endpoint === 'start' ? aligned : fixedPoint
|
const nextStart = endpoint === 'start' ? aligned : fixedPoint
|
||||||
|
|||||||
@@ -13,13 +13,13 @@ import {
|
|||||||
import {
|
import {
|
||||||
alignFloorplanDraftPoint,
|
alignFloorplanDraftPoint,
|
||||||
getSegmentGridStep,
|
getSegmentGridStep,
|
||||||
|
isAngleSnapActive,
|
||||||
isMagneticSnapActive,
|
isMagneticSnapActive,
|
||||||
isSegmentLongEnough,
|
isSegmentLongEnough,
|
||||||
snapBuildingLocalToWorldGrid,
|
snapBuildingLocalToWorldGrid,
|
||||||
snapScalarToGrid,
|
snapScalarToGrid,
|
||||||
snapWallDraftPoint,
|
snapWallDraftPoint,
|
||||||
useAlignmentGuides,
|
useAlignmentGuides,
|
||||||
WALL_GRID_STEP,
|
|
||||||
type WallPlanPoint,
|
type WallPlanPoint,
|
||||||
} from '@pascal-app/editor'
|
} from '@pascal-app/editor'
|
||||||
|
|
||||||
@@ -183,19 +183,28 @@ export const wallMoveEndpointAffordance: FloorplanAffordance<WallNode> = {
|
|||||||
// the legacy flow.
|
// the legacy flow.
|
||||||
const sceneNodes = useScene.getState().nodes
|
const sceneNodes = useScene.getState().nodes
|
||||||
const walls = collectLevelWalls(sceneNodes, node.id)
|
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({
|
const snapped = snapWallDraftPoint({
|
||||||
point: planPoint as WallPlanPoint,
|
point: planPoint as WallPlanPoint,
|
||||||
walls,
|
walls,
|
||||||
ignoreWallIds: [node.id],
|
ignoreWallIds: [node.id],
|
||||||
|
start: angleLocked ? fixedPoint : undefined,
|
||||||
|
angleSnap: angleLocked,
|
||||||
magnetic: isMagneticSnapActive(),
|
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
|
// Figma-style alignment on the dragged corner — snaps it onto another
|
||||||
// object's edge / wall face and publishes a guide. The dragged wall
|
// object's edge / wall face and publishes a guide. It is a line snap,
|
||||||
// and its linked siblings (which cascade with the corner) are excluded
|
// so gate it on the magnetic (`'lines'`) mode like the draft tool does.
|
||||||
// from the candidate pool. Alt is reserved for detach, NOT bypass.
|
// 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, {
|
const aligned = alignFloorplanDraftPoint(snapped, {
|
||||||
|
bypass: !isMagneticSnapActive(),
|
||||||
excludeIds: [node.id, ...linkedWalls.map((w) => w.id)],
|
excludeIds: [node.id, ...linkedWalls.map((w) => w.id)],
|
||||||
}) as WallPlanPoint
|
}) as WallPlanPoint
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user