Phase 5 depth-first: spawn C, fence B+C, slab B+C, ceiling C
Depth-first session: drive registered kinds through Stage B (pure
def.geometry, drop system re-export) and Stage C (def.floorplan,
short-circuit legacy inline rendering in floorplan-panel.tsx).
spawn → C
- buildSpawnFloorplan wired on definition (was written but deferred
to avoid double-render).
- floorplan-panel.tsx's floorplanSpawnEntries useMemo short-circuits
to [] when nodeRegistry.has('spawn').
fence → B
- generateFenceGeometry exported from viewer; buildFenceGeometry
wraps it in a Group+Mesh with DEFAULT_STAIR_MATERIAL.
- def.geometry set; renderer + system fields dropped.
- Deleted nodes/src/fence/{renderer.tsx,system.tsx}.
fence → C
- buildFenceFloorplan: polyline along centerline (sampled for curved
fences via sampleWallCenterline from core). Stroke width = node.thickness.
- floorplan-panel.tsx's floorplanFenceEntries short-circuits.
slab → B
- generateSlabGeometry exported from viewer; buildSlabGeometry wraps
it in a Group+Mesh + cached material (preset / custom / default
pattern preserved from legacy renderer).
- def.geometry set; renderer + system fields dropped.
- Deleted nodes/src/slab/{renderer.tsx,system.tsx}.
slab → C
- buildSlabFloorplan: SVG path with outer polygon + hole subpaths
(uses getRenderableSlabPolygon from core for wall-clipping parity).
- floorplan-panel.tsx's slabPolygons short-circuits.
ceiling → B INTENTIONALLY SKIPPED
- Ceiling renderer renders React children (hosted items) + uses TSL
shader materials + named meshes that other systems poke
(getObjectByName('ceiling-grid')). Pure def.geometry can't preserve
that. Ceiling keeps def.renderer (the custom escape hatch) — same
pattern item uses. Documented in ceiling/definition.ts.
ceiling → C
- buildCeilingFloorplan: dashed-outline path with hole subpaths
(visually distinct from slab since ceilings are above).
- floorplan-panel.tsx's ceilingPolygons short-circuits.
Per-kind progress after this session:
- shelf: B ✅ C ✅ (Stage E since brand-new)
- spawn: A ✅ C ✅
- wall: A ✅ (B blocked on ctx.levelData design)
- fence: A ✅ B ✅ C ✅
- slab: A ✅ B ✅ C ✅
- ceiling: A ✅ C ✅ (B intentionally not applicable)
- door / window / item: A ✅ (B+C pending in future sessions)
Known test issue: `bun test` in packages/nodes fails to load
`three-bvh-csg` through the viewer's transitive imports (UMD/ESM
mismatch in Bun's test runner). The Next.js editor build works fine
because it bundles differently. Fix requires either dynamic imports
(breaks sync def.geometry contract) or test env config — deferred.
Other tests (schema, geometry, parity) pass.
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
df07f7bcb2
commit
969b154b08
@@ -8440,9 +8440,13 @@ export function FloorplanPanel() {
|
|||||||
|
|
||||||
return hasPreviewWalls ? nextFloorplanWallById : floorplanWallById
|
return hasPreviewWalls ? nextFloorplanWallById : floorplanWallById
|
||||||
}, [displayWallById, floorplanWallById, wallCurveDraft, wallEndpointDraft])
|
}, [displayWallById, floorplanWallById, wallCurveDraft, wallEndpointDraft])
|
||||||
const floorplanFenceEntries = useMemo(
|
const floorplanFenceEntries = useMemo(() => {
|
||||||
() =>
|
// Fence migrated to def.floorplan (Phase 5 Stage C). When registered,
|
||||||
fences.flatMap((fence) => {
|
// FloorplanRegistryLayer renders the fence polyline; this legacy
|
||||||
|
// path short-circuits to avoid double-render. Removed entirely in
|
||||||
|
// Phase 6 cleanup.
|
||||||
|
if (nodeRegistry.has('fence')) return []
|
||||||
|
return fences.flatMap((fence) => {
|
||||||
const live = useLiveTransforms.getState().get(fence.id)
|
const live = useLiveTransforms.getState().get(fence.id)
|
||||||
const fenceCenterX = (fence.start[0] + fence.end[0]) / 2
|
const fenceCenterX = (fence.start[0] + fence.end[0]) / 2
|
||||||
const fenceCenterZ = (fence.start[1] + fence.end[1]) / 2
|
const fenceCenterZ = (fence.start[1] + fence.end[1]) / 2
|
||||||
@@ -8480,9 +8484,8 @@ 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(
|
||||||
() =>
|
() =>
|
||||||
walls.map((wall) => {
|
walls.map((wall) => {
|
||||||
@@ -8579,9 +8582,13 @@ export function FloorplanPanel() {
|
|||||||
}),
|
}),
|
||||||
[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,
|
||||||
slabs.flatMap((slab) => {
|
// FloorplanRegistryLayer renders the slab polygon; this legacy
|
||||||
|
// path short-circuits to avoid double-render. Removed entirely in
|
||||||
|
// Phase 6 cleanup.
|
||||||
|
if (nodeRegistry.has('slab')) return []
|
||||||
|
return slabs.flatMap((slab) => {
|
||||||
const polygon = toFloorplanPolygon(slab.polygon)
|
const polygon = toFloorplanPolygon(slab.polygon)
|
||||||
if (polygon.length < 3) {
|
if (polygon.length < 3) {
|
||||||
return []
|
return []
|
||||||
@@ -8603,9 +8610,8 @@ export function FloorplanPanel() {
|
|||||||
path: formatPolygonPath(visualPolygon, visualHoles),
|
path: formatPolygonPath(visualPolygon, visualHoles),
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
}),
|
})
|
||||||
[slabs],
|
}, [slabs])
|
||||||
)
|
|
||||||
const displaySlabPolygons = useMemo(() => {
|
const displaySlabPolygons = useMemo(() => {
|
||||||
if (!(slabBoundaryDraft || slabHoleBoundaryDraft || slabHoleMoveDraft)) {
|
if (!(slabBoundaryDraft || slabHoleBoundaryDraft || slabHoleMoveDraft)) {
|
||||||
return slabPolygons
|
return slabPolygons
|
||||||
@@ -8662,9 +8668,12 @@ export function FloorplanPanel() {
|
|||||||
return nextEntry
|
return nextEntry
|
||||||
})
|
})
|
||||||
}, [slabBoundaryDraft, slabHoleBoundaryDraft, slabHoleMoveDraft, slabPolygons])
|
}, [slabBoundaryDraft, slabHoleBoundaryDraft, slabHoleMoveDraft, slabPolygons])
|
||||||
const ceilingPolygons = useMemo(
|
const ceilingPolygons = useMemo(() => {
|
||||||
() =>
|
// Ceiling migrated to def.floorplan (Phase 5 Stage C). When registered,
|
||||||
ceilings.flatMap((ceiling) => {
|
// FloorplanRegistryLayer renders the ceiling polygon; this legacy
|
||||||
|
// path short-circuits. Removed entirely in Phase 6 cleanup.
|
||||||
|
if (nodeRegistry.has('ceiling')) return []
|
||||||
|
return ceilings.flatMap((ceiling) => {
|
||||||
const polygon = toFloorplanPolygon(ceiling.polygon)
|
const polygon = toFloorplanPolygon(ceiling.polygon)
|
||||||
if (polygon.length < 3) {
|
if (polygon.length < 3) {
|
||||||
return []
|
return []
|
||||||
@@ -8682,9 +8691,8 @@ export function FloorplanPanel() {
|
|||||||
path: formatPolygonPath(polygon, holes),
|
path: formatPolygonPath(polygon, holes),
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
}),
|
})
|
||||||
[ceilings],
|
}, [ceilings])
|
||||||
)
|
|
||||||
const displayCeilingPolygons = useMemo(() => {
|
const displayCeilingPolygons = useMemo(() => {
|
||||||
if (!(ceilingBoundaryDraft || ceilingHoleBoundaryDraft || ceilingHoleMoveDraft)) {
|
if (!(ceilingBoundaryDraft || ceilingHoleBoundaryDraft || ceilingHoleMoveDraft)) {
|
||||||
return ceilingPolygons
|
return ceilingPolygons
|
||||||
@@ -8802,13 +8810,18 @@ export function FloorplanPanel() {
|
|||||||
),
|
),
|
||||||
[levelDescendantNodes],
|
[levelDescendantNodes],
|
||||||
)
|
)
|
||||||
const floorplanSpawnEntries = useMemo<FloorplanSpawnEntry[]>(
|
const floorplanSpawnEntries = useMemo<FloorplanSpawnEntry[]>(() => {
|
||||||
() =>
|
// Spawn migrated to the registry-driven floor-plan layer (Phase 5
|
||||||
spawns
|
// Stage C). When registered, FloorplanRegistryLayer renders the
|
||||||
|
// spawn marker via def.floorplan; FloorplanRegistryActionMenu
|
||||||
|
// handles select / move / delete. Returning [] here skips the
|
||||||
|
// legacy rendering + action menu paths to avoid double-render.
|
||||||
|
// Removed entirely in Phase 6 cleanup.
|
||||||
|
if (nodeRegistry.has('spawn')) return []
|
||||||
|
return spawns
|
||||||
.filter((spawn) => spawn.visible !== false)
|
.filter((spawn) => spawn.visible !== false)
|
||||||
.map((spawn) => {
|
.map((spawn) => {
|
||||||
const live = useLiveTransforms.getState().get(spawn.id)
|
const live = useLiveTransforms.getState().get(spawn.id)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
spawn,
|
spawn,
|
||||||
position: {
|
position: {
|
||||||
@@ -8817,9 +8830,8 @@ export function FloorplanPanel() {
|
|||||||
},
|
},
|
||||||
rotation: live?.rotation ?? spawn.rotation,
|
rotation: live?.rotation ?? spawn.rotation,
|
||||||
}
|
}
|
||||||
}),
|
})
|
||||||
[movingFloorplanNodeRevision, spawns],
|
}, [movingFloorplanNodeRevision, spawns])
|
||||||
)
|
|
||||||
const floorplanItemEntries = useMemo(() => {
|
const floorplanItemEntries = useMemo(() => {
|
||||||
const transformCache = new Map<string, SharedFloorplanNodeTransform | null>()
|
const transformCache = new Map<string, SharedFloorplanNodeTransform | null>()
|
||||||
|
|
||||||
|
|||||||
@@ -1,20 +1,23 @@
|
|||||||
import type { NodeDefinition } from '@pascal-app/core'
|
import type { NodeDefinition } from '@pascal-app/core'
|
||||||
|
import { buildCeilingFloorplan } from './floorplan'
|
||||||
import { ceilingParametrics } from './parametrics'
|
import { ceilingParametrics } from './parametrics'
|
||||||
import { CeilingNode } from './schema'
|
import { CeilingNode } from './schema'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ceiling — Phase 5 batch kind, polygon-based. Structurally identical
|
* Ceiling — Phase 5 batch kind, polygon-based. Structurally similar to
|
||||||
* to slab but mounted at `height` rather than `elevation`.
|
* slab but with React-rendered hosted children + TSL shader materials +
|
||||||
|
* named meshes that other systems poke (`getObjectByName('ceiling-grid')`).
|
||||||
*
|
*
|
||||||
* Capabilities:
|
* **Stage B intentionally skipped**: pure `def.geometry` extraction
|
||||||
* - **No `movable`**: ceiling move is bespoke via legacy `MoveCeilingTool`
|
* would lose the React children rendering (hosted items) and the
|
||||||
* + the floor-plan boundary / hole editors. Capability-driven dispatch
|
* named-mesh structure. Ceiling keeps `def.renderer` as the custom
|
||||||
* keeps the legacy mover (preserves polygon-aware behavior).
|
* escape hatch (per plans/editor-node-registry.md "custom-behavior
|
||||||
* - **`surfaces.top`**: items host on the ceiling at `height`.
|
* escape hatch"). Renderer wraps the legacy CeilingRenderer; system
|
||||||
* - `selectable`, `duplicable`, `deletable` standard.
|
* wraps the legacy CeilingSystem.
|
||||||
*
|
*
|
||||||
* Relations: `hosts: ['item']` for ceiling-mounted items (lights, fans).
|
* **Stage C completed**: `def.floorplan` builder draws the ceiling
|
||||||
* `cascadeDelete: 'descendants'` removes hosted items on ceiling delete.
|
* polygon as a dashed outline in floor plan; legacy `ceilingPolygons`
|
||||||
|
* short-circuits to [] when ceiling is registered.
|
||||||
*/
|
*/
|
||||||
export const ceilingDefinition: NodeDefinition<typeof CeilingNode> = {
|
export const ceilingDefinition: NodeDefinition<typeof CeilingNode> = {
|
||||||
kind: 'ceiling',
|
kind: 'ceiling',
|
||||||
@@ -59,6 +62,7 @@ export const ceilingDefinition: NodeDefinition<typeof CeilingNode> = {
|
|||||||
module: () => import('./system'),
|
module: () => import('./system'),
|
||||||
priority: 4,
|
priority: 4,
|
||||||
},
|
},
|
||||||
|
floorplan: buildCeilingFloorplan,
|
||||||
|
|
||||||
toolHints: [
|
toolHints: [
|
||||||
{ key: 'Left click', label: 'Trace ceiling outline' },
|
{ key: 'Left click', label: 'Trace ceiling outline' },
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import type { CeilingNode, FloorplanGeometry, FloorplanPoint } from '@pascal-app/core'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stage C floor-plan builder for ceiling. Renders the polygon outline
|
||||||
|
* as a dashed boundary (ceilings are above and would visually obscure
|
||||||
|
* the slab/walls if drawn solid). Same shape as slab but visually
|
||||||
|
* distinct.
|
||||||
|
*/
|
||||||
|
export function buildCeilingFloorplan(node: CeilingNode): FloorplanGeometry | null {
|
||||||
|
const polygon = node.polygon
|
||||||
|
if (!polygon || polygon.length < 3) return null
|
||||||
|
|
||||||
|
const outer: FloorplanPoint[] = polygon.map(([x, z]) => [x, z] as FloorplanPoint)
|
||||||
|
|
||||||
|
const ring = (points: FloorplanPoint[]) => {
|
||||||
|
const [first, ...rest] = points
|
||||||
|
if (!first) return ''
|
||||||
|
return [`M ${first[0]} ${first[1]}`, ...rest.map(([x, y]) => `L ${x} ${y}`), 'Z'].join(' ')
|
||||||
|
}
|
||||||
|
|
||||||
|
const segments: string[] = [ring(outer)]
|
||||||
|
const holes = node.holes ?? []
|
||||||
|
for (const hole of holes) {
|
||||||
|
if (hole.length < 3) continue
|
||||||
|
segments.push(ring(hole.map(([x, z]) => [x, z] as FloorplanPoint)))
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
kind: 'path',
|
||||||
|
d: segments.join(' '),
|
||||||
|
fill: 'none',
|
||||||
|
stroke: '#94a3b8',
|
||||||
|
strokeWidth: 0.03,
|
||||||
|
strokeDasharray: '0.15 0.1',
|
||||||
|
opacity: 0.7,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,34 +1,21 @@
|
|||||||
import type { NodeDefinition } from '@pascal-app/core'
|
import type { NodeDefinition } from '@pascal-app/core'
|
||||||
|
import { buildFenceFloorplan } from './floorplan'
|
||||||
|
import { buildFenceGeometry } from './geometry'
|
||||||
import { fenceParametrics } from './parametrics'
|
import { fenceParametrics } from './parametrics'
|
||||||
import { FenceNode } from './schema'
|
import { FenceNode } from './schema'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fence — the first Phase 5 batch-migration kind.
|
* Fence — Phase 5 batch kind. Stage B complete: `def.geometry` drives
|
||||||
|
* the rebuild via the generic `<GeometrySystem>`; `<ParametricNodeRenderer>`
|
||||||
|
* mounts the empty group. No per-kind renderer or system file.
|
||||||
*
|
*
|
||||||
* What this definition encodes:
|
* Capabilities:
|
||||||
* - **Capabilities**: snappable (other walls/fences/items snap to it),
|
* - **No `movable`**: fence move is bespoke endpoint-drag. Capability-
|
||||||
* surfaces (front + back faces host items), selectable, duplicable,
|
* driven dispatch keeps the legacy MoveFenceTool until the
|
||||||
* deletable. No `movable` capability — fence move is bespoke
|
* affordance port (Stage D).
|
||||||
* endpoint-drag, same shape as wall (handled by legacy MoveFenceTool
|
* - `surfaces.sides`, `selectable`, `duplicable`, `deletable` standard.
|
||||||
* until the affordance port).
|
|
||||||
* - **Relations**: `linkedBy: 'endpoint-match'` for fence-corner cascade.
|
|
||||||
* No `hosts` field — doors/windows don't mount on fences. `affectsSpatial`
|
|
||||||
* omitted: moving a fence doesn't dirty slabs/zones in the legacy
|
|
||||||
* behavior, so the registry stays parity-equivalent until we
|
|
||||||
* explicitly add the cascade (separate decision).
|
|
||||||
* - **Parametrics**: dimensions, posts, style — see `./parametrics.ts`.
|
|
||||||
* - **toolHints**: placement panel hints for the fence-build tool.
|
|
||||||
* - **Renderer + system**: thin placeholder mesh + re-export of the
|
|
||||||
* legacy `FenceSystem`. Same shape as wall milestone B; future Phase 5+
|
|
||||||
* extracts the pure geometry function and migrates to `def.geometry`.
|
|
||||||
*
|
*
|
||||||
* Tool field stays absent: fence has 4 separate tools (build, curve,
|
* Relations: `linkedBy: 'endpoint-match'` for corner cascade.
|
||||||
* move, move-endpoint) wired through editor state, not the registry
|
|
||||||
* tool dispatch. They keep running unchanged until the affordance port.
|
|
||||||
*
|
|
||||||
* Migration is gated by `feature-flag.ts`
|
|
||||||
* (env: `NEXT_PUBLIC_USE_REGISTRY_FOR_FENCE`). See
|
|
||||||
* `plans/editor-node-registry.md#phase-5` for the batch order.
|
|
||||||
*/
|
*/
|
||||||
export const fenceDefinition: NodeDefinition<typeof FenceNode> = {
|
export const fenceDefinition: NodeDefinition<typeof FenceNode> = {
|
||||||
kind: 'fence',
|
kind: 'fence',
|
||||||
@@ -71,16 +58,15 @@ export const fenceDefinition: NodeDefinition<typeof FenceNode> = {
|
|||||||
|
|
||||||
parametrics: fenceParametrics,
|
parametrics: fenceParametrics,
|
||||||
|
|
||||||
renderer: {
|
// Stage B: pure geometry function. Generic <GeometrySystem> rebuilds
|
||||||
kind: 'parametric',
|
// on dirtyNodes; <ParametricNodeRenderer> mounts the empty group.
|
||||||
module: () => import('./renderer'),
|
// `renderer` + `system` fields dropped along with their files.
|
||||||
},
|
geometry: buildFenceGeometry,
|
||||||
system: {
|
// Stage C: floor-plan rendering. FloorplanRegistryLayer iterates kinds
|
||||||
module: () => import('./system'),
|
// with `floorplan` set and renders via FloorplanGeometryRenderer.
|
||||||
// Same frame priority as the legacy FenceSystem (4 — runs after door/
|
// Legacy `floorplanFenceEntries` short-circuits to [] when fence is
|
||||||
// window animations at 2-3, before zone/level systems at 6+).
|
// registered (see floorplan-panel.tsx).
|
||||||
priority: 4,
|
floorplan: buildFenceFloorplan,
|
||||||
},
|
|
||||||
|
|
||||||
toolHints: [
|
toolHints: [
|
||||||
{ key: 'Left click', label: 'Set fence start / end' },
|
{ key: 'Left click', label: 'Set fence start / end' },
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import type { FloorplanGeometry, FloorplanPoint } from '@pascal-app/core'
|
||||||
|
import { isCurvedWall, sampleWallCenterline } from '@pascal-app/core'
|
||||||
|
import type { FenceNode } from './schema'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stage C floor-plan builder for fence. Draws the fence centerline as
|
||||||
|
* a polyline; thickness becomes the stroke width.
|
||||||
|
*
|
||||||
|
* Curved fences sample the centerline at 24 segments — same density the
|
||||||
|
* legacy `floorplanFenceEntries` useMemo uses, so straight + curved
|
||||||
|
* fences look comparable to the legacy rendering.
|
||||||
|
*
|
||||||
|
* Visual nuances the legacy ships (side hatching to indicate thickness
|
||||||
|
* direction, post markers along the centerline) are deferred — Phase 5
|
||||||
|
* Stage D will revisit if real visual parity is needed.
|
||||||
|
*/
|
||||||
|
export function buildFenceFloorplan(node: FenceNode): FloorplanGeometry {
|
||||||
|
const points: FloorplanPoint[] = isCurvedWall(node)
|
||||||
|
? sampleWallCenterline(node, 24).map((p) => [p.x, p.y] as FloorplanPoint)
|
||||||
|
: [
|
||||||
|
[node.start[0], node.start[1]],
|
||||||
|
[node.end[0], node.end[1]],
|
||||||
|
]
|
||||||
|
|
||||||
|
return {
|
||||||
|
kind: 'polyline',
|
||||||
|
points,
|
||||||
|
stroke: node.color || '#475569',
|
||||||
|
strokeWidth: Math.max(node.thickness, 0.05),
|
||||||
|
opacity: 0.9,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import { DEFAULT_STAIR_MATERIAL, generateFenceGeometry } from '@pascal-app/viewer'
|
||||||
|
import { Group, Mesh } from 'three'
|
||||||
|
import type { FenceNode } from './schema'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stage B builder for fence. Reuses the legacy `generateFenceGeometry`
|
||||||
|
* (pure function from viewer that returns a merged BufferGeometry of
|
||||||
|
* posts + base + top rail + curve spans) and wraps it in a Mesh-in-Group
|
||||||
|
* shape the generic `<GeometrySystem>` expects.
|
||||||
|
*
|
||||||
|
* Material is a single shared reference — fences look the same regardless
|
||||||
|
* of instance, so we don't clone per node. If per-fence material
|
||||||
|
* customization lands later (color picker on the panel maps to a real
|
||||||
|
* material), this becomes a per-node lookup.
|
||||||
|
*
|
||||||
|
* Phase 6 cleanup moves the 280 lines of geometry math out of the
|
||||||
|
* legacy `viewer/src/systems/fence/fence-system.tsx` into this folder
|
||||||
|
* once the legacy system file is deleted. Until then `generateFenceGeometry`
|
||||||
|
* is publicly re-exported from viewer.
|
||||||
|
*/
|
||||||
|
export function buildFenceGeometry(node: FenceNode): Group {
|
||||||
|
const group = new Group()
|
||||||
|
const geometry = generateFenceGeometry(node)
|
||||||
|
const mesh = new Mesh(geometry, DEFAULT_STAIR_MATERIAL)
|
||||||
|
mesh.castShadow = true
|
||||||
|
mesh.receiveShadow = true
|
||||||
|
group.add(mesh)
|
||||||
|
return group
|
||||||
|
}
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
'use client'
|
|
||||||
|
|
||||||
import { type FenceNode, useRegistry, useScene } from '@pascal-app/core'
|
|
||||||
import { DEFAULT_STAIR_MATERIAL, useNodeEvents } from '@pascal-app/viewer'
|
|
||||||
import { useLayoutEffect, useMemo, useRef } from 'react'
|
|
||||||
import type { Mesh } from 'three'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Thin fence renderer — registers an empty mesh, marks the node dirty so
|
|
||||||
* `FenceSystem` (re-exported via `./system`) fills the geometry next
|
|
||||||
* frame, and wires pointer events through `useNodeEvents`.
|
|
||||||
*
|
|
||||||
* Behaviorally identical to the legacy `FenceRenderer` in
|
|
||||||
* `@pascal-app/viewer/components/renderers/fence/fence-renderer.tsx`.
|
|
||||||
* Phase 0 shims pick which one mounts based on `nodeRegistry.has('fence')`.
|
|
||||||
*
|
|
||||||
* Material is `DEFAULT_STAIR_MATERIAL` (legacy reuse; fence and stairs
|
|
||||||
* share the wood-tone preset).
|
|
||||||
*/
|
|
||||||
const FenceRenderer = ({ node }: { node: FenceNode }) => {
|
|
||||||
const ref = useRef<Mesh>(null!)
|
|
||||||
const handlers = useNodeEvents(node, 'fence')
|
|
||||||
const material = useMemo(() => DEFAULT_STAIR_MATERIAL, [])
|
|
||||||
|
|
||||||
useRegistry(node.id, 'fence', ref)
|
|
||||||
useLayoutEffect(() => {
|
|
||||||
useScene.getState().markDirty(node.id)
|
|
||||||
}, [node.id])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<mesh
|
|
||||||
castShadow
|
|
||||||
material={material}
|
|
||||||
receiveShadow
|
|
||||||
ref={ref}
|
|
||||||
visible={node.visible}
|
|
||||||
{...handlers}
|
|
||||||
>
|
|
||||||
<boxGeometry args={[0, 0, 0]} />
|
|
||||||
</mesh>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default FenceRenderer
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
'use client'
|
|
||||||
|
|
||||||
import { FenceSystem } from '@pascal-app/viewer'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Registry-driven fence system bundle.
|
|
||||||
*
|
|
||||||
* Wraps the legacy `FenceSystem` (re-exported from viewer) so it mounts
|
|
||||||
* via `RegisteredSystems` when fence is registry-driven. The legacy
|
|
||||||
* `<LegacySystem kind="fence">` wrapper around `<FenceSystem />` in
|
|
||||||
* `viewer/components/viewer/index.tsx` short-circuits whenever
|
|
||||||
* `nodeRegistry.has('fence')` is true — same pattern wall uses.
|
|
||||||
*
|
|
||||||
* Phase 6 deletes the legacy mount point; until then this bundle is the
|
|
||||||
* single mount surface for fence's per-frame work when registry-driven.
|
|
||||||
*
|
|
||||||
* Future Phase 5+ work: extract fence geometry out of `FenceSystem`'s
|
|
||||||
* useFrame body into a pure `buildFenceGeometry(node, ctx)` and migrate
|
|
||||||
* to `def.geometry`. The generic `<GeometrySystem>` will then handle
|
|
||||||
* the rebuild loop and this bundle can be deleted.
|
|
||||||
*/
|
|
||||||
const FenceSystems = () => {
|
|
||||||
return <FenceSystem />
|
|
||||||
}
|
|
||||||
|
|
||||||
export default FenceSystems
|
|
||||||
@@ -1,29 +1,24 @@
|
|||||||
import type { NodeDefinition } from '@pascal-app/core'
|
import type { NodeDefinition } from '@pascal-app/core'
|
||||||
|
import { buildSlabFloorplan } from './floorplan'
|
||||||
|
import { buildSlabGeometry } from './geometry'
|
||||||
import { slabParametrics } from './parametrics'
|
import { slabParametrics } from './parametrics'
|
||||||
import { SlabNode } from './schema'
|
import { SlabNode } from './schema'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Slab — Phase 5 batch kind, polygon-based.
|
* Slab — Phase 5 batch kind, polygon-based. Stage B: `def.geometry`
|
||||||
|
* drives the rebuild via generic <GeometrySystem>; <ParametricNodeRenderer>
|
||||||
|
* mounts the empty group. No per-kind renderer or system file.
|
||||||
*
|
*
|
||||||
* Capabilities:
|
* Capabilities:
|
||||||
* - **No `movable`**: slab's "move" today is whole-slab translation via
|
* - **No `movable`**: slab's "move" today is whole-slab translation via
|
||||||
* legacy `MoveSlabTool`, which integrates with the floor-plan boundary /
|
* legacy `MoveSlabTool`, which integrates with the floor-plan boundary /
|
||||||
* hole editors. Per the capability-driven dispatch rule, omitting
|
* hole editors. Capability-driven dispatch keeps the legacy mover.
|
||||||
* `movable` keeps the legacy mover (preserves polygon-aware behavior).
|
|
||||||
* Migration to the generic mover is possible in a later milestone if
|
|
||||||
* the legacy mover proves equivalent.
|
|
||||||
* - **`surfaces.top`**: items host on the slab top at `elevation`.
|
* - **`surfaces.top`**: items host on the slab top at `elevation`.
|
||||||
* - `selectable`, `duplicable`, `deletable` standard.
|
* - `selectable`, `duplicable`, `deletable` standard.
|
||||||
*
|
*
|
||||||
* Relations:
|
* Relations:
|
||||||
* - `hosts: ['item']` — items mount on the slab top.
|
* - `hosts: ['item']` — items mount on the slab top.
|
||||||
* - `cascadeDelete: 'descendants'` — deleting a slab removes hosted items.
|
* - `cascadeDelete: 'descendants'` — deleting a slab removes hosted items.
|
||||||
*
|
|
||||||
* Renderer + system: thin renderer + re-export of the legacy `SlabSystem`.
|
|
||||||
* Same shape as wall / fence runtime port.
|
|
||||||
*
|
|
||||||
* Tool field absent: slab has 3 tools (slab-tool, boundary-editor, hole-
|
|
||||||
* editor) wired through editor state, not registry dispatch.
|
|
||||||
*/
|
*/
|
||||||
export const slabDefinition: NodeDefinition<typeof SlabNode> = {
|
export const slabDefinition: NodeDefinition<typeof SlabNode> = {
|
||||||
kind: 'slab',
|
kind: 'slab',
|
||||||
@@ -59,14 +54,11 @@ export const slabDefinition: NodeDefinition<typeof SlabNode> = {
|
|||||||
|
|
||||||
parametrics: slabParametrics,
|
parametrics: slabParametrics,
|
||||||
|
|
||||||
renderer: {
|
// Stage B: pure geometry function.
|
||||||
kind: 'parametric',
|
geometry: buildSlabGeometry,
|
||||||
module: () => import('./renderer'),
|
// Stage C: floor-plan rendering. Legacy `slabPolygons` short-circuits
|
||||||
},
|
// to [] when slab is registered (see floorplan-panel.tsx).
|
||||||
system: {
|
floorplan: buildSlabFloorplan,
|
||||||
module: () => import('./system'),
|
|
||||||
priority: 4,
|
|
||||||
},
|
|
||||||
|
|
||||||
toolHints: [
|
toolHints: [
|
||||||
{ key: 'Left click', label: 'Trace slab outline' },
|
{ key: 'Left click', label: 'Trace slab outline' },
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import {
|
||||||
|
type FloorplanGeometry,
|
||||||
|
type FloorplanPoint,
|
||||||
|
getRenderableSlabPolygon,
|
||||||
|
type SlabNode,
|
||||||
|
} from '@pascal-app/core'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stage C floor-plan builder for slab. Renders the slab polygon as a
|
||||||
|
* filled path with holes cut out.
|
||||||
|
*
|
||||||
|
* Uses `getRenderableSlabPolygon` (the same helper the legacy
|
||||||
|
* floorplan-panel.tsx uses) to compute the visual polygon — accounts
|
||||||
|
* for wall-clipping when a slab is auto-generated from walls.
|
||||||
|
*/
|
||||||
|
export function buildSlabFloorplan(node: SlabNode): FloorplanGeometry | null {
|
||||||
|
const polygon = node.polygon
|
||||||
|
if (!polygon || polygon.length < 3) return null
|
||||||
|
|
||||||
|
const visualPolygon = getRenderableSlabPolygon(node)
|
||||||
|
if (!visualPolygon || visualPolygon.length < 3) return null
|
||||||
|
|
||||||
|
const outer: FloorplanPoint[] = visualPolygon.map(([x, z]) => [x, z] as FloorplanPoint)
|
||||||
|
|
||||||
|
// SVG path with outer ring + hole subpaths. Each subpath uses M/L
|
||||||
|
// commands + Z to close. Holes follow the outer ring; FloorplanGeometry
|
||||||
|
// 'path' kind supports this natively (renderer passes the `d` string
|
||||||
|
// straight to the SVG <path>).
|
||||||
|
const segments: string[] = []
|
||||||
|
const ring = (points: FloorplanPoint[]) => {
|
||||||
|
const [first, ...rest] = points
|
||||||
|
if (!first) return ''
|
||||||
|
return [`M ${first[0]} ${first[1]}`, ...rest.map(([x, y]) => `L ${x} ${y}`), 'Z'].join(' ')
|
||||||
|
}
|
||||||
|
segments.push(ring(outer))
|
||||||
|
|
||||||
|
const holes = node.holes ?? []
|
||||||
|
for (const hole of holes) {
|
||||||
|
if (hole.length < 3) continue
|
||||||
|
const holePts: FloorplanPoint[] = hole.map(([x, z]) => [x, z] as FloorplanPoint)
|
||||||
|
segments.push(ring(holePts))
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
kind: 'path',
|
||||||
|
d: segments.join(' '),
|
||||||
|
fill: '#cbd5e1',
|
||||||
|
stroke: '#475569',
|
||||||
|
strokeWidth: 0.03,
|
||||||
|
opacity: 0.85,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import { getMaterialPresetByRef, type SlabNode } from '@pascal-app/core'
|
||||||
|
import {
|
||||||
|
applyMaterialPresetToMaterials,
|
||||||
|
createMaterial,
|
||||||
|
DEFAULT_SLAB_MATERIAL,
|
||||||
|
generateSlabGeometry,
|
||||||
|
} from '@pascal-app/viewer'
|
||||||
|
import { DoubleSide, Group, Mesh, MeshStandardMaterial } from 'three'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stage B builder for slab. Reuses `generateSlabGeometry` (pure
|
||||||
|
* triangulation + hole CSG from viewer) and the same material cache
|
||||||
|
* pattern the legacy slab renderer used.
|
||||||
|
*
|
||||||
|
* Materials are cached by `{material, materialPreset}` signature so
|
||||||
|
* slabs sharing settings share the GPU resource. Cached entry mutation
|
||||||
|
* (preset apply) is preserved — async texture loads still update the
|
||||||
|
* rendered material after re-mount.
|
||||||
|
*/
|
||||||
|
const slabMaterialCache = new Map<string, MeshStandardMaterial>()
|
||||||
|
|
||||||
|
function getSlabMaterial(node: SlabNode): MeshStandardMaterial {
|
||||||
|
const cacheKey = JSON.stringify({
|
||||||
|
material: node.material ?? null,
|
||||||
|
materialPreset: node.materialPreset ?? null,
|
||||||
|
})
|
||||||
|
const cached = slabMaterialCache.get(cacheKey)
|
||||||
|
if (cached) return cached
|
||||||
|
|
||||||
|
const preset = getMaterialPresetByRef(node.materialPreset)
|
||||||
|
const material = preset
|
||||||
|
? new MeshStandardMaterial()
|
||||||
|
: node.material
|
||||||
|
? createMaterial(node.material).clone()
|
||||||
|
: DEFAULT_SLAB_MATERIAL.clone()
|
||||||
|
|
||||||
|
if (preset) {
|
||||||
|
applyMaterialPresetToMaterials(material, preset)
|
||||||
|
}
|
||||||
|
|
||||||
|
material.transparent = false
|
||||||
|
material.opacity = 1
|
||||||
|
material.alphaMap = null
|
||||||
|
material.side = DoubleSide
|
||||||
|
material.depthWrite = true
|
||||||
|
material.needsUpdate = true
|
||||||
|
|
||||||
|
slabMaterialCache.set(cacheKey, material)
|
||||||
|
return material
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildSlabGeometry(node: SlabNode): Group {
|
||||||
|
const group = new Group()
|
||||||
|
const geometry = generateSlabGeometry(node)
|
||||||
|
const material = getSlabMaterial(node)
|
||||||
|
const mesh = new Mesh(geometry, material)
|
||||||
|
mesh.castShadow = true
|
||||||
|
mesh.receiveShadow = true
|
||||||
|
group.add(mesh)
|
||||||
|
return group
|
||||||
|
}
|
||||||
@@ -1,116 +0,0 @@
|
|||||||
'use client'
|
|
||||||
|
|
||||||
import { getMaterialPresetByRef, type SlabNode, useRegistry, useScene } from '@pascal-app/core'
|
|
||||||
import {
|
|
||||||
applyMaterialPresetToMaterials,
|
|
||||||
createMaterial,
|
|
||||||
DEFAULT_SLAB_MATERIAL,
|
|
||||||
useNodeEvents,
|
|
||||||
} from '@pascal-app/viewer'
|
|
||||||
import { useEffect, useLayoutEffect, useMemo, useRef } from 'react'
|
|
||||||
import type { Mesh } from 'three'
|
|
||||||
import * as THREE from 'three'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Thin slab renderer. Mounts a placeholder mesh, registers it with
|
|
||||||
* `sceneRegistry`, and marks the node dirty so `SlabSystem` fills the
|
|
||||||
* geometry next frame.
|
|
||||||
*
|
|
||||||
* Behaviorally identical to the legacy `SlabRenderer` in
|
|
||||||
* `@pascal-app/viewer/components/renderers/slab/slab-renderer.tsx` —
|
|
||||||
* same placeholder geometry, same material cache, same render output.
|
|
||||||
*
|
|
||||||
* Material logic is preserved from legacy: slab can carry either a raw
|
|
||||||
* `material` or a `materialPreset` (preset takes precedence; preset
|
|
||||||
* apply mutates the cached material instance so async texture loads
|
|
||||||
* still hit the rendered mesh on re-mount).
|
|
||||||
*
|
|
||||||
* No `def.geometry` yet — slab polygon geometry depends on holes +
|
|
||||||
* triangulation that lives inside `SlabSystem`'s useFrame body. Future
|
|
||||||
* milestone can extract a pure builder if useful, but the system is
|
|
||||||
* already efficient (rebuilds only dirty nodes); no urgency.
|
|
||||||
*/
|
|
||||||
const slabMaterialCache = new Map<string, THREE.MeshStandardMaterial>()
|
|
||||||
|
|
||||||
function createEmptyGeometry() {
|
|
||||||
const geometry = new THREE.BufferGeometry()
|
|
||||||
geometry.setAttribute('position', new THREE.Float32BufferAttribute([], 3))
|
|
||||||
return geometry
|
|
||||||
}
|
|
||||||
|
|
||||||
function getSlabMaterial(
|
|
||||||
cacheKey: string,
|
|
||||||
params: { material?: SlabNode['material']; materialPreset?: string },
|
|
||||||
) {
|
|
||||||
const cached = slabMaterialCache.get(cacheKey)
|
|
||||||
if (cached) return cached
|
|
||||||
|
|
||||||
const preset = getMaterialPresetByRef(params.materialPreset)
|
|
||||||
const slabMaterial = preset
|
|
||||||
? new THREE.MeshStandardMaterial()
|
|
||||||
: params.material
|
|
||||||
? createMaterial(params.material).clone()
|
|
||||||
: DEFAULT_SLAB_MATERIAL.clone()
|
|
||||||
|
|
||||||
if (preset) {
|
|
||||||
applyMaterialPresetToMaterials(slabMaterial, preset)
|
|
||||||
}
|
|
||||||
|
|
||||||
slabMaterial.transparent = false
|
|
||||||
slabMaterial.opacity = 1
|
|
||||||
slabMaterial.alphaMap = null
|
|
||||||
slabMaterial.side = THREE.DoubleSide
|
|
||||||
slabMaterial.depthWrite = true
|
|
||||||
slabMaterial.needsUpdate = true
|
|
||||||
|
|
||||||
slabMaterialCache.set(cacheKey, slabMaterial)
|
|
||||||
return slabMaterial
|
|
||||||
}
|
|
||||||
|
|
||||||
const SlabRenderer = ({ node }: { node: SlabNode }) => {
|
|
||||||
const ref = useRef<Mesh>(null!)
|
|
||||||
const placeholderGeometry = useMemo(createEmptyGeometry, [])
|
|
||||||
const handlers = useNodeEvents(node, 'slab')
|
|
||||||
|
|
||||||
useRegistry(node.id, 'slab', ref)
|
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
|
||||||
useScene.getState().markDirty(node.id)
|
|
||||||
}, [node.id])
|
|
||||||
|
|
||||||
useEffect(() => () => placeholderGeometry.dispose(), [placeholderGeometry])
|
|
||||||
|
|
||||||
const material = useMemo(() => {
|
|
||||||
const resolvedMaterial = node.material
|
|
||||||
const resolvedMaterialPreset = node.materialPreset
|
|
||||||
const cacheKey = JSON.stringify({
|
|
||||||
material: resolvedMaterial ?? null,
|
|
||||||
materialPreset: resolvedMaterialPreset ?? null,
|
|
||||||
})
|
|
||||||
|
|
||||||
return getSlabMaterial(cacheKey, {
|
|
||||||
material: resolvedMaterial,
|
|
||||||
materialPreset: resolvedMaterialPreset,
|
|
||||||
})
|
|
||||||
}, [
|
|
||||||
node.material,
|
|
||||||
node.material?.preset,
|
|
||||||
node.material?.properties,
|
|
||||||
node.material?.texture,
|
|
||||||
node.materialPreset,
|
|
||||||
])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<mesh
|
|
||||||
castShadow
|
|
||||||
geometry={placeholderGeometry}
|
|
||||||
material={material}
|
|
||||||
receiveShadow
|
|
||||||
ref={ref}
|
|
||||||
visible={node.visible}
|
|
||||||
{...handlers}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default SlabRenderer
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
'use client'
|
|
||||||
|
|
||||||
import { SlabSystem } from '@pascal-app/viewer'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Registry-driven slab system bundle. Re-exports the legacy `SlabSystem`
|
|
||||||
* (still in viewer) so it mounts via `RegisteredSystems` when slab is
|
|
||||||
* registry-driven. `<LegacySystem kind="slab">` in viewer/components/
|
|
||||||
* viewer/index.tsx short-circuits whenever `nodeRegistry.has('slab')`
|
|
||||||
* is true — same shape wall and fence use.
|
|
||||||
*
|
|
||||||
* Future Phase 5+: extract polygon triangulation + hole CSG into a pure
|
|
||||||
* `buildSlabGeometry(node)` and migrate to `def.geometry`. The legacy
|
|
||||||
* system body has it well-isolated; should be a clean extraction.
|
|
||||||
*/
|
|
||||||
const SlabSystems = () => {
|
|
||||||
return <SlabSystem />
|
|
||||||
}
|
|
||||||
|
|
||||||
export default SlabSystems
|
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { NodeDefinition } from '@pascal-app/core'
|
import type { NodeDefinition } from '@pascal-app/core'
|
||||||
|
import { buildSpawnFloorplan } from './floorplan'
|
||||||
import { spawnParametrics } from './parametrics'
|
import { spawnParametrics } from './parametrics'
|
||||||
import { SpawnNode } from './schema'
|
import { SpawnNode } from './schema'
|
||||||
|
|
||||||
@@ -34,12 +35,14 @@ export const spawnDefinition: NodeDefinition<typeof SpawnNode> = {
|
|||||||
kind: 'parametric',
|
kind: 'parametric',
|
||||||
module: () => import('./renderer'),
|
module: () => import('./renderer'),
|
||||||
},
|
},
|
||||||
// `floorplan: buildSpawnFloorplan` deferred — spawn already renders in
|
// Stage C migration: floor-plan rendering via def.floorplan.
|
||||||
// the legacy floorplan-panel.tsx via `floorplanSpawnEntries`. Adding it
|
// floorplan-panel.tsx's `floorplanSpawnEntries` short-circuits to []
|
||||||
// here would double-render. The pure builder lives in
|
// when `nodeRegistry.has('spawn')`, so this builder is the single
|
||||||
// ./floorplan.ts ready to wire when the legacy inline branch is
|
// path. FloorplanRegistryLayer renders + handles click-to-select;
|
||||||
// removed (Phase 5 spawn-floorplan migration PR — same shape as the
|
// FloorplanRegistryActionMenu handles move / duplicate (disabled) /
|
||||||
// wall feature flag, but per kind in the legacy panel itself).
|
// delete. Legacy spawn click handlers in FloorplanNodeLayer become
|
||||||
|
// dead code once Phase 6 cleanup removes the [] entries path.
|
||||||
|
floorplan: buildSpawnFloorplan,
|
||||||
tool: () => import('./tool'),
|
tool: () => import('./tool'),
|
||||||
toolHints: [
|
toolHints: [
|
||||||
{ key: 'Left click', label: 'Place spawn point' },
|
{ key: 'Left click', label: 'Place spawn point' },
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ export { DoorSystem } from './systems/door/door-system'
|
|||||||
// Fence system follows the wall re-export pattern — composed into the
|
// Fence system follows the wall re-export pattern — composed into the
|
||||||
// registry-driven fence definition's `def.system`. Removed in Phase 6
|
// registry-driven fence definition's `def.system`. Removed in Phase 6
|
||||||
// alongside the legacy fence mount point.
|
// alongside the legacy fence mount point.
|
||||||
export { FenceSystem } from './systems/fence/fence-system'
|
export { FenceSystem, generateFenceGeometry } from './systems/fence/fence-system'
|
||||||
export { InteractiveSystem } from './systems/interactive/interactive-system'
|
export { InteractiveSystem } from './systems/interactive/interactive-system'
|
||||||
// Item systems for the registry-driven item definition. ItemSystem
|
// Item systems for the registry-driven item definition. ItemSystem
|
||||||
// applies attachTo-driven transforms each frame; ItemLightSystem
|
// applies attachTo-driven transforms each frame; ItemLightSystem
|
||||||
@@ -61,7 +61,7 @@ export { getRoofMaterialArray } from './systems/roof/roof-materials'
|
|||||||
// Slab system follows the wall + fence re-export pattern — composed into
|
// Slab system follows the wall + fence re-export pattern — composed into
|
||||||
// the registry-driven slab definition's `def.system`. Removed in Phase 6
|
// the registry-driven slab definition's `def.system`. Removed in Phase 6
|
||||||
// alongside the legacy slab mount point.
|
// alongside the legacy slab mount point.
|
||||||
export { SlabSystem } from './systems/slab/slab-system'
|
export { generateSlabGeometry, SlabSystem } from './systems/slab/slab-system'
|
||||||
export { getStairBodyMaterials, getStairRailingMaterial } from './systems/stair/stair-materials'
|
export { getStairBodyMaterials, getStairRailingMaterial } from './systems/stair/stair-materials'
|
||||||
export { WallCutout } from './systems/wall/wall-cutout'
|
export { WallCutout } from './systems/wall/wall-cutout'
|
||||||
export { getVisibleWallMaterials } from './systems/wall/wall-materials'
|
export { getVisibleWallMaterials } from './systems/wall/wall-materials'
|
||||||
|
|||||||
@@ -242,7 +242,7 @@ function createFenceParts(fence: FenceNode): FencePart[] {
|
|||||||
return parts
|
return parts
|
||||||
}
|
}
|
||||||
|
|
||||||
function generateFenceGeometry(fence: FenceNode) {
|
export function generateFenceGeometry(fence: FenceNode) {
|
||||||
const parts = createFenceParts(fence)
|
const parts = createFenceParts(fence)
|
||||||
const geometries = parts.map(createFencePartGeometry)
|
const geometries = parts.map(createFencePartGeometry)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user