feat(editor): placement & interaction overhaul — FSM spine, bug tracks, perf
Implements plans/editor-placement-interaction-overhaul.md: an authoritative interaction-scope state machine plus the catalogued placement/interaction fixes, and split-view floor-plan performance. - Interaction-scope spine (lib/interaction/* + store/use-interaction-scope), driven from central useEditor setters; overlay scoping (zone labels, context badges, floating action menu) reads resolveOverlayPolicy. - Bug tracks A/B/D/E/F/G/H: handle/cutout raycast, footprint validity, auto-slab loop, ceiling hosting, B-key tool desync, 2D drop offset, per-frame jank. - Snapping modes (grid/lines/angles/off) + contextual HUD chips; modifier model (Shift=cycle, Alt=free place, Ctrl=grid step). - Item move now tracks the cursor 1:1 (was a laggy per-frame lerp); handle rig hides during a whole-node move; rotate gizmo advertises Shift=free rotation in the HUD and hides the move cross while rotating. - Floor-plan perf: pause live reactivity while in 3D-only view; per-node geometry cache so only changed nodes rebuild on a drag; hoist wall miters to a once-per-pass ctx.levelData (O(N^2) -> O(N) on wall/opening drags). 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
b2f1a8432e
commit
f773e6b8c5
@@ -15,9 +15,8 @@ import type { CloneNodesIntoOptions, Subtree } from './subtree'
|
||||
// door cutouts read parent wall — use `ctx` to resolve those references
|
||||
// without importing `useScene`. Builders stay pure and unit-testable.
|
||||
//
|
||||
// Future extension: `levelData?: { miters?: ... }` for level-scoped batch
|
||||
// data (wall mitering across an entire level). Decided alongside the wall
|
||||
// migration off its dedicated system (Phase 3+).
|
||||
// `levelData` carries level-scoped batch data (wall mitering across an
|
||||
// entire level) from registry dispatchers into pure builders.
|
||||
|
||||
export type GeometryContext = {
|
||||
/** Look up any node by ID. Returns undefined if the node doesn't exist. */
|
||||
@@ -30,18 +29,16 @@ export type GeometryContext = {
|
||||
parent: AnyNode | null
|
||||
/**
|
||||
* Pre-computed level-batch data, populated by the dispatcher when the
|
||||
* kind declares `def.computeLevelData`. Shared across every
|
||||
* `def.geometry(node, ctx)` call in the same level batch within a
|
||||
* single frame, so kinds whose geometry depends on cross-sibling
|
||||
* data (wall mitering, gradient sky uniforms across a zone, etc.)
|
||||
* don't pay an O(N²) recomputation cost.
|
||||
* kind declares `def.computeLevelData` (3D) or
|
||||
* `def.computeFloorplanLevelData` (2D). Shared across every builder call
|
||||
* in the same level batch within a single frame/render pass, so kinds
|
||||
* whose geometry depends on cross-sibling data (wall mitering, gradient
|
||||
* sky uniforms across a zone, etc.) don't pay an O(N²) recomputation cost.
|
||||
*
|
||||
* Typed as `unknown` at the framework boundary — kinds cast to their
|
||||
* own `LevelData` shape inside `def.geometry` (the same kind owns
|
||||
* both the `computeLevelData` return shape and the `geometry`
|
||||
* consumer, so the cast is internal). Only populated for `def.
|
||||
* geometry` calls today; not used by `def.floorplan` (which already
|
||||
* has cheap access to siblings through `ctx.siblings`).
|
||||
* own `LevelData` shape inside `def.geometry` / `def.floorplan` (the
|
||||
* same kind owns both the compute hook's return shape and the builder
|
||||
* consumer, so the cast is internal).
|
||||
*/
|
||||
levelData?: unknown
|
||||
/**
|
||||
@@ -820,6 +817,21 @@ export type NodeDefinition<S extends ZodObject<any>> = {
|
||||
* runs once even when many walls are dirty in the same frame.
|
||||
*/
|
||||
computeLevelData?: (siblings: ReadonlyArray<z.infer<S>>) => unknown
|
||||
/**
|
||||
* Floor-plan level-batch precompute hook. The floor-plan layer calls this
|
||||
* once per level per render pass, de-duplicated by kind, before the
|
||||
* per-node `def.floorplan` calls. The result lands in `ctx.levelData` for
|
||||
* every node of this kind in the level.
|
||||
*
|
||||
* Used to hoist cross-sibling floor-plan work that would otherwise be
|
||||
* O(N²) when rebuilding every node in a kind — e.g. wall mitering. `nodes`
|
||||
* is the live-merged scene snapshot; `siblings` is every node of this kind
|
||||
* in the level, also live-merged.
|
||||
*/
|
||||
computeFloorplanLevelData?: (args: {
|
||||
siblings: ReadonlyArray<z.infer<S>>
|
||||
nodes: Record<string, AnyNode>
|
||||
}) => unknown
|
||||
/**
|
||||
* Pure 2D builder for floor-plan rendering. Mirrors `geometry` but emits
|
||||
* plain `FloorplanGeometry` data (SVG-renderable) rather than three.js
|
||||
@@ -877,6 +889,12 @@ export type NodeDefinition<S extends ZodObject<any>> = {
|
||||
* unset and rely on the generic overlay path.
|
||||
*/
|
||||
floorplanMoveTarget?: FloorplanMoveTarget<z.infer<S>>
|
||||
/**
|
||||
* Geometry reads sibling/parent/child nodes (e.g. wall miters, opening
|
||||
* dimensions); the floor-plan layer must rebuild it whenever a
|
||||
* sibling-affecting node is being dragged live.
|
||||
*/
|
||||
floorplanDependsOnSiblings?: boolean
|
||||
/**
|
||||
* Optional hook letting a kind project the `useLiveNodeOverrides` map
|
||||
* into a fresh `nodes` snapshot before its `def.floorplan` builder
|
||||
|
||||
Reference in New Issue
Block a user