feat(editor): node-declared per-context snapping + contextual HUD + painter scope
Generify the snapping/modifier HUD off the FSM scope and node declarations
instead of wall-creation-shaped, leaking pills.
- Per-context snapping (`snappingModeByContext`, persisted): wall / item /
polygon mode-sets with exclusive modes (grid | lines | angles | off), each
doing exactly what its chip says. Context is node-declared via the new
`NodeDefinition.snapProfile` ('item' | 'structural'); the resolver maps
(profile × action) → context with no per-kind switch.
- Scope-driven HUD: helper-manager reads the interaction scope; reshaping
(endpoint/curve/boundary) and item move get their own chip, no select-hint
leak. Rotate R/T rounds to 45°; Alt = force-place only (hidden for
structural kinds); Shift = cycle everywhere.
- Slab/ceiling drafting: Shift=cycle, mode-aware grid/angle, Enter finishes
(minDraftVertices); polygon boundary vertex/edge drag begins a reshaping
scope. Fix grid/angle being ignored on boundary edit + slab creation:
make resolveSurfacePlanPointSnap exclusive (alignment gated on magnetic) so
grid/angles keep the snapped fallback instead of the raw cursor.
- Painter application scope: node-derived (single/object/matching/room) from
the hovered node, cyclable via Shift, single-source HUD chip.
- Remove the redundant GridSnapControl from view-toggles (grid step lives in
the contextual HUD now).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
b8b3d35f26
commit
04f1b0d59e
@@ -19,6 +19,7 @@ import {
|
||||
formatAngleRadians,
|
||||
getAngleToSegmentReference,
|
||||
getSegmentAngleReferenceAtPoint,
|
||||
isAngleSnapActive,
|
||||
isMagneticSnapActive,
|
||||
isSegmentLongEnough,
|
||||
MeasurementPill,
|
||||
@@ -177,7 +178,6 @@ function getLinkedWallUpdates(
|
||||
export const MoveWallEndpointTool: React.FC<{ target: MovingWallEndpoint }> = ({ target }) => {
|
||||
const hasDraggedRef = useRef(false)
|
||||
const previousGridPosRef = useRef<WallPlanPoint | null>(null)
|
||||
const shiftPressedRef = useRef(false)
|
||||
const altPressedRef = useRef(false)
|
||||
const nodeIdRef = useRef(target.wall.id)
|
||||
const originalStartRef = useRef<WallPlanPoint>([...target.wall.start] as WallPlanPoint)
|
||||
@@ -288,21 +288,17 @@ export const MoveWallEndpointTool: React.FC<{ target: MovingWallEndpoint }> = ({
|
||||
|
||||
const onGridMove = (event: GridEvent) => {
|
||||
const planPoint: WallPlanPoint = [event.localPosition[0], event.localPosition[2]]
|
||||
// Endpoint *move* snaps to the grid (and to other wall corners) —
|
||||
// 45° angle snap is for the initial draft, where it gives clean
|
||||
// orthogonal corners; here it would fight every perpendicular
|
||||
// drag by warping the endpoint onto the nearest 45° line from
|
||||
// the fixed corner.
|
||||
//
|
||||
// Shift is a hard snap bypass: raw endpoint position, no grid,
|
||||
// no magnetic wall snap, and no alignment guide snap.
|
||||
const bypassSnap = shiftPressedRef.current || event.nativeEvent.shiftKey
|
||||
// Endpoint move honours the active snapping mode (the HUD chip): grid →
|
||||
// lattice; lines → magnetic corner/alignment snap; angles → lock the
|
||||
// segment to 15° rays from the FIXED corner; off → raw. No Shift bypass —
|
||||
// Shift cycles the mode now, and Off is the bypass.
|
||||
const snapResult = snapWallDraftPointDetailed({
|
||||
point: planPoint,
|
||||
walls: levelWalls,
|
||||
ignoreWallIds: [nodeId],
|
||||
bypassSnap,
|
||||
magnetic: !bypassSnap && isMagneticSnapActive(),
|
||||
start: fixedPoint,
|
||||
angleSnap: isAngleSnapActive(),
|
||||
magnetic: isMagneticSnapActive(),
|
||||
})
|
||||
const snappedPoint = snapResult.point
|
||||
|
||||
@@ -312,8 +308,10 @@ export const MoveWallEndpointTool: React.FC<{ target: MovingWallEndpoint }> = ({
|
||||
// candidate, so the dot always sits on an actual point (endpoint /
|
||||
// midpoint), never an empty-space bbox corner. Layered on top of the
|
||||
// grid + corner snap above; Alt is reserved for corner-detach here.
|
||||
// Alignment axes are the "Lines" snap, so gate them on the magnetic flag —
|
||||
// Off / Grid / Angles must not pull the endpoint onto other elements' lines.
|
||||
let alignedPoint = snappedPoint
|
||||
if (!bypassSnap && wallAlignmentCandidates.length > 0) {
|
||||
if (isMagneticSnapActive() && wallAlignmentCandidates.length > 0) {
|
||||
const ar = resolveAlignment({
|
||||
moving: [{ nodeId, kind: 'corner', x: snappedPoint[0], z: snappedPoint[1] }],
|
||||
candidates: wallAlignmentCandidates,
|
||||
@@ -328,7 +326,6 @@ export const MoveWallEndpointTool: React.FC<{ target: MovingWallEndpoint }> = ({
|
||||
}
|
||||
|
||||
if (
|
||||
!bypassSnap &&
|
||||
previousGridPosRef.current &&
|
||||
(alignedPoint[0] !== previousGridPosRef.current[0] ||
|
||||
alignedPoint[1] !== previousGridPosRef.current[1])
|
||||
@@ -414,9 +411,6 @@ export const MoveWallEndpointTool: React.FC<{ target: MovingWallEndpoint }> = ({
|
||||
if (event.target instanceof HTMLInputElement || event.target instanceof HTMLTextAreaElement) {
|
||||
return
|
||||
}
|
||||
if (event.key === 'Shift') {
|
||||
shiftPressedRef.current = true
|
||||
}
|
||||
if (event.key === 'Alt') {
|
||||
altPressedRef.current = true
|
||||
setAltPressed(true)
|
||||
@@ -424,9 +418,6 @@ export const MoveWallEndpointTool: React.FC<{ target: MovingWallEndpoint }> = ({
|
||||
}
|
||||
|
||||
const onKeyUp = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Shift') {
|
||||
shiftPressedRef.current = false
|
||||
}
|
||||
if (event.key === 'Alt') {
|
||||
altPressedRef.current = false
|
||||
setAltPressed(false)
|
||||
@@ -434,7 +425,6 @@ export const MoveWallEndpointTool: React.FC<{ target: MovingWallEndpoint }> = ({
|
||||
}
|
||||
|
||||
const onWindowBlur = () => {
|
||||
shiftPressedRef.current = false
|
||||
altPressedRef.current = false
|
||||
setAltPressed(false)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user