Feat/stairs fence update (#226)

* feat: railing on the straight stairs and new fence

* feat: added spiral and curved stairs with bug fix for fence

* feat:fence are linked to each other ... so moving one move the other sharing the same coordinate

* fix: update stair railing logic to include front-side attachments for terminal landings

* Integrate fence rendering into the fence system

* fix: pass nodeId instead of undefined node to WallTreeNode and FenceTreeNode

TreeNode was passing `node` (undefined variable) instead of `nodeId` to
WallTreeNode and FenceTreeNode, causing a runtime ReferenceError.
Updated FenceTreeNode to accept nodeId and look up the node from the
scene store internally, consistent with all other tree node components.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: update fence icon with new isometric design

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Aymeric Rabot <aymeric@pascal.app>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Sudhir Yadav
2026-04-13 23:31:57 -04:00
committed by GitHub
co-authored by Claude Opus 4.6 Aymeric Rabot
parent 682e2a1a12
commit a205e4f778
42 changed files with 2774 additions and 92 deletions
+2 -1
View File
@@ -15,6 +15,7 @@ export {
export { BuildingNode } from './nodes/building'
export { CeilingNode } from './nodes/ceiling'
export { DoorNode, DoorSegment } from './nodes/door'
export { FenceBaseStyle, FenceNode, FenceStyle } from './nodes/fence'
export { GuideNode } from './nodes/guide'
export type {
AnimationEffect,
@@ -36,7 +37,7 @@ export { ScanNode } from './nodes/scan'
// Nodes
export { SiteNode } from './nodes/site'
export { SlabNode } from './nodes/slab'
export { StairNode } from './nodes/stair'
export { StairNode, StairRailingMode, StairTopLandingMode, StairType } from './nodes/stair'
export { AttachmentSide, StairSegmentNode, StairSegmentType } from './nodes/stair-segment'
export { WallNode } from './nodes/wall'
export { WindowNode } from './nodes/window'
+35
View File
@@ -0,0 +1,35 @@
import dedent from 'dedent'
import { z } from 'zod'
import { BaseNode, nodeType, objectId } from '../base'
export const FenceStyle = z.enum(['slat', 'rail', 'privacy'])
export const FenceBaseStyle = z.enum(['floating', 'grounded'])
export const FenceNode = BaseNode.extend({
id: objectId('fence'),
type: nodeType('fence'),
start: z.tuple([z.number(), z.number()]),
end: z.tuple([z.number(), z.number()]),
height: z.number().default(1.8),
thickness: z.number().default(0.08),
baseHeight: z.number().default(0.22),
postSpacing: z.number().default(2),
postSize: z.number().default(0.1),
topRailHeight: z.number().default(0.04),
groundClearance: z.number().default(0),
edgeInset: z.number().default(0.015),
baseStyle: FenceBaseStyle.default('grounded'),
color: z.string().default('#ffffff'),
style: FenceStyle.default('slat'),
}).describe(
dedent`
Fence node - used to represent a fence segment in the building/site level coordinate system
- start/end: fence endpoints in level coordinate system
- height/thickness: overall fence dimensions in meters
- baseHeight/postSpacing/postSize/topRailHeight: exact geometric controls from the plan3D fence model
- groundClearance/edgeInset/baseStyle: fence support and inset configuration
- color/style: visual appearance options
`,
)
export type FenceNode = z.infer<typeof FenceNode>
+2
View File
@@ -2,6 +2,7 @@ import dedent from 'dedent'
import { z } from 'zod'
import { BaseNode, nodeType, objectId } from '../base'
import { CeilingNode } from './ceiling'
import { FenceNode } from './fence'
import { GuideNode } from './guide'
import { RoofNode } from './roof'
import { ScanNode } from './scan'
@@ -17,6 +18,7 @@ export const LevelNode = BaseNode.extend({
.array(
z.union([
WallNode.shape.id,
FenceNode.shape.id,
ZoneNode.shape.id,
SlabNode.shape.id,
CeilingNode.shape.id,
+39 -3
View File
@@ -4,6 +4,14 @@ import { BaseNode, nodeType, objectId } from '../base'
import { MaterialSchema } from '../material'
import { StairSegmentNode } from './stair-segment'
export const StairRailingMode = z.enum(['none', 'left', 'right', 'both'])
export const StairType = z.enum(['straight', 'curved', 'spiral'])
export const StairTopLandingMode = z.enum(['none', 'integrated'])
export type StairRailingMode = z.infer<typeof StairRailingMode>
export type StairType = z.infer<typeof StairType>
export type StairTopLandingMode = z.infer<typeof StairTopLandingMode>
export const StairNode = BaseNode.extend({
id: objectId('stair'),
type: nodeType('stair'),
@@ -11,16 +19,44 @@ export const StairNode = BaseNode.extend({
position: z.tuple([z.number(), z.number(), z.number()]).default([0, 0, 0]),
// Rotation around Y axis in radians
rotation: z.number().default(0),
stairType: StairType.default('straight'),
width: z.number().default(1.0),
totalRise: z.number().default(2.5),
stepCount: z.number().default(10),
thickness: z.number().default(0.25),
fillToFloor: z.boolean().default(true),
innerRadius: z.number().default(0.9),
sweepAngle: z.number().default(Math.PI / 2),
topLandingMode: StairTopLandingMode.default('none'),
topLandingDepth: z.number().default(0.9),
showCenterColumn: z.boolean().default(true),
showStepSupports: z.boolean().default(true),
railingMode: StairRailingMode.default('none'),
railingHeight: z.number().default(0.92),
// Child stair segment IDs
children: z.array(StairSegmentNode.shape.id).default([]),
}).describe(
dedent`
Stair node - a container for stair segments.
Acts as a group that holds one or more StairSegmentNodes (flights and landings).
Segments chain together based on their attachmentSide to form complex staircase shapes.
Acts as a group that either holds one or more StairSegmentNodes (straight stairs)
or stores stair-level geometry properties for curved stairs.
- position: center position of the stair group
- rotation: rotation around Y axis
- children: array of StairSegmentNode IDs
- stairType: straight (segment-based), curved (arc-based), or spiral
- width: stair width
- totalRise: total stair height
- stepCount: number of visible steps
- thickness: stair slab / tread thickness
- fillToFloor: whether the stair mass fills down to the floor or uses tread thickness only
- innerRadius: inner curve radius for curved stairs
- sweepAngle: total curved stair sweep in radians
- topLandingMode: optional integrated top landing for spiral stairs
- topLandingDepth: depth used to size the integrated spiral top landing
- showCenterColumn: whether spiral stairs render a center column
- showStepSupports: whether spiral stairs render step support brackets
- railingMode: whether to render railings and on which side(s)
- railingHeight: top height of the railing above the stair surface
- children: array of StairSegmentNode IDs for straight stairs
`,
)
+2
View File
@@ -2,6 +2,7 @@ import z from 'zod'
import { BuildingNode } from './nodes/building'
import { CeilingNode } from './nodes/ceiling'
import { DoorNode } from './nodes/door'
import { FenceNode } from './nodes/fence'
import { GuideNode } from './nodes/guide'
import { ItemNode } from './nodes/item'
import { LevelNode } from './nodes/level'
@@ -21,6 +22,7 @@ export const AnyNode = z.discriminatedUnion('type', [
BuildingNode,
LevelNode,
WallNode,
FenceNode,
ItemNode,
ZoneNode,
SlabNode,