editor: level-scoped alignment, reference-floor symbols & registry slab tool (#373)
* feat(editor): level-scoped alignment, reference-floor registry symbols, registry slab tool - Scope alignment candidates to the active level so a node directly below on another floor no longer snaps (alignment is XZ-only); building-scoped nodes like elevator shafts stay in the pool across floors. - Derive the elevator alignment footprint from its outer shaft (not the inset cab), so its guide actually surfaces within the snap threshold. - Extract shared stair footprint geometry (rotateXZ, segment transforms, stairFootprintAABB) so opening-sync and alignment anchors derive the chain identically; stairs now contribute plan bbox anchors. - Render reference-floor stairs/roofs/elevators/shelves/spawns through their registry floorplan builders for pixel-identical symbols. - Move slab creation to the registry-driven slab tool (parity with ceiling); 2D handlers only maintain draft state. - Lift the 3D alignment guide ribbon to the active level's Y each frame. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(core): registry-drive elevator/stair alignment footprints Address architecture review of the level-scoped alignment work: the core anchor bridge (`alignment-anchors.ts`) hardcoded `if (node.type === 'elevator')` and `if (node.type === 'stair')` branches to derive their plan footprints. Move that onto the kinds themselves via a new `alignmentFootprint` capability so the bridge dispatches generically — matching the registry composition model. - Add `Capabilities.alignmentFootprint`: returns a `box` (rotatable rect centred on position, relocatable for movable kinds) or an `aabb` (already resolved, for non-rectangular plan shapes). Elevator uses `box` (its outer shaft, and it's movable); stair uses `aabb` (segment chain / annular sector, moves by origin). - Drop both hardcoded branches; the bridge now consults the capability via `floorFootprint` (box) and a unified `alignmentAABB` (box ∪ aabb). - Export `stairFootprintAABB` from core so the stair definition consumes it. - Tests register synthetic defs carrying the capability (the bridge no longer knows elevator/stair by name), reproducing the production glue from the same core helpers. - Document why `REFERENCE_REGISTRY_KINDS` is a deliberate editor-local curation, not an auto-derived set (most floorplan-builder kinds shouldn't appear as standalone reference symbols, and "reference floor" is an editor concept core must not know). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(editor): hide node arrow handles during thumbnail capture The arrow-handle rig lives on SCENE_LAYER (so its chevrons read as proper 3D plates), but the thumbnail camera only filters EDITOR_LAYER + GRID_LAYER — so a node selected at capture time would leak its arrows into the snapshot. Hide the rig on `thumbnail:before-capture` and restore it on `thumbnail:after-capture`, the same emitter handshake SelectionManager uses. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- 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
46f94b97b3
commit
d1b40aa98d
@@ -166,6 +166,21 @@ export const elevatorDefinition: NodeDefinition<typeof ElevatorNode> = {
|
||||
// 2D body-move flow through `FloorplanRegistryMoveOverlay`'s
|
||||
// Path 2 — position[0] / position[2] update with a 0.5m grid snap.
|
||||
movable: { axes: ['x', 'z'], gridSnap: true },
|
||||
// Align by the OUTER SHAFT (shaft + wall — what's drawn in plan and 3D),
|
||||
// not the cab `width × depth`: the cab is inset by the shaft wall +
|
||||
// clearance, so cab corners sit ~9 cm inside the visible edge — past the
|
||||
// 8 cm snap, which is why the elevator never surfaced a guide. A `box`
|
||||
// shape (not `aabb`) because the elevator is `movable`, so the anchor
|
||||
// bridge relocates this same footprint to the drag point.
|
||||
alignmentFootprint: (node) => {
|
||||
const e = node as ElevatorNodeType
|
||||
const wall = getElevatorShaftWallThickness(e)
|
||||
return {
|
||||
shape: 'box',
|
||||
dimensions: [getElevatorShaftWidth(e) + wall * 2, 1, getElevatorShaftDepth(e) + wall * 2],
|
||||
rotation: [0, e.rotation ?? 0, 0],
|
||||
}
|
||||
},
|
||||
duplicable: true,
|
||||
deletable: true,
|
||||
},
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
type NodeDefinition,
|
||||
StairNode as StairNodeSchema,
|
||||
type StairNode as StairNodeType,
|
||||
stairFootprintAABB,
|
||||
} from '@pascal-app/core'
|
||||
|
||||
const MIN_CURVED_RISE = 0.3
|
||||
@@ -318,6 +319,15 @@ export const stairDefinition: NodeDefinition<typeof StairNode> = {
|
||||
|
||||
capabilities: {
|
||||
selectable: { hitVolume: 'bbox' },
|
||||
// A stair has no centred box footprint: straight = a cumulative
|
||||
// `stair-segment` chain, curved / spiral = an annular sector. Hand the
|
||||
// alignment bridge the resolved plan `aabb` directly (not a `box`) — the
|
||||
// stair moves by its origin via `affordanceTools.move`, so it only ever
|
||||
// contributes static candidate anchors, never the relocatable box path.
|
||||
alignmentFootprint: (node, nodes) => {
|
||||
const aabb = stairFootprintAABB(node as StairNodeType, nodes)
|
||||
return aabb ? { shape: 'aabb', ...aabb } : null
|
||||
},
|
||||
duplicable: true,
|
||||
deletable: true,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user