fix(editor): unify wall-endpoint move activation; stop wall-move from co-firing
Two fixes to the wall-endpoint reshape interaction.
selection-manager: a node:click is synthesized on R3F pointer-up, so an endpoint
handle that sits on the wall body lets the wall mesh (raycast-hit behind it from
a 3D angle) emit its own click on the same release — selecting the wall and
arming its move tool on top of the endpoint move. Ignore the body click while an
`endpoint` reshape owns the pointer. Scoped to `endpoint` so hole-edit (which
relies on node clicks to exit) is unaffected.
move-endpoint-tool: a press-drag committed on release but a tap dismissed, and
whether the tap's release ran at all raced the window pointer-up listener
mounting a tick after the handle's pointerdown ("works once, then needs a long
press"). Unify on one rule: commit only when the endpoint actually moved,
otherwise stay armed. A tap now grabs the endpoint (it follows the cursor; the
next click after a move commits) exactly like a press-drag — both engage
identically. Drops the now-dead hasDraggedRef.
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
eeadec11a7
commit
5f603e83c4
@@ -1418,6 +1418,16 @@ export const SelectionManager = () => {
|
|||||||
// Skip if box-select just completed (drag ended over a node)
|
// Skip if box-select just completed (drag ended over a node)
|
||||||
if (boxSelectHandled) return
|
if (boxSelectHandled) return
|
||||||
|
|
||||||
|
// node:click is synthesized on pointer-up (use-node-events). A wall/fence
|
||||||
|
// endpoint handle sits ON the wall body, so from a 3D angle the wall mesh
|
||||||
|
// is raycast-hit behind it and the SAME pointer-up also emits the wall's
|
||||||
|
// click — which would select + arm the wall move tool on top of the
|
||||||
|
// endpoint move. While an endpoint reshape owns the pointer, ignore the
|
||||||
|
// body click so only the reshape tool handles the release. (Scoped to
|
||||||
|
// `endpoint`: hole-edit relies on node clicks to exit, just below.)
|
||||||
|
const activeScope = useInteractionScope.getState().scope
|
||||||
|
if (activeScope.kind === 'reshaping' && activeScope.reshape === 'endpoint') return
|
||||||
|
|
||||||
const node = event.node
|
const node = event.node
|
||||||
|
|
||||||
// A ceiling is selectable only through its corner handles, never via
|
// A ceiling is selectable only through its corner handles, never via
|
||||||
|
|||||||
@@ -177,7 +177,6 @@ function getLinkedWallUpdates(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const MoveWallEndpointTool: React.FC<{ target: MovingWallEndpoint }> = ({ target }) => {
|
export const MoveWallEndpointTool: React.FC<{ target: MovingWallEndpoint }> = ({ target }) => {
|
||||||
const hasDraggedRef = useRef(false)
|
|
||||||
const previousGridPosRef = useRef<WallPlanPoint | null>(null)
|
const previousGridPosRef = useRef<WallPlanPoint | null>(null)
|
||||||
const altPressedRef = useRef(false)
|
const altPressedRef = useRef(false)
|
||||||
const nodeIdRef = useRef(target.wall.id)
|
const nodeIdRef = useRef(target.wall.id)
|
||||||
@@ -366,7 +365,6 @@ export const MoveWallEndpointTool: React.FC<{ target: MovingWallEndpoint }> = ({
|
|||||||
triggerSFX('sfx:grid-snap')
|
triggerSFX('sfx:grid-snap')
|
||||||
}
|
}
|
||||||
previousGridPosRef.current = alignedPoint
|
previousGridPosRef.current = alignedPoint
|
||||||
hasDraggedRef.current = true
|
|
||||||
|
|
||||||
// Stand the magnetic beacon at the endpoint when it locked onto existing
|
// Stand the magnetic beacon at the endpoint when it locked onto existing
|
||||||
// wall geometry (corner / midpoint / crossing / wall body).
|
// wall geometry (corner / midpoint / crossing / wall body).
|
||||||
@@ -390,20 +388,22 @@ export const MoveWallEndpointTool: React.FC<{ target: MovingWallEndpoint }> = ({
|
|||||||
// through to the selection manager and arms the wall MOVE tool, a mode the
|
// through to the selection manager and arms the wall MOVE tool, a mode the
|
||||||
// user never asked for.
|
// user never asked for.
|
||||||
swallowNextClick()
|
swallowNextClick()
|
||||||
// Press-release without drag: dismiss the tool without committing.
|
|
||||||
if (!hasDraggedRef.current) {
|
|
||||||
useViewer.getState().setSelection({ selectedIds: [nodeId] })
|
|
||||||
setAngleLabel(null)
|
|
||||||
exitMoveMode()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const preview = previewRef.current ?? { start: originalStart, end: originalEnd }
|
const preview = previewRef.current ?? { start: originalStart, end: originalEnd }
|
||||||
const hasChanged = !(
|
const hasChanged = !(
|
||||||
samePoint(preview.start, originalStart) && samePoint(preview.end, originalEnd)
|
samePoint(preview.start, originalStart) && samePoint(preview.end, originalEnd)
|
||||||
)
|
)
|
||||||
|
|
||||||
if (hasChanged && isSegmentLongEnough(preview.start, preview.end)) {
|
// Endpoint still at its original spot: this release is the *grab* of a
|
||||||
|
// click-to-move (a tap on the handle, or a press that never dragged). Stay
|
||||||
|
// armed so the endpoint keeps following the cursor — the next release after
|
||||||
|
// an actual move commits. A press-drag and a click thus engage identically;
|
||||||
|
// previously the no-drag branch dismissed the tool, and whether it even ran
|
||||||
|
// raced the window pointer-up listener mounting (hence "works once, then
|
||||||
|
// needs a long press").
|
||||||
|
if (!hasChanged) return
|
||||||
|
|
||||||
|
if (isSegmentLongEnough(preview.start, preview.end)) {
|
||||||
wasCommitted = true
|
wasCommitted = true
|
||||||
|
|
||||||
const linkedUpdates = altPressedRef.current
|
const linkedUpdates = altPressedRef.current
|
||||||
|
|||||||
Reference in New Issue
Block a user