floorplan/registry-layer: z-order buckets + overlay pass + click guard
Three layered improvements on the 2D registry layer:
- Z-order buckets so the SVG document order reflects intent: zones (0)
paint first, slabs/ceilings (1) next, every other kind (walls / items /
shelves / columns / stairs / …) on top. Stable sort preserves DFS order
within a bucket.
- Base / overlay split: each entry's `FloorplanGeometry` tree is walked
through `splitFloorplanOverlay`, partitioning into a base group
(polygons, paths, fills, hatches) and an overlay group (interactive
handles + labels — `text` / `endpoint-handle` / `midpoint-handle` /
`edge-handle` / `move-handle` / `dimension` / `dimension-label`). Base
renders rank-sorted; overlays paint after every base entry so polygon-
editor chrome on a selected slab and zone name labels stay legible
above the structural fills sitting on top of them.
- Click guard on the outer layer `<g>`. The base/overlay split means
pointer-down lands on base and pointer-up lands on overlay (selection
mounts the overlay on top mid-gesture). The browser then dispatches
`click` to the lowest common ancestor, ABOVE the entry's
`onClick={handleClickStop}`. Without a higher-level stop, the click
reached the SVG's `handleBackgroundClick` → `clear-elements` and the
selection set on pointer-down vanished a frame later. Scoped to
`onClick` only so pointer / hover / drag still propagate inside the
registry tree.
Also extends the `text` FloorplanGeometry with stroke / strokeWidth /
paintOrder / fontFamily so kinds can match the legacy "white fill +
colored outline" label look (used by the new zone name label).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
586cec8ad7
commit
7a92641baa
@@ -9,11 +9,13 @@ export type {
|
||||
EventSuffix,
|
||||
FenceEvent,
|
||||
GridEvent,
|
||||
GuideEvent,
|
||||
ItemEvent,
|
||||
LevelEvent,
|
||||
NodeEvent,
|
||||
RoofEvent,
|
||||
RoofSegmentEvent,
|
||||
ScanEvent,
|
||||
ShelfEvent,
|
||||
SiteEvent,
|
||||
SlabEvent,
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
export {
|
||||
discoverPlugins,
|
||||
getSelectableKinds,
|
||||
isRegistrySelectable,
|
||||
loadPlugin,
|
||||
nodeRegistry,
|
||||
type PluginDiscovery,
|
||||
registerNode,
|
||||
setPluginDiscovery,
|
||||
} from './registry'
|
||||
export {
|
||||
type CascadeContext,
|
||||
@@ -22,7 +25,14 @@ export type {
|
||||
CuttableConfig,
|
||||
DragAction,
|
||||
EditorCtx,
|
||||
FloorplanAffordance,
|
||||
FloorplanAffordanceModifiers,
|
||||
FloorplanAffordancePoint,
|
||||
FloorplanAffordanceSession,
|
||||
FloorplanGeometry,
|
||||
FloorplanMoveTarget,
|
||||
FloorplanMoveTargetSession,
|
||||
FloorplanPalette,
|
||||
FloorplanPoint,
|
||||
FloorplanStyle,
|
||||
GeometryContext,
|
||||
|
||||
@@ -95,3 +95,41 @@ export async function loadPlugin(plugin: Plugin): Promise<void> {
|
||||
registerNode(def)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* App-level plugin discovery hook. The bootstrap loads `builtinPlugin`
|
||||
* unconditionally and then awaits this to pick up any extra plugins
|
||||
* (third-party node packs, AI-authored bundles, user-installed kinds).
|
||||
* Defaults to returning `[]` — apps that want external plugins call
|
||||
* {@link setPluginDiscovery} before the bootstrap module runs.
|
||||
*
|
||||
* Kept async so a future loader can fetch over the network without
|
||||
* changing the contract. See `wiki/editor-plugin-authoring.md` for the
|
||||
* plugin author surface this enables.
|
||||
*/
|
||||
export type PluginDiscovery = () => Promise<Plugin[]>
|
||||
|
||||
let pluginDiscovery: PluginDiscovery = async () => []
|
||||
|
||||
/**
|
||||
* Replace the plugin discovery implementation. Call once at app startup
|
||||
* before {@link discoverPlugins} is invoked (bootstrap order matters).
|
||||
*
|
||||
* The contract is intentionally minimal — just "return a list of
|
||||
* plugins to load." The loader can be a static `import.meta.glob`, a
|
||||
* `fetch` against a registry endpoint, a worker IPC, etc. Each returned
|
||||
* plugin still goes through {@link loadPlugin} so the same API-version
|
||||
* gate + duplicate-kind protection applies.
|
||||
*/
|
||||
export function setPluginDiscovery(fn: PluginDiscovery): void {
|
||||
pluginDiscovery = fn
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the active plugin discovery and return the discovered plugins.
|
||||
* Bootstrap code is expected to call this after `loadPlugin(builtinPlugin)`
|
||||
* and then `await loadPlugin(...)` each result in order.
|
||||
*/
|
||||
export function discoverPlugins(): Promise<Plugin[]> {
|
||||
return pluginDiscovery()
|
||||
}
|
||||
|
||||
@@ -24,6 +24,90 @@ export type GeometryContext = {
|
||||
siblings: AnyNode[]
|
||||
/** Resolved parent (null for root-level nodes). */
|
||||
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.
|
||||
*
|
||||
* 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`).
|
||||
*/
|
||||
levelData?: unknown
|
||||
/**
|
||||
* Optional view state — only populated for `def.floorplan` builders. The
|
||||
* 2D floor-plan layer surfaces selection / hover here so kinds can vary
|
||||
* their output (themed stroke when selected, endpoint dots when
|
||||
* selected, hatch overlay, hover-side highlight). For `def.geometry`
|
||||
* (3D) this is always undefined — the 3D selection outline is handled
|
||||
* by the merged-outline post-process pass instead.
|
||||
*/
|
||||
viewState?: {
|
||||
selected: boolean
|
||||
/** Marquee or programmatic highlight — shows selected chrome without keyboard focus. */
|
||||
highlighted: boolean
|
||||
/** Pointer-hovered. */
|
||||
hovered: boolean
|
||||
/**
|
||||
* True while this node is the target of an active 2D move (i.e.
|
||||
* `useEditor.movingNode === node`). Used by kinds whose move
|
||||
* preview includes extra chrome — e.g. door / window emit
|
||||
* dimension lines showing the distance to adjacent openings or
|
||||
* wall ends only during the move.
|
||||
*/
|
||||
moving: boolean
|
||||
/**
|
||||
* The kind's theme palette. Theme-aware colors (selection stroke,
|
||||
* endpoint handle fill, hatch color) live here so kinds don't need
|
||||
* to import `useViewer.theme` themselves.
|
||||
*/
|
||||
palette: FloorplanPalette
|
||||
}
|
||||
}
|
||||
|
||||
// ─── FloorplanPalette ────────────────────────────────────────────────
|
||||
//
|
||||
// Centralised set of themed colors that kinds pull from when building
|
||||
// their floor-plan geometry. Mirrors the legacy `FloorplanPalette` in
|
||||
// `floorplan-panel.tsx`. The 2D layer constructs this from
|
||||
// `useViewer.theme` and passes it via `GeometryContext.viewState.palette`.
|
||||
|
||||
export type FloorplanPalette = {
|
||||
selectedStroke: string
|
||||
selectedFill: string
|
||||
/** Hatch / cross-stroke color used for selected fills with patterns. */
|
||||
selectedHatch: string
|
||||
/**
|
||||
* Stroke colour applied to a wall (and fence by analogy) when the
|
||||
* pointer hovers it. Light blue in the legacy palette — distinct from
|
||||
* the orange endpoint-handle hover so the body and its handles can
|
||||
* both glow independently. Pass through `viewState.palette.wall
|
||||
* HoverStroke` in `def.floorplan` when `viewState.hovered === true`
|
||||
* and the node isn't selected.
|
||||
*/
|
||||
wallHoverStroke: string
|
||||
endpointHandleFill: string
|
||||
endpointHandleStroke: string
|
||||
endpointHandleHoverStroke: string
|
||||
endpointHandleActiveFill: string
|
||||
endpointHandleActiveStroke: string
|
||||
/**
|
||||
* Curve sagitta handle slot — distinct teal colour-set the legacy
|
||||
* `FloorplanWallCurveLayer` uses so users can tell endpoint dots
|
||||
* (orange) and curve dots (teal) apart at a glance.
|
||||
*/
|
||||
curveHandleFill: string
|
||||
curveHandleStroke: string
|
||||
curveHandleHoverStroke: string
|
||||
measurementStroke: string
|
||||
measurementLabelBackground: string
|
||||
measurementLabelText: string
|
||||
}
|
||||
|
||||
// ─── FloorplanGeometry ───────────────────────────────────────────────
|
||||
@@ -45,6 +129,22 @@ export type FloorplanStyle = {
|
||||
strokeWidth?: number
|
||||
strokeDasharray?: string
|
||||
opacity?: number
|
||||
/**
|
||||
* When `'non-scaling-stroke'`, the SVG renderer interprets `strokeWidth`
|
||||
* as a constant screen-pixel width regardless of viewport zoom. Maps
|
||||
* straight to the SVG `vector-effect` attribute. Default (undefined)
|
||||
* treats `strokeWidth` as plan-unit metres.
|
||||
*
|
||||
* Kinds that emit hand-drawn-looking strokes (fence body, wall hairlines,
|
||||
* post markers) want non-scaling so the visual weight stays stable as
|
||||
* the user zooms. Kinds whose stroke represents a real-world thickness
|
||||
* (wall body in floor plan, slab outline) leave it undefined.
|
||||
*/
|
||||
vectorEffect?: 'non-scaling-stroke'
|
||||
strokeLinecap?: 'butt' | 'round' | 'square'
|
||||
strokeLinejoin?: 'miter' | 'round' | 'bevel'
|
||||
strokeOpacity?: number
|
||||
fillOpacity?: number
|
||||
}
|
||||
|
||||
// ─── ToolHint ────────────────────────────────────────────────────────
|
||||
@@ -85,12 +185,319 @@ export type FloorplanGeometry =
|
||||
x2: number
|
||||
y2: number
|
||||
} & FloorplanStyle)
|
||||
/**
|
||||
* Plain SVG text in plan space. Used for short labels that need to
|
||||
* sit at a specific plan coordinate — e.g. the elevator served-level
|
||||
* chips' floor numbers. Rotates with the floor plan's transform
|
||||
* (same as polygon coordinates) so it shares the building's
|
||||
* orientation. For text that needs to stay screen-upright regardless
|
||||
* of plan rotation, use `dimension-label` instead (it auto-flips
|
||||
* upside-down labels).
|
||||
*
|
||||
* `fontSize` is in plan metres — typical values are 0.1–0.2m. The
|
||||
* registry layer doesn't apply any text-rendering chrome (no plate,
|
||||
* no rotation auto-flip) — it's just a styled `<text>` element.
|
||||
*/
|
||||
| {
|
||||
kind: 'text'
|
||||
x: number
|
||||
y: number
|
||||
text: string
|
||||
fontSize: number
|
||||
fill?: string
|
||||
fontWeight?: number | string
|
||||
fontFamily?: string
|
||||
textAnchor?: 'start' | 'middle' | 'end'
|
||||
dominantBaseline?: 'auto' | 'middle' | 'central' | 'hanging' | 'alphabetic'
|
||||
opacity?: number
|
||||
/**
|
||||
* Outlined-text styling — when `stroke` is set the renderer applies
|
||||
* `stroke` / `strokeWidth` plus `paintOrder='stroke'` so the stroke
|
||||
* is drawn under the fill. Used by zone name labels for the
|
||||
* "white text inside a colored outline" look that stays legible
|
||||
* against any fill color.
|
||||
*/
|
||||
stroke?: string
|
||||
strokeWidth?: number
|
||||
paintOrder?: 'stroke' | 'fill' | 'normal'
|
||||
}
|
||||
/**
|
||||
* Bitmap overlay — captured top-down asset thumbnail, AI-generated
|
||||
* floor-plan symbol, scan slice, etc. `url` is passed through the
|
||||
* editor's `loadAssetUrl` resolver (handles CDN / Supabase storage),
|
||||
* so kinds emit the raw `asset.floorPlanUrl` and don't worry about
|
||||
* fetching.
|
||||
*
|
||||
* `rotation` is in radians around `center`. The image is drawn at
|
||||
* `center` with size `width × height` in plan-local metres;
|
||||
* `preserveAspectRatio` controls letterboxing (default
|
||||
* `'xMidYMid meet'`).
|
||||
*/
|
||||
| {
|
||||
kind: 'image'
|
||||
url: string
|
||||
center: FloorplanPoint
|
||||
width: number
|
||||
height: number
|
||||
rotation?: number
|
||||
preserveAspectRatio?: string
|
||||
opacity?: number
|
||||
}
|
||||
| {
|
||||
kind: 'group'
|
||||
children: FloorplanGeometry[]
|
||||
/** Optional transform applied to all children. Rotation in radians. */
|
||||
transform?: { translate?: FloorplanPoint; rotate?: number }
|
||||
}
|
||||
/**
|
||||
* Hatched fill overlay — same polygon shape as the kind's main fill but
|
||||
* stroked with diagonal lines on top. Used for the selected-wall hatch
|
||||
* effect from the legacy floor-plan panel. The 2D layer mounts a
|
||||
* shared `<pattern>` in `<defs>` and references it via `fill=url(...)`.
|
||||
*/
|
||||
| { kind: 'hatch'; points: readonly FloorplanPoint[]; color: string; opacity?: number }
|
||||
/**
|
||||
* Transparent click-detection segment. Sits on top of the kind's main
|
||||
* geometry with a wide stroke so the user doesn't need to pixel-hunt
|
||||
* the polygon. `select` is the only affordance for now — clicking
|
||||
* triggers selection of the owning node.
|
||||
*/
|
||||
| {
|
||||
kind: 'hit-line'
|
||||
x1: number
|
||||
y1: number
|
||||
x2: number
|
||||
y2: number
|
||||
/** Stroke width in screen pixels — converted to plan units by the dispatcher. */
|
||||
strokeWidthPx: number
|
||||
cursor?: string
|
||||
}
|
||||
/**
|
||||
* Endpoint manipulation handle — the 5-circle stack from the legacy
|
||||
* floor-plan: outer hover glow ring + hover ring + filled outer +
|
||||
* inner dot + transparent hit. Rendered with theme-aware colors from
|
||||
* `viewState.palette`. `affordance` keys into a kind-owned drag flow
|
||||
* the dispatcher invokes; `payload` is opaque kind data the
|
||||
* affordance handler unpacks.
|
||||
*/
|
||||
| {
|
||||
kind: 'endpoint-handle'
|
||||
point: FloorplanPoint
|
||||
/** `active` = currently being dragged; `idle` = visible but inert. */
|
||||
state: 'idle' | 'active'
|
||||
/**
|
||||
* Visual colour-set. `'endpoint'` (default) → orange — wall /
|
||||
* fence endpoints, polygon vertices. `'curve'` → teal — the
|
||||
* sagitta midpoint handle. Other values are reserved for future
|
||||
* affordances (rotation, scale) without expanding the union.
|
||||
*/
|
||||
variant?: 'endpoint' | 'curve'
|
||||
affordance: string
|
||||
payload: unknown
|
||||
}
|
||||
/**
|
||||
* Smaller "insert here" handle drawn between two polygon vertices.
|
||||
* Visually a small white dot with a `+` icon; hover-expanded. Triggers
|
||||
* an affordance that typically inserts a new vertex at the midpoint
|
||||
* and then drags it (matches the legacy slab / ceiling boundary
|
||||
* editor's edge-midpoint behaviour).
|
||||
*/
|
||||
| {
|
||||
kind: 'midpoint-handle'
|
||||
point: FloorplanPoint
|
||||
affordance: string
|
||||
payload: unknown
|
||||
}
|
||||
/**
|
||||
* Hit-target along an entire polygon edge. Renders as a transparent
|
||||
* wide stroke for click detection; the dispatcher overlays a glow +
|
||||
* solid stroke when hovered or actively being dragged. Used by the
|
||||
* slab / ceiling boundary editor's "drag whole edge perpendicular"
|
||||
* affordance — both endpoints translate together along the edge
|
||||
* normal.
|
||||
*/
|
||||
| {
|
||||
kind: 'edge-handle'
|
||||
x1: number
|
||||
y1: number
|
||||
x2: number
|
||||
y2: number
|
||||
affordance: string
|
||||
payload: unknown
|
||||
}
|
||||
/**
|
||||
* "Grab to move" handle drawn at a node's centroid — the orange dot
|
||||
* users click-and-drag to move a door / window / item in the
|
||||
* floorplan without going through the inspector's Move button.
|
||||
*
|
||||
* Pointer-down on the handle sets `useEditor.movingNode` to the
|
||||
* owning node, which `FloorplanRegistryMoveOverlay` picks up and
|
||||
* routes through the kind's `def.floorplanMoveTarget`. So both
|
||||
* entry points (Move button + dot grab) share the same move
|
||||
* pipeline — no parallel kind-side logic.
|
||||
*/
|
||||
| {
|
||||
kind: 'move-handle'
|
||||
point: FloorplanPoint
|
||||
}
|
||||
/**
|
||||
* Centered length / distance label. Renders as a small rounded
|
||||
* background plate with text, oriented along `angle` (radians). The
|
||||
* 2D layer flips the label upright when it would otherwise be upside
|
||||
* down. Use this for simple "what length am I?" badges (fence, item
|
||||
* width, draft preview).
|
||||
*/
|
||||
| {
|
||||
kind: 'dimension-label'
|
||||
cx: number
|
||||
cy: number
|
||||
text: string
|
||||
/** Rotation in radians. The renderer auto-flips to keep text upright. */
|
||||
angle: number
|
||||
}
|
||||
/**
|
||||
* Architect's dimension overlay — extension lines from the edge
|
||||
* endpoints out past the dimension line, two dimension line halves
|
||||
* with the label sitting in the gap, end ticks perpendicular to the
|
||||
* line. Used for the selected wall's full measurement; the rounded
|
||||
* plate label is the wrong shape when you want plan-drawing chrome.
|
||||
*
|
||||
* The renderer computes the segment geometry from these inputs so the
|
||||
* kind only needs to know "where is the edge and which way does the
|
||||
* dimension line offset." `offsetNormal` is a unit vector
|
||||
* perpendicular to the edge; pass the *outward* normal so the line
|
||||
* sits on the side facing away from the wall interior.
|
||||
*/
|
||||
| {
|
||||
kind: 'dimension'
|
||||
start: FloorplanPoint
|
||||
end: FloorplanPoint
|
||||
/** Outward-pointing unit normal — the dimension line offsets along this. */
|
||||
offsetNormal: FloorplanPoint
|
||||
/** Distance (plan units) from the edge to the dimension line. */
|
||||
offsetDistance: number
|
||||
/** How far past the offset point the extension line continues. */
|
||||
extensionOvershoot: number
|
||||
text: string
|
||||
/** Optional override for the line/text colour. Defaults to the palette accent. */
|
||||
stroke?: string
|
||||
}
|
||||
|
||||
// ─── FloorplanAffordance ─────────────────────────────────────────────
|
||||
//
|
||||
// 2D drag session contract for floor-plan interactions. The registry
|
||||
// layer (`FloorplanRegistryLayer`) drives the SVG event plumbing; each
|
||||
// affordance handler owns the actual mutation logic for its kind.
|
||||
//
|
||||
// Lifecycle:
|
||||
// 1. Pointer-down on a handle whose `affordance` key matches.
|
||||
// 2. Layer captures node snapshots for `affectedIds` and pauses
|
||||
// history.
|
||||
// 3. Layer calls `apply` on every pointer-move with the current plan
|
||||
// point + modifier keys.
|
||||
// 4. On pointer-up: layer reads the resulting scene state, reverts to
|
||||
// the snapshot (still paused, untracked), resumes history, then
|
||||
// re-applies the final state as a single tracked change (single-
|
||||
// undo dance — same shape as Stage D 3D moves).
|
||||
// 5. On pointer-cancel / unmount: revert + resume without committing.
|
||||
//
|
||||
// `apply` is expected to call `scene.updateNodes` directly to drive
|
||||
// previews — the layer doesn't keep a separate draft state.
|
||||
|
||||
export type FloorplanAffordancePoint = readonly [x: number, y: number]
|
||||
|
||||
export type FloorplanAffordanceModifiers = {
|
||||
shiftKey: boolean
|
||||
altKey: boolean
|
||||
ctrlKey: boolean
|
||||
metaKey: boolean
|
||||
}
|
||||
|
||||
export type FloorplanAffordanceSession = {
|
||||
/** Node IDs the drag may mutate. Used by the dispatcher for the snapshot. */
|
||||
affectedIds: AnyNodeId[]
|
||||
/**
|
||||
* Run a single drag tick. Implementations call `scene.updateNodes` to
|
||||
* preview the next position. Snap logic, linked-node cascade, and
|
||||
* angle locking live here.
|
||||
*/
|
||||
apply(args: {
|
||||
planPoint: FloorplanAffordancePoint
|
||||
modifiers: FloorplanAffordanceModifiers
|
||||
}): void
|
||||
/**
|
||||
* Called on pointer-up. Return `true` if the scene's current state
|
||||
* should be committed; `false` reverts to the snapshot (e.g. wall too
|
||||
* short, vertex collapsed onto neighbour).
|
||||
*/
|
||||
canCommit(): boolean
|
||||
}
|
||||
|
||||
export type FloorplanAffordance<N> = {
|
||||
start(args: {
|
||||
node: N
|
||||
/** Opaque kind-specific payload from the handle primitive. */
|
||||
payload: unknown
|
||||
/** Current scene snapshot at drag start. */
|
||||
nodes: Record<AnyNodeId, AnyNode>
|
||||
/** Initial pointer position in plan coordinates. */
|
||||
initialPlanPoint: FloorplanAffordancePoint
|
||||
}): FloorplanAffordanceSession
|
||||
}
|
||||
|
||||
// ─── FloorplanMoveTarget ─────────────────────────────────────────────
|
||||
//
|
||||
// Kind-specific 2D move-on-floorplan handler. Distinct from
|
||||
// `FloorplanAffordance` because the lifecycle is different:
|
||||
//
|
||||
// - `FloorplanAffordance` is **handle-driven** — the user pointer-downs
|
||||
// on a specific handle (endpoint dot, vertex, edge), drags, releases.
|
||||
// Has an `initialPlanPoint`. One drag = one session.
|
||||
// - `FloorplanMoveTarget` is **movingNode-driven** — the user clicks
|
||||
// "Move" in the inspector / action menu, the floor-plan tracks the
|
||||
// cursor from that moment until pointer-up or Esc. No initial
|
||||
// pointer-down. The session starts when `useEditor.movingNode` is
|
||||
// set to a node whose kind exposes `floorplanMoveTarget`.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// - door / window: pointer must hit a wall in plan space; commit
|
||||
// re-anchors to the new wall (parentId + wallId + local position +
|
||||
// side + rotation). Reuses `door-math` / `window-math` clamp +
|
||||
// overlap helpers.
|
||||
// - item with `attachTo: 'wall'` / `'wall-side'`: same as door /
|
||||
// window but the local Y is free (item can move up/down the wall).
|
||||
// - item with `attachTo: 'ceiling'`: hit-test ceiling polygons,
|
||||
// reparent on transition.
|
||||
// - item with `attachTo: 'floor'` (or no attachTo): point-in-slab
|
||||
// check, snap to slab elevation.
|
||||
//
|
||||
// Falls back to `FloorplanRegistryMoveOverlay`'s generic free-floating
|
||||
// translate when `floorplanMoveTarget` is unset on the kind.
|
||||
|
||||
export type FloorplanMoveTargetSession = {
|
||||
/** Node IDs the move may mutate. Used by the dispatcher for snapshot capture. */
|
||||
affectedIds: AnyNodeId[]
|
||||
/**
|
||||
* Single move-preview tick. Implementations call `scene.updateNodes`
|
||||
* directly to drive the live preview (no separate draft state).
|
||||
*/
|
||||
apply(args: {
|
||||
planPoint: FloorplanAffordancePoint
|
||||
modifiers: FloorplanAffordanceModifiers
|
||||
}): void
|
||||
/**
|
||||
* Called on pointer-up. Return `true` to commit the current scene
|
||||
* state; `false` reverts to the snapshot (e.g. dropped in invalid
|
||||
* area, overlap detected, ...).
|
||||
*/
|
||||
canCommit(): boolean
|
||||
}
|
||||
|
||||
export type FloorplanMoveTarget<N> = (args: {
|
||||
node: N
|
||||
nodes: Record<AnyNodeId, AnyNode>
|
||||
}) => FloorplanMoveTargetSession
|
||||
|
||||
// ─── Plugin manifest ─────────────────────────────────────────────────
|
||||
|
||||
@@ -142,6 +549,23 @@ export type NodeDefinition<S extends ZodObject<any>> = {
|
||||
* work (animations, named-mesh material poking).
|
||||
*/
|
||||
geometry?: (node: z.infer<S>, ctx: GeometryContext) => Object3D
|
||||
/**
|
||||
* Level-batch precompute hook. Called by `<GeometrySystem>` once per
|
||||
* level per frame, **before** the per-node `def.geometry` calls in
|
||||
* that batch. The result lands in `ctx.levelData` for every node in
|
||||
* the same level.
|
||||
*
|
||||
* Used by kinds whose geometry depends on cross-sibling data that
|
||||
* would be O(N²) to recompute per node:
|
||||
* - wall: `calculateLevelMiters(walls)` — every wall's mesh
|
||||
* reads its junctions from the level-wide miter graph.
|
||||
* - zone (planned): shared TSL gradient uniforms.
|
||||
*
|
||||
* `siblings` is every node of this kind in the same level (including
|
||||
* the dirty ones). The dispatcher de-duplicates per level so this
|
||||
* runs once even when many walls are dirty in the same frame.
|
||||
*/
|
||||
computeLevelData?: (siblings: ReadonlyArray<z.infer<S>>) => unknown
|
||||
/**
|
||||
* Pure 2D builder for floor-plan rendering. Mirrors `geometry` but emits
|
||||
* plain `FloorplanGeometry` data (SVG-renderable) rather than three.js
|
||||
@@ -157,6 +581,36 @@ export type NodeDefinition<S extends ZodObject<any>> = {
|
||||
* the legacy `floorplan-panel.tsx` monolith.
|
||||
*/
|
||||
floorplan?: (node: z.infer<S>, ctx: GeometryContext) => FloorplanGeometry | null
|
||||
/**
|
||||
* 2D drag affordances keyed by the string identifier emitted on
|
||||
* `endpoint-handle` (and similar interactive floor-plan primitives) via
|
||||
* the `affordance` field. The floor-plan registry layer calls
|
||||
* `def.floorplanAffordances?.[affordance].start({...})` on pointer-down,
|
||||
* receives a session, calls `apply(...)` on pointer-move and
|
||||
* `commit()` / `cancel()` on pointer-up / pointer-cancel. The session
|
||||
* mutates scene state directly during `apply`; the dispatcher handles
|
||||
* the snapshot + single-undo dance around it.
|
||||
*
|
||||
* Mirrors the existing 3D `affordanceTools` map but for 2D SVG events,
|
||||
* and operates on plain JS data instead of mounting React. Kinds with
|
||||
* both 3D and 2D affordances expose both fields — they're independent.
|
||||
*/
|
||||
floorplanAffordances?: Record<string, FloorplanAffordance<z.infer<S>>>
|
||||
/**
|
||||
* Kind-specific 2D move handler for `useEditor.movingNode`-driven
|
||||
* placement in the floor plan. When set, `FloorplanRegistryMove
|
||||
* Overlay` invokes this once when `movingNode` becomes a node of
|
||||
* this kind, and drives the session through pointer events until
|
||||
* pointer-up / Esc. Falls back to the generic free-floating
|
||||
* translate when unset.
|
||||
*
|
||||
* Use this for kinds whose move semantics are anchor-aware:
|
||||
* doors / windows need wall hits + reparenting; items with
|
||||
* `attachTo` need parent-surface hits. Kinds with simple
|
||||
* translate-on-XZ semantics (shelf, spawn, fence) leave this
|
||||
* unset and rely on the generic overlay path.
|
||||
*/
|
||||
floorplanMoveTarget?: FloorplanMoveTarget<z.infer<S>>
|
||||
system?: SystemContribution
|
||||
tool?: LazyComponent
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user