Improve editor manipulation flows

This commit is contained in:
Aymeric Rabot
2026-06-08 01:07:53 -04:00
parent ab271df9b6
commit 8dc602caa9
57 changed files with 3442 additions and 735 deletions
@@ -13,6 +13,7 @@ import {
collectAlignmentAnchors,
footprintAABB,
footprintAABBFrom,
movingAlignmentAnchors,
movingFootprintAnchors,
polygonAnchors,
wallSegmentAnchors,
@@ -196,6 +197,71 @@ describe('movingFootprintAnchors', () => {
})
})
describe('movingAlignmentAnchors', () => {
beforeEach(() => nodeRegistry._reset())
test('relocates a straight stair by its segment-chain footprint', () => {
registerNode(stairDef())
const nodes = {
st: node({
id: 'st',
type: 'stair',
position: [0, 0, 0],
rotation: 0,
stairType: 'straight',
width: 1,
children: ['seg'],
}),
seg: node({
id: 'seg',
type: 'stair-segment',
parentId: 'st',
width: 1,
length: 3,
height: 2.5,
attachmentSide: 'front',
}),
}
const anchors = movingAlignmentAnchors(nodes.st, nodes, 10, 20, 0)
expect(anchors).toHaveLength(4)
expect(new Set(anchors.map((a) => a.x))).toEqual(new Set([9.5, 10.5]))
expect(new Set(anchors.map((a) => a.z))).toEqual(new Set([20, 23]))
})
test('rotation override drives a moving straight stair footprint', () => {
registerNode(stairDef())
const nodes = {
st: node({
id: 'st',
type: 'stair',
position: [0, 0, 0],
rotation: 0,
stairType: 'straight',
width: 1,
children: ['seg'],
}),
seg: node({
id: 'seg',
type: 'stair-segment',
parentId: 'st',
width: 1,
length: 3,
height: 2.5,
attachmentSide: 'front',
}),
}
const anchors = movingAlignmentAnchors(nodes.st, nodes, 10, 20, Math.PI / 2)
const xs = anchors.map((a) => a.x)
const zs = anchors.map((a) => a.z)
expect(Math.min(...xs)).toBeCloseTo(10, 10)
expect(Math.max(...xs)).toBeCloseTo(13, 10)
expect(Math.min(...zs)).toBeCloseTo(19.5, 10)
expect(Math.max(...zs)).toBeCloseTo(20.5, 10)
})
})
describe('wallSegmentAnchors', () => {
test('returns both endpoints as corners and the chord midpoint as center', () => {
const anchors = wallSegmentAnchors('w', [0, 0], [4, 2])
@@ -152,6 +152,61 @@ export function movingFootprintAnchors(
return bboxCornerAnchors(node.id, aabb.minX, aabb.minZ, aabb.maxX, aabb.maxZ)
}
function relocatedPlanNode(node: AnyNode, x: number, z: number, rotationY?: number): AnyNode {
const position = (node as { position?: unknown }).position
const y = Array.isArray(position) && typeof position[1] === 'number' ? position[1] : 0
const relocated: Record<string, unknown> = {
...(node as Record<string, unknown>),
position: [x, y, z],
}
if (rotationY !== undefined && 'rotation' in node) {
const rotation = (node as { rotation?: unknown }).rotation
relocated.rotation = Array.isArray(rotation)
? [rotation[0] ?? 0, rotationY, rotation[2] ?? 0]
: rotationY
}
return relocated as AnyNode
}
/**
* Corner anchors for a moving node relocated to the proposed plan position.
* Covers both the centred-box path (`floorPlaced.footprint` /
* `alignmentFootprint: box`) and explicit AABB footprints such as stairs,
* whose occupied plan bounds depend on children or curved/spiral geometry.
*/
export function movingAlignmentAnchors(
node: AnyNode,
nodes: Readonly<Record<string, AnyNode>> | undefined,
x: number,
z: number,
rotationY?: number,
): AlignmentAnchor[] {
const box = footprintAABBAt(node, x, z, rotationY)
if (box) return bboxCornerAnchors(node.id, box.minX, box.minZ, box.maxX, box.maxZ)
const alignment = nodeRegistry
.get(node.type)
?.capabilities?.alignmentFootprint?.(relocatedPlanNode(node, x, z, rotationY), nodes)
if (alignment?.shape === 'box') {
const aabb = footprintAABBFrom([x, 0, z], alignment.dimensions, alignment.rotation[1] ?? 0)
return bboxCornerAnchors(node.id, aabb.minX, aabb.minZ, aabb.maxX, aabb.maxZ)
}
if (alignment?.shape === 'aabb') {
return bboxCornerAnchors(
node.id,
alignment.minX,
alignment.minZ,
alignment.maxX,
alignment.maxZ,
)
}
return []
}
/**
* Alignment anchors for a wall segment: the two centerline endpoints + chord
* midpoint, plus — when `thickness` is known — four **face** corner anchors,
+1
View File
@@ -15,6 +15,7 @@ export {
footprintAABB,
footprintAABBAt,
footprintAABBFrom,
movingAlignmentAnchors,
movingFootprintAnchors,
nodeAlignmentAnchors,
polygonAnchors,