Phase 5 Stage C continued: wall, door, window now in registry floor plan
Three remaining kinds at Stage C this session: wall → C - buildWallFloorplan: uses ctx.siblings to gather other walls in the level, runs calculateLevelMiters, computes plan footprint via getWallPlanFootprint. Same visual output as legacy. - getFloorplanWall thickness exaggeration inlined (~25 lines from editor/lib/floorplan/walls.ts) to keep nodes/wall self-contained. - floorplan-panel.tsx's wallPolygons short-circuits to [] when wall is registered. - Performance note: recomputes level miter data per wall (O(N²) for N walls in a level). Acceptable for typical scenes; ctx.levelData?. miters optimization deferred to Stage B's wall design pass. door → C - buildDoorFloorplan: inlines getOpeningFootprint math from floorplan-panel.tsx (40 lines, pure math). Uses ctx.parent as the wall to compute direction + perpendicular for the cutout footprint. - Returns null when parent isn't a wall (orphaned doors during placement). window → C - buildWindowFloorplan: same shape as door, glass-blue tint to distinguish visually. Both share the legacy openingsPolygons gating: - floorplan-panel.tsx's openingsPolygons useMemo filters per kind so a partial migration still works (e.g., if only door registers, only doors get skipped). When both registered, returns [] entirely. Item C intentionally deferred — needs parent-chain transform helpers (buildFloorplanItemEntry / getItemFloorplanTransform from editor/lib/ floorplan/items.ts) exposed publicly or moved into core. A focused session is the right place to design that boundary. Stage B for door / window / wall still pending — each is a focused session per kind (large geometry math extractions, wall needs ctx. levelData design). 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
969b154b08
commit
9bcb25d0aa
@@ -8486,9 +8486,12 @@ export function FloorplanPanel() {
|
|||||||
return [{ fence: displayFence, centerline, markerFrames, path }]
|
return [{ fence: displayFence, centerline, markerFrames, path }]
|
||||||
})
|
})
|
||||||
}, [fences, movingFloorplanNodeRevision])
|
}, [fences, movingFloorplanNodeRevision])
|
||||||
const wallPolygons = useMemo(
|
const wallPolygons = useMemo(() => {
|
||||||
() =>
|
// Wall migrated to def.floorplan (Phase 5 Stage C). When registered,
|
||||||
walls.map((wall) => {
|
// FloorplanRegistryLayer renders the mitered wall polygon; this
|
||||||
|
// legacy path short-circuits. Removed entirely in Phase 6 cleanup.
|
||||||
|
if (nodeRegistry.has('wall')) return []
|
||||||
|
return walls.map((wall) => {
|
||||||
const floorplanWall = floorplanWallById.get(wall.id) ?? getFloorplanWall(wall)
|
const floorplanWall = floorplanWallById.get(wall.id) ?? getFloorplanWall(wall)
|
||||||
const polygon = getWallPlanFootprint(floorplanWall, wallMiterData)
|
const polygon = getWallPlanFootprint(floorplanWall, wallMiterData)
|
||||||
return {
|
return {
|
||||||
@@ -8496,9 +8499,8 @@ export function FloorplanPanel() {
|
|||||||
wall,
|
wall,
|
||||||
polygon,
|
polygon,
|
||||||
}
|
}
|
||||||
}),
|
})
|
||||||
[floorplanWallById, wallMiterData, walls],
|
}, [floorplanWallById, wallMiterData, walls])
|
||||||
)
|
|
||||||
const displayWallPolygons = useMemo(() => {
|
const displayWallPolygons = useMemo(() => {
|
||||||
if (!(wallEndpointDraft || wallCurveDraft)) {
|
if (!(wallEndpointDraft || wallCurveDraft)) {
|
||||||
return wallPolygons
|
return wallPolygons
|
||||||
@@ -8547,9 +8549,18 @@ export function FloorplanPanel() {
|
|||||||
)
|
)
|
||||||
}, [displayWallById, wallCurveDraft, wallEndpointDraft, wallPolygons])
|
}, [displayWallById, wallCurveDraft, wallEndpointDraft, wallPolygons])
|
||||||
|
|
||||||
const openingsPolygons = useMemo(
|
const openingsPolygons = useMemo(() => {
|
||||||
() =>
|
// Doors + windows migrated to def.floorplan (Phase 5 Stage C). When
|
||||||
openings.flatMap((opening) => {
|
// both registered, FloorplanRegistryLayer renders each opening's
|
||||||
|
// polygon via its kind's builder; this legacy path short-circuits
|
||||||
|
// to avoid double-render. Filter per kind so a partial migration
|
||||||
|
// would still work.
|
||||||
|
const doorRegistered = nodeRegistry.has('door')
|
||||||
|
const windowRegistered = nodeRegistry.has('window')
|
||||||
|
if (doorRegistered && windowRegistered) return []
|
||||||
|
return openings.flatMap((opening) => {
|
||||||
|
if (doorRegistered && opening.type === 'door') return []
|
||||||
|
if (windowRegistered && opening.type === 'window') return []
|
||||||
const wall = displayFloorplanWallById.get(opening.parentId as WallNode['id'])
|
const wall = displayFloorplanWallById.get(opening.parentId as WallNode['id'])
|
||||||
if (!wall) return []
|
if (!wall) return []
|
||||||
const live = useLiveTransforms.getState().get(opening.id)
|
const live = useLiveTransforms.getState().get(opening.id)
|
||||||
@@ -8579,9 +8590,8 @@ export function FloorplanPanel() {
|
|||||||
polygon,
|
polygon,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
}),
|
})
|
||||||
[displayFloorplanWallById, movingFloorplanNodeRevision, movingNode, openings],
|
}, [displayFloorplanWallById, movingFloorplanNodeRevision, movingNode, openings])
|
||||||
)
|
|
||||||
const slabPolygons = useMemo(() => {
|
const slabPolygons = useMemo(() => {
|
||||||
// Slab migrated to def.floorplan (Phase 5 Stage C). When registered,
|
// Slab migrated to def.floorplan (Phase 5 Stage C). When registered,
|
||||||
// FloorplanRegistryLayer renders the slab polygon; this legacy
|
// FloorplanRegistryLayer renders the slab polygon; this legacy
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { NodeDefinition } from '@pascal-app/core'
|
import type { NodeDefinition } from '@pascal-app/core'
|
||||||
|
import { buildDoorFloorplan } from './floorplan'
|
||||||
import { doorParametrics } from './parametrics'
|
import { doorParametrics } from './parametrics'
|
||||||
import { DoorNode } from './schema'
|
import { DoorNode } from './schema'
|
||||||
|
|
||||||
@@ -12,16 +13,14 @@ import { DoorNode } from './schema'
|
|||||||
* keeps legacy `MoveDoorTool`.
|
* keeps legacy `MoveDoorTool`.
|
||||||
* - `selectable`, `duplicable`, `deletable` standard.
|
* - `selectable`, `duplicable`, `deletable` standard.
|
||||||
*
|
*
|
||||||
* Relations:
|
* Stages:
|
||||||
* - `parentId` references a wall — re-anchors on wall move (handled by
|
* - A: registered.
|
||||||
* `DoorSystem`'s cascade to parent wall).
|
* - B: deferred — door geometry (frame / leaf / glass / hardware /
|
||||||
* - `cascadeDelete: 'children'` — door has no children in v1.
|
* segments) is ~800 lines in DoorSystem; extraction is a focused
|
||||||
*
|
* session. `def.renderer` (wrap-export of legacy DoorRenderer) +
|
||||||
* Renderer + system: wrap-export legacy `DoorRenderer` + bundle
|
* `def.system` (DoorSystem + DoorAnimationSystem bundle) hold parity.
|
||||||
* `DoorSystem` + `DoorAnimationSystem`.
|
* - C: `def.floorplan` polygon sits in parent wall's cutout. Legacy
|
||||||
*
|
* `openingPolygons` short-circuits door entries when registered.
|
||||||
* Tool field absent: door placement / move tools wired through editor
|
|
||||||
* state, not registry dispatch. Legacy DoorTool / MoveDoorTool continue.
|
|
||||||
*/
|
*/
|
||||||
export const doorDefinition: NodeDefinition<typeof DoorNode> = {
|
export const doorDefinition: NodeDefinition<typeof DoorNode> = {
|
||||||
kind: 'door',
|
kind: 'door',
|
||||||
@@ -56,6 +55,9 @@ export const doorDefinition: NodeDefinition<typeof DoorNode> = {
|
|||||||
// before wall mitering at 4).
|
// before wall mitering at 4).
|
||||||
priority: 3,
|
priority: 3,
|
||||||
},
|
},
|
||||||
|
// Stage C: floor-plan polygon. Needs ctx.parent (the wall) to compute
|
||||||
|
// direction + perpendicular for the cutout footprint.
|
||||||
|
floorplan: buildDoorFloorplan,
|
||||||
|
|
||||||
toolHints: [
|
toolHints: [
|
||||||
{ key: 'Left click', label: 'Place door on wall' },
|
{ key: 'Left click', label: 'Place door on wall' },
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import type {
|
||||||
|
DoorNode,
|
||||||
|
FloorplanGeometry,
|
||||||
|
FloorplanPoint,
|
||||||
|
GeometryContext,
|
||||||
|
WallNode,
|
||||||
|
} from '@pascal-app/core'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stage C floor-plan builder for door. Doors render as a small polygon
|
||||||
|
* sitting in the wall's cutout — width = door.width along the wall
|
||||||
|
* direction, depth = wall.thickness perpendicular.
|
||||||
|
*
|
||||||
|
* Requires `ctx.parent` to be a wall (door.parentId is the wall it's
|
||||||
|
* mounted on). Returns null when the parent isn't a wall (orphaned
|
||||||
|
* doors during placement etc.).
|
||||||
|
*
|
||||||
|
* Inlined from the legacy `getOpeningFootprint` helper in
|
||||||
|
* floorplan-panel.tsx. Window's builder is structurally identical.
|
||||||
|
*/
|
||||||
|
export function buildDoorFloorplan(node: DoorNode, ctx: GeometryContext): FloorplanGeometry | null {
|
||||||
|
const wall = ctx.parent as WallNode | null
|
||||||
|
if (!wall || wall.type !== 'wall') return null
|
||||||
|
|
||||||
|
const [x1, z1] = wall.start
|
||||||
|
const [x2, z2] = wall.end
|
||||||
|
const dx = x2 - x1
|
||||||
|
const dz = z2 - z1
|
||||||
|
const length = Math.sqrt(dx * dx + dz * dz)
|
||||||
|
if (length < 1e-9) return null
|
||||||
|
|
||||||
|
const dirX = dx / length
|
||||||
|
const dirZ = dz / length
|
||||||
|
const perpX = -dirZ
|
||||||
|
const perpZ = dirX
|
||||||
|
|
||||||
|
const distance = node.position[0] // door's local X = distance along wall
|
||||||
|
const width = node.width
|
||||||
|
const depth = wall.thickness ?? 0.1
|
||||||
|
const cx = x1 + dirX * distance
|
||||||
|
const cz = z1 + dirZ * distance
|
||||||
|
const halfWidth = width / 2
|
||||||
|
const halfDepth = depth / 2
|
||||||
|
|
||||||
|
const points: readonly FloorplanPoint[] = [
|
||||||
|
[cx - dirX * halfWidth + perpX * halfDepth, cz - dirZ * halfWidth + perpZ * halfDepth],
|
||||||
|
[cx + dirX * halfWidth + perpX * halfDepth, cz + dirZ * halfWidth + perpZ * halfDepth],
|
||||||
|
[cx + dirX * halfWidth - perpX * halfDepth, cz + dirZ * halfWidth - perpZ * halfDepth],
|
||||||
|
[cx - dirX * halfWidth - perpX * halfDepth, cz - dirZ * halfWidth - perpZ * halfDepth],
|
||||||
|
]
|
||||||
|
|
||||||
|
return {
|
||||||
|
kind: 'polygon',
|
||||||
|
points,
|
||||||
|
fill: '#f8fafc',
|
||||||
|
stroke: '#374151',
|
||||||
|
strokeWidth: 0.015,
|
||||||
|
opacity: 0.95,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,34 +1,21 @@
|
|||||||
import type { NodeDefinition } from '@pascal-app/core'
|
import type { NodeDefinition } from '@pascal-app/core'
|
||||||
|
import { buildWallFloorplan } from './floorplan'
|
||||||
import { wallParametrics } from './parametrics'
|
import { wallParametrics } from './parametrics'
|
||||||
import { WallNode } from './schema'
|
import { WallNode } from './schema'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wall — the Phase 3 stress test of the registry-driven node model.
|
* Wall — the Phase 3 stress test of the registry-driven node model.
|
||||||
*
|
*
|
||||||
* What this definition encodes today:
|
* Stage A: registered (capabilities, relations, parametrics, presentation).
|
||||||
* - **Capabilities**: cuttable (doors/windows punch holes), snappable
|
* Stage B: deferred — wall geometry depends on level-batch miter data that
|
||||||
* (other walls, doors, windows snap to wall geometry), surfaces (front
|
* doesn't fit the generic `(node, ctx) => Group` shape without `ctx.
|
||||||
* + back faces host items), selectable, duplicable, deletable.
|
* levelData?.miters`. See plan's "GeometryContext" extension note.
|
||||||
* - **Relations**: hosts doors/windows/items; affects spatial slabs +
|
* `renderer` + `system` keep wrap-exporting legacy WallRenderer +
|
||||||
* ceilings + zones when moved; descendants cascade-delete; linked walls
|
* WallSystem + WallCutout.
|
||||||
* follow corners via endpoint-match (consumed by the affordances in a
|
* Stage C: `def.floorplan` builder produces the mitered plan footprint
|
||||||
* later milestone — the relations resolver already understands the
|
* polygon using `ctx.siblings` to assemble miter context.
|
||||||
* declaration).
|
* floorplan-panel.tsx's `wallPolygons` short-circuits to [] when
|
||||||
* - **Parametrics**: thickness / height / curveOffset for the inspector.
|
* wall is registered.
|
||||||
*
|
|
||||||
* What this definition does *not* yet encode:
|
|
||||||
* - `geometry` / `renderer` / `system` runtime — the existing
|
|
||||||
* `wall-renderer.tsx` + `wall-system.tsx` keep serving wall until
|
|
||||||
* Milestone B ports them into this folder. Until then, this definition
|
|
||||||
* is metadata-only and *intentionally not registered* in
|
|
||||||
* `builtinPlugin.nodes` — the Phase 0 shims only flip behavior when a
|
|
||||||
* kind is registered, so wall stays on its legacy path.
|
|
||||||
* - `tool` — wall's placement + endpoint drag + curve drag tools port in
|
|
||||||
* a follow-up milestone, expressed via the `DragAction` primitive so
|
|
||||||
* the affordances declared in `relations` get real handles.
|
|
||||||
*
|
|
||||||
* Migration is gated by `feature-flag.ts` (env: `NEXT_PUBLIC_USE_REGISTRY_FOR_WALL`).
|
|
||||||
* See `plans/editor-node-registry.md#phase-3` for the milestone breakdown.
|
|
||||||
*/
|
*/
|
||||||
export const wallDefinition: NodeDefinition<typeof WallNode> = {
|
export const wallDefinition: NodeDefinition<typeof WallNode> = {
|
||||||
kind: 'wall',
|
kind: 'wall',
|
||||||
@@ -49,18 +36,12 @@ export const wallDefinition: NodeDefinition<typeof WallNode> = {
|
|||||||
}),
|
}),
|
||||||
|
|
||||||
capabilities: {
|
capabilities: {
|
||||||
// Wall move is bespoke today (endpoint drag, linked-wall corner cascade,
|
// Wall move is bespoke (endpoint drag, linked-wall corner cascade,
|
||||||
// ALT-detach). `MoveRegistryNodeTool`'s "translate on X/Z plane" shape
|
// ALT-detach). Omitting `movable` keeps the legacy MoveWallTool via
|
||||||
// doesn't apply — wall stays on its own move tool until the affordance
|
// capability-driven dispatch.
|
||||||
// port. Leaving `movable` omitted keeps that dispatch.
|
|
||||||
selectable: { hitVolume: 'bbox' },
|
selectable: { hitVolume: 'bbox' },
|
||||||
// Front + back faces host items (paintings, shelves, switches).
|
// Front + back faces host items (paintings, shelves, switches).
|
||||||
// `height` callback resolves per-instance so taller walls expose taller
|
|
||||||
// hosting surface — same shape used by shelf.top.
|
|
||||||
surfaces: {
|
surfaces: {
|
||||||
// Sides config — wall has two faces; concrete face-selection logic
|
|
||||||
// stays in the renderer/system for now. Phase 4 will derive snap
|
|
||||||
// targets from this.
|
|
||||||
sides: { faces: 'all' },
|
sides: { faces: 'all' },
|
||||||
},
|
},
|
||||||
duplicable: true,
|
duplicable: true,
|
||||||
@@ -68,50 +49,26 @@ export const wallDefinition: NodeDefinition<typeof WallNode> = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
relations: {
|
relations: {
|
||||||
// Doors / windows / items mount on walls. The host-resolver consumes
|
|
||||||
// this to validate `parentId` on creation and to re-anchor children
|
|
||||||
// when a wall moves (a milestone-B concern; the declaration lives here
|
|
||||||
// so the wiring exists ahead of time).
|
|
||||||
hosts: ['door', 'window', 'item'],
|
hosts: ['door', 'window', 'item'],
|
||||||
// Moving a wall dirties the slabs / ceilings / zones that border it.
|
|
||||||
// Today this is *not* wired (the existing `wall-system` doesn't cascade
|
|
||||||
// to slab / zone) — the registry resolver gains this behavior for free
|
|
||||||
// once wall is registered. This is the "slab reflow on wall move"
|
|
||||||
// behavior gain called out in Phase 3 acceptance.
|
|
||||||
affectsSpatial: ['slab', 'ceiling', 'zone'],
|
affectsSpatial: ['slab', 'ceiling', 'zone'],
|
||||||
// Walls sharing an endpoint move together when a corner is dragged.
|
|
||||||
// The endpoint affordance (milestone C) uses this declaration via the
|
|
||||||
// shared cascade resolver — no hand-rolled `getLinkedWallSnapshots`.
|
|
||||||
linkedBy: 'endpoint-match',
|
linkedBy: 'endpoint-match',
|
||||||
// Deleting a wall deletes its hosted doors/windows/items (today's
|
|
||||||
// implicit behavior, now declarative).
|
|
||||||
cascadeDelete: 'descendants',
|
cascadeDelete: 'descendants',
|
||||||
},
|
},
|
||||||
|
|
||||||
parametrics: wallParametrics,
|
parametrics: wallParametrics,
|
||||||
|
|
||||||
// Wall's renderer is the thin placeholder-mesh mount point from milestone
|
|
||||||
// B; the system bundle composes the legacy `WallSystem` + `WallCutout`
|
|
||||||
// re-exported from viewer (so we don't duplicate ~970 lines of CSG /
|
|
||||||
// mitering / cutaway logic just to swap the dispatch). The legacy
|
|
||||||
// mount in `<LegacySystem kind="wall">` short-circuits the moment
|
|
||||||
// `nodeRegistry.has('wall')` is true.
|
|
||||||
//
|
|
||||||
// No `tool` yet — wall placement / endpoint drag / curve drag remain
|
|
||||||
// bespoke until the affordance port lands. Phase 0 shims keep the legacy
|
|
||||||
// wall tool running while wall is registered (it's not wired through the
|
|
||||||
// registry tool dispatch).
|
|
||||||
renderer: {
|
renderer: {
|
||||||
kind: 'parametric',
|
kind: 'parametric',
|
||||||
module: () => import('./renderer'),
|
module: () => import('./renderer'),
|
||||||
},
|
},
|
||||||
system: {
|
system: {
|
||||||
module: () => import('./system'),
|
module: () => import('./system'),
|
||||||
// Priority 4 mirrors the legacy WallSystem's useFrame priority — keeps
|
// Priority 4 mirrors the legacy WallSystem's useFrame priority.
|
||||||
// miter cascade running after door/window animation systems (priority 2)
|
|
||||||
// but before zone/level systems that read wall positions.
|
|
||||||
priority: 4,
|
priority: 4,
|
||||||
},
|
},
|
||||||
|
// Stage C: floor-plan rendering. ctx.siblings provides other walls in
|
||||||
|
// the level so `calculateLevelMiters` can compute correct corner joins.
|
||||||
|
floorplan: buildWallFloorplan,
|
||||||
|
|
||||||
presentation: {
|
presentation: {
|
||||||
label: 'Wall',
|
label: 'Wall',
|
||||||
@@ -123,8 +80,5 @@ export const wallDefinition: NodeDefinition<typeof WallNode> = {
|
|||||||
|
|
||||||
mcp: {
|
mcp: {
|
||||||
description: 'A wall segment defined by start + end points, with optional curve sagitta.',
|
description: 'A wall segment defined by start + end points, with optional curve sagitta.',
|
||||||
// Wall has hand-written semantic MCP tooling (`create_wall` builds full
|
|
||||||
// rooms from polygons; this entry is for the auto-derived single-wall
|
|
||||||
// primitive). Stays auto-derived until Phase 4 says otherwise.
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import {
|
||||||
|
type AnyNode,
|
||||||
|
calculateLevelMiters,
|
||||||
|
type FloorplanGeometry,
|
||||||
|
type FloorplanPoint,
|
||||||
|
type GeometryContext,
|
||||||
|
getWallPlanFootprint,
|
||||||
|
type WallNode,
|
||||||
|
} from '@pascal-app/core'
|
||||||
|
|
||||||
|
// Same constants the legacy `getFloorplanWall` uses (editor/lib/floorplan/walls.ts).
|
||||||
|
// Slightly exaggerates thin walls so the 2D plan stays legible without
|
||||||
|
// drifting from BIM data. Inlined to keep nodes/wall self-contained.
|
||||||
|
const FLOORPLAN_WALL_THICKNESS_SCALE = 1.18
|
||||||
|
const FLOORPLAN_MIN_VISIBLE_WALL_THICKNESS = 0.13
|
||||||
|
const FLOORPLAN_MAX_EXTRA_THICKNESS = 0.035
|
||||||
|
|
||||||
|
function floorplanWallThickness(wall: WallNode): number {
|
||||||
|
const baseThickness = wall.thickness ?? 0.1
|
||||||
|
const scaledThickness = baseThickness * FLOORPLAN_WALL_THICKNESS_SCALE
|
||||||
|
return Math.min(
|
||||||
|
baseThickness + FLOORPLAN_MAX_EXTRA_THICKNESS,
|
||||||
|
Math.max(baseThickness, scaledThickness, FLOORPLAN_MIN_VISIBLE_WALL_THICKNESS),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function exaggerateWallThickness(wall: WallNode): WallNode {
|
||||||
|
return { ...wall, thickness: floorplanWallThickness(wall) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stage C floor-plan builder for wall. Returns the mitered plan
|
||||||
|
* footprint polygon. Uses `ctx.siblings` to gather other walls in the
|
||||||
|
* level so `calculateLevelMiters` produces the correct corner joins.
|
||||||
|
*
|
||||||
|
* Performance note: this recomputes level miter data per wall (O(N²)
|
||||||
|
* across N walls in the level). For < 100 walls per level this is
|
||||||
|
* sub-millisecond. If a real perf hotspot surfaces, the `ctx.levelData?.
|
||||||
|
* miters` extension flagged in the plan moves the batch computation to
|
||||||
|
* the dispatcher.
|
||||||
|
*/
|
||||||
|
export function buildWallFloorplan(node: WallNode, ctx: GeometryContext): FloorplanGeometry | null {
|
||||||
|
const siblings = ctx.siblings.filter((s): s is AnyNode & WallNode => s.type === 'wall')
|
||||||
|
const all = [node, ...siblings].map(exaggerateWallThickness)
|
||||||
|
const miters = calculateLevelMiters(all)
|
||||||
|
const self = all.find((w) => w.id === node.id)
|
||||||
|
if (!self) return null
|
||||||
|
|
||||||
|
const polygon = getWallPlanFootprint(self, miters)
|
||||||
|
if (!polygon || polygon.length < 3) return null
|
||||||
|
|
||||||
|
return {
|
||||||
|
kind: 'polygon',
|
||||||
|
points: polygon.map((p) => [p.x, p.y] as FloorplanPoint),
|
||||||
|
fill: '#374151',
|
||||||
|
stroke: '#1f2937',
|
||||||
|
strokeWidth: 0.02,
|
||||||
|
opacity: 0.92,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { NodeDefinition } from '@pascal-app/core'
|
import type { NodeDefinition } from '@pascal-app/core'
|
||||||
|
import { buildWindowFloorplan } from './floorplan'
|
||||||
import { windowParametrics } from './parametrics'
|
import { windowParametrics } from './parametrics'
|
||||||
import { WindowNode } from './schema'
|
import { WindowNode } from './schema'
|
||||||
|
|
||||||
@@ -6,8 +7,12 @@ import { WindowNode } from './schema'
|
|||||||
* Window — Phase 5 batch kind. Mirrors door's shape: hosted on walls,
|
* Window — Phase 5 batch kind. Mirrors door's shape: hosted on walls,
|
||||||
* cuts holes in them, animated open/close state for opening windows.
|
* cuts holes in them, animated open/close state for opening windows.
|
||||||
*
|
*
|
||||||
* Capabilities: no `movable` (wall-bound drag is bespoke). Tool field
|
* Stages:
|
||||||
* absent (legacy WindowTool / MoveWindowTool continue).
|
* - A: registered.
|
||||||
|
* - B: deferred — window geometry ~800 lines; extraction is a focused
|
||||||
|
* session. `def.renderer` + `def.system` wrap-export legacy.
|
||||||
|
* - C: `def.floorplan` polygon sits in parent wall's cutout. Legacy
|
||||||
|
* `openingPolygons` short-circuits window entries when registered.
|
||||||
*/
|
*/
|
||||||
export const windowDefinition: NodeDefinition<typeof WindowNode> = {
|
export const windowDefinition: NodeDefinition<typeof WindowNode> = {
|
||||||
kind: 'window',
|
kind: 'window',
|
||||||
@@ -39,6 +44,9 @@ export const windowDefinition: NodeDefinition<typeof WindowNode> = {
|
|||||||
module: () => import('./system'),
|
module: () => import('./system'),
|
||||||
priority: 3,
|
priority: 3,
|
||||||
},
|
},
|
||||||
|
// Stage C: floor-plan polygon. ctx.parent gives the wall for direction
|
||||||
|
// + thickness — same shape as door.
|
||||||
|
floorplan: buildWindowFloorplan,
|
||||||
|
|
||||||
toolHints: [
|
toolHints: [
|
||||||
{ key: 'Left click', label: 'Place window on wall' },
|
{ key: 'Left click', label: 'Place window on wall' },
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import type {
|
||||||
|
FloorplanGeometry,
|
||||||
|
FloorplanPoint,
|
||||||
|
GeometryContext,
|
||||||
|
WallNode,
|
||||||
|
WindowNode,
|
||||||
|
} from '@pascal-app/core'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stage C floor-plan builder for window. Mirrors door's shape — window
|
||||||
|
* polygon sits in the wall's cutout, width along wall, depth = wall
|
||||||
|
* thickness. Visually distinct via a glass-blue tint.
|
||||||
|
*/
|
||||||
|
export function buildWindowFloorplan(
|
||||||
|
node: WindowNode,
|
||||||
|
ctx: GeometryContext,
|
||||||
|
): FloorplanGeometry | null {
|
||||||
|
const wall = ctx.parent as WallNode | null
|
||||||
|
if (!wall || wall.type !== 'wall') return null
|
||||||
|
|
||||||
|
const [x1, z1] = wall.start
|
||||||
|
const [x2, z2] = wall.end
|
||||||
|
const dx = x2 - x1
|
||||||
|
const dz = z2 - z1
|
||||||
|
const length = Math.sqrt(dx * dx + dz * dz)
|
||||||
|
if (length < 1e-9) return null
|
||||||
|
|
||||||
|
const dirX = dx / length
|
||||||
|
const dirZ = dz / length
|
||||||
|
const perpX = -dirZ
|
||||||
|
const perpZ = dirX
|
||||||
|
|
||||||
|
const distance = node.position[0]
|
||||||
|
const width = node.width
|
||||||
|
const depth = wall.thickness ?? 0.1
|
||||||
|
const cx = x1 + dirX * distance
|
||||||
|
const cz = z1 + dirZ * distance
|
||||||
|
const halfWidth = width / 2
|
||||||
|
const halfDepth = depth / 2
|
||||||
|
|
||||||
|
const points: readonly FloorplanPoint[] = [
|
||||||
|
[cx - dirX * halfWidth + perpX * halfDepth, cz - dirZ * halfWidth + perpZ * halfDepth],
|
||||||
|
[cx + dirX * halfWidth + perpX * halfDepth, cz + dirZ * halfWidth + perpZ * halfDepth],
|
||||||
|
[cx + dirX * halfWidth - perpX * halfDepth, cz + dirZ * halfWidth - perpZ * halfDepth],
|
||||||
|
[cx - dirX * halfWidth - perpX * halfDepth, cz - dirZ * halfWidth - perpZ * halfDepth],
|
||||||
|
]
|
||||||
|
|
||||||
|
return {
|
||||||
|
kind: 'polygon',
|
||||||
|
points,
|
||||||
|
fill: '#bae6fd',
|
||||||
|
stroke: '#0c4a6e',
|
||||||
|
strokeWidth: 0.015,
|
||||||
|
opacity: 0.8,
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user