feat(editor): wire reshape scope for edit-tool chips + 2D-only wall commit
The 2D affordance dispatcher (`startAffordanceDrag`) now begins the matching reshaping interaction scope (boundary / hole / curve / endpoint) on pointer-down and tears it down on release/cancel, matched by node id. This makes the contextual snapping HUD show the right chip during polygon vertex/edge and wall endpoint/curve edits, and lets `getActiveSnapContext()` resolve the correct per-context snapping mode the affordance snap math already reads. Wall creation is owned by the 3D `WallTool`, which is dead in 2D-only view (canvas `display:none`). Mirror the slab/ceiling 2D-only committers: commit locally via `createWallOnCurrentLevel`, gated on `viewMode === '2d'`, chaining the next segment from the committed wall's resolved end. Split/3D keep their single-owner tool commit. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
6f37783fbd
commit
a80924e011
@@ -39,11 +39,21 @@ import {
|
||||
resolveDirectRotationPatch,
|
||||
} from '../../../lib/direct-manipulation'
|
||||
import { createEditorApi } from '../../../lib/editor-api'
|
||||
import {
|
||||
type ActiveInteractionScope,
|
||||
boundaryReshapeScope,
|
||||
curveReshapeScope,
|
||||
endpointReshapeScope,
|
||||
holeEditScope,
|
||||
} from '../../../lib/interaction/scope'
|
||||
import { sfxEmitter } from '../../../lib/sfx-bus'
|
||||
import { clearSurfacePlanSnapFeedback } from '../../../lib/surface-plan-snap'
|
||||
import useDirectManipulationFeedback from '../../../store/use-direct-manipulation-feedback'
|
||||
import useEditor from '../../../store/use-editor'
|
||||
import { useEndpointReshape, useMovingNode } from '../../../store/use-interaction-scope'
|
||||
import useInteractionScope, {
|
||||
useEndpointReshape,
|
||||
useMovingNode,
|
||||
} from '../../../store/use-interaction-scope'
|
||||
import { suppressBoxSelectForPointer } from '../../tools/select/box-select-state'
|
||||
import { useFloorplanRender } from '../floorplan-render-context'
|
||||
import { FloorplanGeometryRenderer } from './floorplan-geometry-renderer'
|
||||
@@ -110,6 +120,40 @@ type ActiveDrag = {
|
||||
* rotate affordance measures it: `atan2(pointer − pivot)`.
|
||||
*/
|
||||
rotation?: { pivot: FloorplanPoint; initialAngle: number; radius: number }
|
||||
/**
|
||||
* Node id of the reshaping scope this drag began (boundary / curve / endpoint
|
||||
* edits), so the matching `endIf` on release/cancel tears down exactly this
|
||||
* scope. Unset for affordances that drive no snapping scope (resize / rotate).
|
||||
*/
|
||||
reshapeScopeNodeId?: string
|
||||
}
|
||||
|
||||
// Map a floor-plan affordance to the reshaping scope it represents, so the
|
||||
// dispatcher can drive the contextual snapping HUD (the chip) AND make
|
||||
// `getActiveSnapContext()` resolve the right mode-set during the edit. Geometry
|
||||
// edits that set a direction/shape map to a scope; resize / rotate / body-move
|
||||
// affordances return `null` (no polygon/wall snapping chip). Keyed off the
|
||||
// affordance name the kinds register (`move-vertex` / `move-edge` / `add-vertex`
|
||||
// / `curve` / `move-endpoint`).
|
||||
function affordanceReshapeScope(
|
||||
affordance: string,
|
||||
nodeId: string,
|
||||
payload: unknown,
|
||||
): ActiveInteractionScope | null {
|
||||
if (affordance.includes('vertex') || affordance.includes('edge')) {
|
||||
const holeIndex = (payload as { holeIndex?: number } | undefined)?.holeIndex
|
||||
return holeIndex !== undefined
|
||||
? holeEditScope({ nodeId, holeIndex })
|
||||
: boundaryReshapeScope(nodeId)
|
||||
}
|
||||
if (affordance.includes('curve')) {
|
||||
return curveReshapeScope(nodeId)
|
||||
}
|
||||
if (affordance.includes('endpoint')) {
|
||||
const endpoint = (payload as { endpoint?: 'start' | 'end' } | undefined)?.endpoint ?? 'end'
|
||||
return endpointReshapeScope(nodeId, endpoint)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -898,6 +942,15 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
|
||||
}
|
||||
}
|
||||
|
||||
// Begin the matching reshaping scope so the contextual snapping HUD shows
|
||||
// the right chip during the edit AND `getActiveSnapContext()` resolves the
|
||||
// polygon / wall mode-set the affordance's snap math reads. Torn down on
|
||||
// release / cancel below. `null` for resize / rotate (no snapping chip).
|
||||
const reshapeScope = affordanceReshapeScope(affordance, nodeId, payload)
|
||||
if (reshapeScope) {
|
||||
useInteractionScope.getState().begin(reshapeScope)
|
||||
}
|
||||
|
||||
dragRef.current = {
|
||||
pointerId: event.pointerId,
|
||||
handleId,
|
||||
@@ -905,6 +958,7 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
|
||||
snapshots,
|
||||
historyPaused: true,
|
||||
rotation,
|
||||
reshapeScopeNodeId: reshapeScope ? nodeId : undefined,
|
||||
}
|
||||
setActiveDragId(handleId)
|
||||
setSelection({ selectedIds: [nodeId] })
|
||||
@@ -914,6 +968,16 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
// Tear down the reshaping scope this drag opened (if any), matched by node
|
||||
// id so a concurrent scope from another path is never ended by mistake.
|
||||
const endReshapeScope = (drag: ActiveDrag) => {
|
||||
if (drag.reshapeScopeNodeId) {
|
||||
useInteractionScope
|
||||
.getState()
|
||||
.endIf((s) => s.kind === 'reshaping' && s.nodeId === drag.reshapeScopeNodeId)
|
||||
}
|
||||
}
|
||||
|
||||
const onPointerMove = (event: PointerEvent) => {
|
||||
const drag = dragRef.current
|
||||
if (!drag || event.pointerId !== drag.pointerId) return
|
||||
@@ -977,6 +1041,7 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
|
||||
drag.session.commit()
|
||||
sfxEmitter.emit('sfx:structure-build')
|
||||
clearSurfacePlanSnapFeedback()
|
||||
endReshapeScope(drag)
|
||||
dragRef.current = null
|
||||
setActiveDragId(null)
|
||||
setRotationOverlay(null)
|
||||
@@ -1029,6 +1094,7 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
|
||||
}
|
||||
|
||||
clearSurfacePlanSnapFeedback()
|
||||
endReshapeScope(drag)
|
||||
dragRef.current = null
|
||||
setActiveDragId(null)
|
||||
setRotationOverlay(null)
|
||||
@@ -1055,6 +1121,7 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
|
||||
const overrides = useLiveNodeOverrides.getState()
|
||||
for (const id of drag.session.affectedIds) overrides.clear(id)
|
||||
|
||||
endReshapeScope(drag)
|
||||
dragRef.current = null
|
||||
setActiveDragId(null)
|
||||
setRotationOverlay(null)
|
||||
@@ -1079,6 +1146,7 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
|
||||
}
|
||||
const overrides = useLiveNodeOverrides.getState()
|
||||
for (const id of drag.session.affectedIds) overrides.clear(id)
|
||||
endReshapeScope(drag)
|
||||
dragRef.current = null
|
||||
}
|
||||
// Clear any alignment guide a session left behind on mid-drag unmount.
|
||||
|
||||
@@ -160,6 +160,7 @@ import {
|
||||
DEFAULT_STAIR_WIDTH,
|
||||
} from '../tools/stair/stair-defaults'
|
||||
import {
|
||||
createWallOnCurrentLevel,
|
||||
isSegmentLongEnough,
|
||||
snapWallDraftPoint,
|
||||
snapWallDraftPointDetailed,
|
||||
@@ -9309,6 +9310,13 @@ export function FloorplanPanel({
|
||||
// well used to double-create walls whenever the two snap
|
||||
// pipelines resolved endpoints ≥1e-6 apart (the duplicate
|
||||
// check compares exact endpoints).
|
||||
//
|
||||
// That 3D path is dead in 2D-only view — the canvas is
|
||||
// `display:none`, so the tool never commits. Mirror the slab /
|
||||
// ceiling 2D-only committers: create locally here, gated on the
|
||||
// view, so split / 3D keep their single-owner tool commit.
|
||||
const createdWall =
|
||||
useEditor.getState().viewMode === '2d' ? createWallOnCurrentLevel(draftStart, point) : null
|
||||
|
||||
// Alt commits a single wall: drop the draft so the next click
|
||||
// starts a fresh segment instead of chaining off this endpoint.
|
||||
@@ -9319,10 +9327,13 @@ export function FloorplanPanel({
|
||||
return
|
||||
}
|
||||
|
||||
// Chain the next segment from the 3D tool's resolved commit
|
||||
// point (it may have corner-snapped or split-adjusted the
|
||||
// endpoint) so both views draft from the same start.
|
||||
const nextStart: WallPlanPoint = useSegmentDraftChain.getState().wall ?? point
|
||||
// Chain the next segment from the resolved commit endpoint (it may
|
||||
// have corner-snapped or split-adjusted): the wall we just made in
|
||||
// 2D-only, otherwise the 3D tool's published chain start. Both views
|
||||
// then draft from the same start.
|
||||
const nextStart: WallPlanPoint = createdWall
|
||||
? (createdWall.end as WallPlanPoint)
|
||||
: (useSegmentDraftChain.getState().wall ?? point)
|
||||
setDraftStart(nextStart)
|
||||
setDraftEnd(nextStart)
|
||||
setCursorPoint(nextStart)
|
||||
|
||||
Reference in New Issue
Block a user