feat: stair system, scene graph utilities, read-only mode, viewer state improvements (#210)
Stair system (full stack): - New StairNode + StairSegmentNode schemas with flights, landings, L/U-shapes - StairSystem: geometry generation with throttled per-frame updates - Stair tool, edit system, panels, tree node, and renderers - Event bus types, scene registry, and command palette entries Scene graph utilities: - cloneLevelSubtree: deep-clone a level with remapped IDs - forkSceneGraph: clone + strip scan/guide nodes for project forking Core improvements: - Read-only mode on scene store (blocks create/update/delete when locked) - readOnly guards on node-actions and collection actions - Upload store for scan/guide file upload handling Viewer state: - previewSelectedIds for box-select live preview - hoverHighlightMode (default/delete) for delete-mode hover outline
This commit is contained in:
@@ -36,6 +36,8 @@ export { ScanNode } from './nodes/scan'
|
||||
// Nodes
|
||||
export { SiteNode } from './nodes/site'
|
||||
export { SlabNode } from './nodes/slab'
|
||||
export { StairNode } from './nodes/stair'
|
||||
export { AttachmentSide, StairSegmentNode, StairSegmentType } from './nodes/stair-segment'
|
||||
export { WallNode } from './nodes/wall'
|
||||
export { WindowNode } from './nodes/window'
|
||||
export { ZoneNode } from './nodes/zone'
|
||||
|
||||
@@ -6,6 +6,7 @@ import { GuideNode } from './guide'
|
||||
import { RoofNode } from './roof'
|
||||
import { ScanNode } from './scan'
|
||||
import { SlabNode } from './slab'
|
||||
import { StairNode } from './stair'
|
||||
import { WallNode } from './wall'
|
||||
import { ZoneNode } from './zone'
|
||||
|
||||
@@ -20,6 +21,7 @@ export const LevelNode = BaseNode.extend({
|
||||
SlabNode.shape.id,
|
||||
CeilingNode.shape.id,
|
||||
RoofNode.shape.id,
|
||||
StairNode.shape.id,
|
||||
ScanNode.shape.id,
|
||||
GuideNode.shape.id,
|
||||
]),
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import dedent from 'dedent'
|
||||
import { z } from 'zod'
|
||||
import { BaseNode, nodeType, objectId } from '../base'
|
||||
import { MaterialSchema } from '../material'
|
||||
|
||||
export const StairSegmentType = z.enum(['stair', 'landing'])
|
||||
|
||||
export type StairSegmentType = z.infer<typeof StairSegmentType>
|
||||
|
||||
export const AttachmentSide = z.enum(['front', 'left', 'right'])
|
||||
|
||||
export type AttachmentSide = z.infer<typeof AttachmentSide>
|
||||
|
||||
export const StairSegmentNode = BaseNode.extend({
|
||||
id: objectId('sseg'),
|
||||
type: nodeType('stair-segment'),
|
||||
material: MaterialSchema.optional(),
|
||||
position: z.tuple([z.number(), z.number(), z.number()]).default([0, 0, 0]),
|
||||
// Rotation around Y axis in radians
|
||||
rotation: z.number().default(0),
|
||||
// Stair or landing
|
||||
segmentType: StairSegmentType.default('stair'),
|
||||
// Width of the stair flight / landing
|
||||
width: z.number().default(1.0),
|
||||
// Horizontal run (depth along travel direction)
|
||||
length: z.number().default(3.0),
|
||||
// Vertical rise (0 for landings)
|
||||
height: z.number().default(2.5),
|
||||
// Number of steps (only used for stair type)
|
||||
stepCount: z.number().default(10),
|
||||
// Which side of the previous segment to attach to
|
||||
attachmentSide: AttachmentSide.default('front'),
|
||||
// Whether to fill the underside down to floor level
|
||||
fillToFloor: z.boolean().default(true),
|
||||
// Thickness of the stair slab when not filled to floor
|
||||
thickness: z.number().default(0.25),
|
||||
}).describe(
|
||||
dedent`
|
||||
Stair segment node - an individual flight or landing within a stair group.
|
||||
Each segment generates a complete stair/landing geometry.
|
||||
Multiple segments chain together to form complex staircase shapes (L-shape, U-shape, etc.).
|
||||
- segmentType: stair (with steps) or landing (flat platform)
|
||||
- width: width of the flight/landing
|
||||
- length: horizontal run distance
|
||||
- height: vertical rise (0 for landings)
|
||||
- stepCount: number of steps (stair type only)
|
||||
- attachmentSide: front, left, or right - which side of the previous segment to attach to
|
||||
- fillToFloor: whether to fill the underside down to the absolute floor level
|
||||
- thickness: slab thickness when not filled to floor
|
||||
`,
|
||||
)
|
||||
|
||||
export type StairSegmentNode = z.infer<typeof StairSegmentNode>
|
||||
@@ -0,0 +1,27 @@
|
||||
import dedent from 'dedent'
|
||||
import { z } from 'zod'
|
||||
import { BaseNode, nodeType, objectId } from '../base'
|
||||
import { MaterialSchema } from '../material'
|
||||
import { StairSegmentNode } from './stair-segment'
|
||||
|
||||
export const StairNode = BaseNode.extend({
|
||||
id: objectId('stair'),
|
||||
type: nodeType('stair'),
|
||||
material: MaterialSchema.optional(),
|
||||
position: z.tuple([z.number(), z.number(), z.number()]).default([0, 0, 0]),
|
||||
// Rotation around Y axis in radians
|
||||
rotation: z.number().default(0),
|
||||
// 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.
|
||||
- position: center position of the stair group
|
||||
- rotation: rotation around Y axis
|
||||
- children: array of StairSegmentNode IDs
|
||||
`,
|
||||
)
|
||||
|
||||
export type StairNode = z.infer<typeof StairNode>
|
||||
@@ -10,6 +10,8 @@ import { RoofSegmentNode } from './nodes/roof-segment'
|
||||
import { ScanNode } from './nodes/scan'
|
||||
import { SiteNode } from './nodes/site'
|
||||
import { SlabNode } from './nodes/slab'
|
||||
import { StairNode } from './nodes/stair'
|
||||
import { StairSegmentNode } from './nodes/stair-segment'
|
||||
import { WallNode } from './nodes/wall'
|
||||
import { WindowNode } from './nodes/window'
|
||||
import { ZoneNode } from './nodes/zone'
|
||||
@@ -25,6 +27,8 @@ export const AnyNode = z.discriminatedUnion('type', [
|
||||
CeilingNode,
|
||||
RoofNode,
|
||||
RoofSegmentNode,
|
||||
StairNode,
|
||||
StairSegmentNode,
|
||||
ScanNode,
|
||||
GuideNode,
|
||||
WindowNode,
|
||||
|
||||
Reference in New Issue
Block a user