From 3794e33242ad21bd7630e35888ceffa5f277afaa Mon Sep 17 00:00:00 2001 From: sudhir Date: Mon, 13 Apr 2026 11:47:25 +0530 Subject: [PATCH 1/7] feat: railing on the straight stairs and new fence --- bun.lock | 4 +- packages/core/src/events/bus.ts | 3 + .../hooks/scene-registry/scene-registry.ts | 1 + packages/core/src/index.ts | 1 + packages/core/src/schema/index.ts | 3 +- packages/core/src/schema/nodes/fence.ts | 35 ++ packages/core/src/schema/nodes/level.ts | 2 + packages/core/src/schema/nodes/stair.ts | 8 + packages/core/src/schema/types.ts | 2 + .../core/src/systems/stair/stair-system.tsx | 553 +++++++++++++++++- .../editor/floating-action-menu.tsx | 8 + .../components/editor/selection-manager.tsx | 8 + .../components/tools/fence/fence-drafting.ts | 125 ++++ .../src/components/tools/fence/fence-tool.tsx | 190 ++++++ .../tools/fence/move-fence-tool.tsx | 129 ++++ .../src/components/tools/item/move-tool.tsx | 3 + .../tools/select/box-select-tool.tsx | 2 +- .../components/tools/stair/stair-defaults.ts | 2 + .../src/components/tools/stair/stair-tool.tsx | 4 + .../src/components/tools/tool-manager.tsx | 2 + .../ui/action-menu/structure-tools.tsx | 1 + .../src/components/ui/panels/fence-panel.tsx | 184 ++++++ .../components/ui/panels/panel-manager.tsx | 3 + .../src/components/ui/panels/stair-panel.tsx | 29 + .../editor/src/components/viewer-overlay.tsx | 1 + .../editor/src/hooks/use-contextual-tools.ts | 12 +- packages/editor/src/store/use-editor.tsx | 4 + .../renderers/fence/fence-renderer.tsx | 135 +++++ .../components/renderers/node-renderer.tsx | 2 + .../renderers/stair/stair-renderer.tsx | 503 +++++++++++++++- .../components/viewer/selection-manager.tsx | 12 +- packages/viewer/src/hooks/use-node-events.ts | 3 + 32 files changed, 1957 insertions(+), 17 deletions(-) create mode 100644 packages/core/src/schema/nodes/fence.ts create mode 100644 packages/editor/src/components/tools/fence/fence-drafting.ts create mode 100644 packages/editor/src/components/tools/fence/fence-tool.tsx create mode 100644 packages/editor/src/components/tools/fence/move-fence-tool.tsx create mode 100644 packages/editor/src/components/ui/panels/fence-panel.tsx create mode 100644 packages/viewer/src/components/renderers/fence/fence-renderer.tsx diff --git a/bun.lock b/bun.lock index 8778a38c..35f9e541 100644 --- a/bun.lock +++ b/bun.lock @@ -48,7 +48,7 @@ }, "packages/core": { "name": "@pascal-app/core", - "version": "0.4.0", + "version": "0.5.0", "dependencies": { "dedent": "^1.7.1", "idb-keyval": "^6.2.2", @@ -166,7 +166,7 @@ }, "packages/viewer": { "name": "@pascal-app/viewer", - "version": "0.4.0", + "version": "0.5.0", "dependencies": { "polygon-clipping": "^0.15.7", "zustand": "^5", diff --git a/packages/core/src/events/bus.ts b/packages/core/src/events/bus.ts index 5592be3d..18bf9aed 100644 --- a/packages/core/src/events/bus.ts +++ b/packages/core/src/events/bus.ts @@ -4,6 +4,7 @@ import type { BuildingNode, CeilingNode, DoorNode, + FenceNode, ItemNode, LevelNode, RoofNode, @@ -41,6 +42,7 @@ export interface NodeEvent { } export type WallEvent = NodeEvent +export type FenceEvent = NodeEvent export type ItemEvent = NodeEvent export type SiteEvent = NodeEvent export type BuildingEvent = NodeEvent @@ -111,6 +113,7 @@ type ThumbnailEvents = { type EditorEvents = GridEvents & NodeEvents<'wall', WallEvent> & + NodeEvents<'fence', FenceEvent> & NodeEvents<'item', ItemEvent> & NodeEvents<'site', SiteEvent> & NodeEvents<'building', BuildingEvent> & diff --git a/packages/core/src/hooks/scene-registry/scene-registry.ts b/packages/core/src/hooks/scene-registry/scene-registry.ts index d6b2c0f0..c44e7909 100644 --- a/packages/core/src/hooks/scene-registry/scene-registry.ts +++ b/packages/core/src/hooks/scene-registry/scene-registry.ts @@ -15,6 +15,7 @@ export const sceneRegistry = { ceiling: new Set(), level: new Set(), wall: new Set(), + fence: new Set(), item: new Set(), slab: new Set(), zone: new Set(), diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 98f66d5a..a91c8259 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -4,6 +4,7 @@ export type { CeilingEvent, DoorEvent, EventSuffix, + FenceEvent, GridEvent, ItemEvent, LevelEvent, diff --git a/packages/core/src/schema/index.ts b/packages/core/src/schema/index.ts index 927c48a5..51e1bffb 100644 --- a/packages/core/src/schema/index.ts +++ b/packages/core/src/schema/index.ts @@ -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 } from './nodes/stair' export { AttachmentSide, StairSegmentNode, StairSegmentType } from './nodes/stair-segment' export { WallNode } from './nodes/wall' export { WindowNode } from './nodes/window' diff --git a/packages/core/src/schema/nodes/fence.ts b/packages/core/src/schema/nodes/fence.ts new file mode 100644 index 00000000..6f7fd213 --- /dev/null +++ b/packages/core/src/schema/nodes/fence.ts @@ -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.04), + 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 diff --git a/packages/core/src/schema/nodes/level.ts b/packages/core/src/schema/nodes/level.ts index 9a834750..4761161c 100644 --- a/packages/core/src/schema/nodes/level.ts +++ b/packages/core/src/schema/nodes/level.ts @@ -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, diff --git a/packages/core/src/schema/nodes/stair.ts b/packages/core/src/schema/nodes/stair.ts index 2901771e..f17780b7 100644 --- a/packages/core/src/schema/nodes/stair.ts +++ b/packages/core/src/schema/nodes/stair.ts @@ -4,6 +4,10 @@ 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 type StairRailingMode = z.infer + export const StairNode = BaseNode.extend({ id: objectId('stair'), type: nodeType('stair'), @@ -11,6 +15,8 @@ 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), + railingMode: StairRailingMode.default('none'), + railingHeight: z.number().default(0.92), // Child stair segment IDs children: z.array(StairSegmentNode.shape.id).default([]), }).describe( @@ -20,6 +26,8 @@ export const StairNode = BaseNode.extend({ Segments chain together based on their attachmentSide to form complex staircase shapes. - position: center position of the stair group - rotation: rotation around Y axis + - 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 `, ) diff --git a/packages/core/src/schema/types.ts b/packages/core/src/schema/types.ts index d7b3538e..1b17a6b0 100644 --- a/packages/core/src/schema/types.ts +++ b/packages/core/src/schema/types.ts @@ -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, diff --git a/packages/core/src/systems/stair/stair-system.tsx b/packages/core/src/systems/stair/stair-system.tsx index d85a8c14..01deba6b 100644 --- a/packages/core/src/systems/stair/stair-system.tsx +++ b/packages/core/src/systems/stair/stair-system.tsx @@ -310,9 +310,7 @@ function updateMergedStairGeometry( .filter((n): n is StairSegmentNode => n?.type === 'stair-segment') if (segments.length === 0) { - mergedMesh.geometry.dispose() - mergedMesh.geometry = new THREE.BufferGeometry() - mergedMesh.geometry.setAttribute('position', new THREE.Float32BufferAttribute([], 3)) + replaceMeshGeometry(mergedMesh, createEmptyGeometry()) return } @@ -337,11 +335,8 @@ function updateMergedStairGeometry( geometries.push(geo) } - const merged = mergeGeometries(geometries, false) - if (merged) { - mergedMesh.geometry.dispose() - mergedMesh.geometry = merged - } + const merged = mergeGeometries(geometries, false) ?? createEmptyGeometry() + replaceMeshGeometry(mergedMesh, merged) // Dispose individual geometries for (const geo of geometries) { @@ -416,6 +411,548 @@ function rotateXZ(x: number, z: number, angle: number): [number, number] { return [x * cos + z * sin, -x * sin + z * cos] } +function createEmptyGeometry(): THREE.BufferGeometry { + const geometry = new THREE.BufferGeometry() + geometry.setAttribute('position', new THREE.Float32BufferAttribute([], 3)) + return geometry +} + +function replaceMeshGeometry(mesh: THREE.Mesh, geometry: THREE.BufferGeometry) { + mesh.geometry.dispose() + mesh.geometry = geometry +} + +type StairRailSide = 'left' | 'right' +type StairRailPathSide = StairRailSide | 'front' +type StairRailSidePath = { + side: StairRailPathSide + points: THREE.Vector3[] +} +type StairSegmentRailPath = { + segment: StairSegmentNode + sidePaths: StairRailSidePath[] + connectFromPrevious: boolean +} +type StairRailLayout = { + center: [number, number] + elevation: number + rotation: number + segment: StairSegmentNode +} + +function generateStairRailingGeometry( + stairNode: StairNode, + segments: StairSegmentNode[], + transforms: SegmentTransform[], +): THREE.BufferGeometry { + const railingMode = stairNode.railingMode ?? 'none' + if (railingMode === 'none') { + return createEmptyGeometry() + } + + const railHeight = Math.max(0.5, stairNode.railingHeight ?? 0.92) + const midRailHeight = Math.max(railHeight * 0.45, 0.35) + const railRadius = 0.022 + const postRadius = 0.018 + const inset = 0.06 + const landingInset = 0.08 + const geometries: THREE.BufferGeometry[] = [] + + const segmentRailPaths = buildStairRailPaths(segments, transforms, railingMode, inset, landingInset) + + for (const segmentRailPath of segmentRailPaths) { + for (const sidePath of segmentRailPath.sidePaths) { + const points = sidePath.points + if (points.length === 0) continue + + geometries.push(...buildBalusterGeometries(points, railHeight, postRadius)) + geometries.push(...buildOffsetRailSegmentGeometries(points, railHeight, railRadius)) + geometries.push( + ...buildOffsetRailSegmentGeometries(points, midRailHeight, railRadius * 0.8), + ) + } + } + + for (let index = 1; index < segmentRailPaths.length; index++) { + const previousPath = segmentRailPaths[index - 1] + const currentPath = segmentRailPaths[index] + if (!(previousPath && currentPath && currentPath.connectFromPrevious)) continue + if (previousPath.segment.segmentType === 'landing') continue + + for (const sidePath of currentPath.sidePaths) { + if (currentPath.segment.segmentType === 'landing') continue + const currentPoint = sidePath.points[0] + if (!currentPoint) continue + + const previousSidePath = [...previousPath.sidePaths] + .map((entry) => ({ + entry, + distance: entry.points.length + ? entry.points[entry.points.length - 1]!.distanceTo(currentPoint) + : Number.POSITIVE_INFINITY, + })) + .sort((left, right) => left.distance - right.distance)[0]?.entry + + const previousPoint = + previousSidePath && previousSidePath.points.length > 0 + ? previousSidePath.points[previousSidePath.points.length - 1] + : null + + if (!(previousPoint && currentPoint)) continue + + const connectorPoints = [previousPoint, currentPoint] + geometries.push(...buildOffsetRailSegmentGeometries(connectorPoints, railHeight, railRadius)) + geometries.push( + ...buildOffsetRailSegmentGeometries(connectorPoints, midRailHeight, railRadius * 0.8), + ) + } + } + + const merged = mergeGeometries(geometries, false) ?? createEmptyGeometry() + for (const geometry of geometries) { + geometry.dispose() + } + + return merged +} + +function buildStairRailPaths( + segments: StairSegmentNode[], + transforms: SegmentTransform[], + railingMode: 'left' | 'right' | 'both', + inset: number, + landingInset: number, +): StairSegmentRailPath[] { + const layouts = computeStairRailLayouts(segments, transforms) + + if (railingMode === 'both') { + const isStraightLineDoubleLandingLayout = + segments.length === 4 && + segments[0]?.segmentType === 'stair' && + segments[1]?.segmentType === 'landing' && + segments[2]?.segmentType === 'stair' && + segments[2]?.attachmentSide === 'front' && + segments[3]?.segmentType === 'landing' && + segments[3]?.attachmentSide === 'front' + + return layouts.map((layout, index) => { + const segment = layout.segment + const previousSegment = index > 0 ? segments[index - 1] : undefined + const nextSegment = index < segments.length - 1 ? segments[index + 1] : undefined + const hideLandingRailing = + segment.segmentType === 'landing' && + previousSegment?.segmentType === 'stair' && + nextSegment?.segmentType === 'stair' + const visualTurnSide = nextSegment?.attachmentSide + const sideCandidates = + hideLandingRailing + ? visualTurnSide === 'left' + ? (['front', 'right'] as const) + : visualTurnSide === 'right' + ? (['front', 'left'] as const) + : (['left', 'right'] as const) + : segment.segmentType === 'landing' + ? nextSegment?.segmentType === 'landing' && visualTurnSide === 'left' + ? (['front', 'right'] as const) + : nextSegment?.segmentType === 'landing' && visualTurnSide === 'right' + ? (['front', 'left'] as const) + : visualTurnSide === 'left' + ? (['right'] as const) + : visualTurnSide === 'right' + ? (['left'] as const) + : (['left', 'right'] as const) + : (['left', 'right'] as const) + const sidePaths = sideCandidates + .map((side) => + buildSegmentRailPath( + layout, + side, + previousSegment, + nextSegment, + inset, + landingInset, + ), + ) + .filter((entry): entry is StairRailSidePath => entry !== null) + + return { + segment, + sidePaths: + isStraightLineDoubleLandingLayout && index === 1 + ? ((['left', 'right'] as const) + .map((side) => + buildSegmentRailPath( + layout, + side, + previousSegment, + nextSegment, + inset, + landingInset, + ), + ) + .filter((entry): entry is StairRailSidePath => entry !== null)) + : sidePaths, + connectFromPrevious: + index > 0 && + !(previousSegment?.segmentType === 'landing' && segment.segmentType === 'landing'), + } + }) + } + + const isStraightLineDoubleLandingLayout = + segments.length === 4 && + segments[0]?.segmentType === 'stair' && + segments[1]?.segmentType === 'landing' && + segments[2]?.segmentType === 'stair' && + segments[2]?.attachmentSide === 'front' && + segments[3]?.segmentType === 'landing' && + segments[3]?.attachmentSide === 'front' + + const resolved: StairSegmentRailPath[] = [] + layouts.forEach((layout, index) => { + const segment = layout.segment + const previousSegment = index > 0 ? segments[index - 1] : undefined + const nextSegment = index < segments.length - 1 ? segments[index + 1] : undefined + const nextAttachmentSide = nextSegment?.attachmentSide + const isMiddleLandingBetweenFlights = + segment.segmentType === 'landing' && + previousSegment?.segmentType === 'stair' && + nextSegment?.segmentType === 'stair' + const suppressLandingRailing = + segment.segmentType === 'landing' && + nextSegment?.segmentType === 'landing' && + nextAttachmentSide === railingMode + const landingContinuesOnPreferredSide = + segment.segmentType === 'landing' + ? nextAttachmentSide == null || + nextAttachmentSide === 'front' || + nextAttachmentSide === railingMode + : true + + const sidePaths = + suppressLandingRailing + ? [] + : segment.segmentType !== 'landing' + ? [ + buildSegmentRailPath( + layout, + railingMode, + previousSegment, + nextSegment, + inset, + landingInset, + ), + ] + : isStraightLineDoubleLandingLayout + ? [ + buildSegmentRailPath( + layout, + railingMode, + previousSegment, + nextSegment, + inset, + landingInset, + ), + ] + : isMiddleLandingBetweenFlights && railingMode === 'left' + ? nextAttachmentSide === 'right' + ? [ + buildSegmentRailPath( + layout, + 'front', + previousSegment, + nextSegment, + inset, + landingInset, + ), + buildSegmentRailPath( + layout, + 'left', + previousSegment, + nextSegment, + inset, + landingInset, + ), + ] + : [] + : isMiddleLandingBetweenFlights && railingMode === 'right' + ? nextAttachmentSide === 'left' + ? [ + buildSegmentRailPath( + layout, + 'front', + previousSegment, + nextSegment, + inset, + landingInset, + ), + buildSegmentRailPath( + layout, + 'right', + previousSegment, + nextSegment, + inset, + landingInset, + ), + ] + : [] + : nextSegment?.segmentType === 'landing' && + nextAttachmentSide != null && + nextAttachmentSide !== 'front' && + nextAttachmentSide !== railingMode + ? [ + buildSegmentRailPath( + layout, + 'front', + previousSegment, + nextSegment, + inset, + landingInset, + ), + buildSegmentRailPath( + layout, + railingMode, + previousSegment, + nextSegment, + inset, + landingInset, + ), + ] + : [ + buildSegmentRailPath( + layout, + railingMode, + previousSegment, + nextSegment, + inset, + landingInset, + ), + ] + + resolved.push({ + segment, + sidePaths: sidePaths.filter((entry): entry is StairRailSidePath => entry !== null), + connectFromPrevious: + index > 0 && + !suppressLandingRailing && + sidePaths.length > 0 && + (segment.segmentType === 'landing' ? landingContinuesOnPreferredSide : true), + }) + }) + + return resolved +} + +function computeStairRailLayouts( + segments: StairSegmentNode[], + transforms: SegmentTransform[], +): StairRailLayout[] { + return segments.map((segment, index) => { + const transform = transforms[index]! + const [centerOffsetX, centerOffsetZ] = rotateXZ(0, segment.length / 2, transform.rotation) + + return { + center: [transform.position[0] + centerOffsetX, transform.position[2] + centerOffsetZ], + elevation: transform.position[1], + rotation: transform.rotation, + segment, + } + }) +} + +function buildSegmentRailPath( + layout: StairRailLayout, + side: StairRailPathSide, + previousSegment: StairSegmentNode | undefined, + nextSegment: StairSegmentNode | undefined, + inset: number, + landingInset: number, +): StairRailSidePath | null { + const segment = layout.segment + const segmentSteps = Math.max(1, segment.segmentType === 'landing' ? 1 : segment.stepCount) + const segmentStepDepth = segment.length / segmentSteps + const segmentStepHeight = segment.segmentType === 'landing' ? 0 : segment.height / segmentSteps + const segmentTopThickness = getSegmentTopThickness(segment) + const flightSideOffset = + side === 'left' ? segment.width / 2 - 0.045 : -segment.width / 2 + 0.045 + const flightStartX = + previousSegment?.segmentType === 'landing' ? -segment.length / 2 + landingInset : -segment.length / 2 + const flightEndX = + nextSegment?.segmentType === 'landing' ? segment.length / 2 - landingInset : segment.length / 2 + + if (segment.segmentType === 'landing') { + return buildLandingRailPathFromScratch( + layout, + side, + previousSegment, + nextSegment, + segmentTopThickness, + landingInset, + ) + } + + return { + side, + points: [ + ...(previousSegment?.segmentType === 'landing' + ? [] + : [ + toRailLayoutWorldPoint(layout, flightStartX, segmentTopThickness, flightSideOffset), + ]), + ...Array.from({ length: segmentSteps }).map((_, index) => + toRailLayoutWorldPoint( + layout, + -segment.length / 2 + segmentStepDepth * index + segmentStepDepth / 2, + segmentStepHeight * (index + 1), + flightSideOffset, + ), + ), + ...(nextSegment?.segmentType === 'landing' + ? [] + : [ + toRailLayoutWorldPoint(layout, flightEndX, segment.height, flightSideOffset), + ]), + ], + } +} + +function buildLandingRailPathFromScratch( + layout: StairRailLayout, + side: StairRailPathSide, + previousSegment: StairSegmentNode | undefined, + nextSegment: StairSegmentNode | undefined, + topY: number, + inset: number, +): StairRailSidePath | null { + const segment = layout.segment + const backX = -segment.length / 2 + inset + const frontX = segment.length / 2 - inset + const leftZ = segment.width / 2 - inset + const rightZ = -segment.width / 2 + inset + + const edgePoints = + side === 'left' + ? ([ + toRailLayoutWorldPoint(layout, backX, topY, leftZ), + toRailLayoutWorldPoint(layout, frontX, topY, leftZ), + ] as THREE.Vector3[]) + : side === 'right' + ? ([ + toRailLayoutWorldPoint(layout, backX, topY, rightZ), + toRailLayoutWorldPoint(layout, frontX, topY, rightZ), + ] as THREE.Vector3[]) + : ([ + // When the next flight turns, rail the visible leading edge nearest the turn opening. + toRailLayoutWorldPoint( + layout, + previousSegment?.segmentType === 'stair' && + nextSegment?.attachmentSide && + nextSegment.attachmentSide !== 'front' + ? backX + : frontX, + topY, + leftZ, + ), + toRailLayoutWorldPoint( + layout, + previousSegment?.segmentType === 'stair' && + nextSegment?.attachmentSide && + nextSegment.attachmentSide !== 'front' + ? backX + : frontX, + topY, + rightZ, + ), + ] as THREE.Vector3[]) + + return { + side, + points: edgePoints, + } +} + +function toRailLayoutWorldPoint( + layout: StairRailLayout, + localX: number, + localY: number, + localZ: number, +): THREE.Vector3 { + const [offsetX, offsetZ] = rotateXZ(localZ, localX, layout.rotation) + return new THREE.Vector3( + layout.center[0] + offsetX, + layout.elevation + localY, + layout.center[1] + offsetZ, + ) +} + +function buildOffsetRailSegmentGeometries( + points: THREE.Vector3[], + heightOffset: number, + radius: number, +): THREE.BufferGeometry[] { + const geometries: THREE.BufferGeometry[] = [] + + for (let index = 0; index < points.length - 1; index++) { + const start = points[index] + const end = points[index + 1] + if (!(start && end)) continue + + const segmentGeometry = createCylinderBetweenPoints( + start.clone().add(new THREE.Vector3(0, heightOffset, 0)), + end.clone().add(new THREE.Vector3(0, heightOffset, 0)), + radius, + 8, + ) + if (segmentGeometry) { + geometries.push(segmentGeometry) + } + } + + return geometries +} + +function buildBalusterGeometries( + points: THREE.Vector3[], + height: number, + radius: number, +): THREE.BufferGeometry[] { + const geometries: THREE.BufferGeometry[] = [] + + for (const point of points) { + const geometry = new THREE.CylinderGeometry(radius, radius, Math.max(height, 0.05), 8) + geometry.translate(point.x, point.y + height / 2, point.z) + geometries.push(geometry) + } + + return geometries +} + +function getSegmentTopThickness(segment: StairSegmentNode): number { + return Math.max(segment.thickness ?? 0.25, 0.02) +} + +function createCylinderBetweenPoints( + start: THREE.Vector3, + end: THREE.Vector3, + radius: number, + radialSegments: number, +): THREE.BufferGeometry | null { + const direction = new THREE.Vector3().subVectors(end, start) + const length = direction.length() + if (length <= 1e-5) return null + + const midpoint = new THREE.Vector3().addVectors(start, end).multiplyScalar(0.5) + const quaternion = new THREE.Quaternion().setFromUnitVectors( + new THREE.Vector3(0, 1, 0), + direction.clone().normalize(), + ) + + const geometry = new THREE.CylinderGeometry(radius, radius, length, radialSegments) + geometry.applyQuaternion(quaternion) + geometry.translate(midpoint.x, midpoint.y, midpoint.z) + return geometry +} + /** * Computes the absolute Y height of a segment by traversing the stair's segment chain. */ diff --git a/packages/editor/src/components/editor/floating-action-menu.tsx b/packages/editor/src/components/editor/floating-action-menu.tsx index 34dc070f..574b318b 100755 --- a/packages/editor/src/components/editor/floating-action-menu.tsx +++ b/packages/editor/src/components/editor/floating-action-menu.tsx @@ -4,6 +4,7 @@ import { type AnyNode, type AnyNodeId, DoorNode, + FenceNode, ItemNode, RoofNode, RoofSegmentNode, @@ -31,6 +32,7 @@ const ALLOWED_TYPES = [ 'stair', 'stair-segment', 'wall', + 'fence', 'slab', ] const DELETE_ONLY_TYPES = ['wall', 'slab'] @@ -77,6 +79,7 @@ export function FloatingActionMenu() { node.type === 'item' || node.type === 'window' || node.type === 'door' || + node.type === 'fence' || node.type === 'roof' || node.type === 'roof-segment' || node.type === 'stair' || @@ -108,6 +111,8 @@ export function FloatingActionMenu() { duplicate = WindowNode.parse(duplicateInfo) } else if (node.type === 'item') { duplicate = ItemNode.parse(duplicateInfo) + } else if (node.type === 'fence') { + duplicate = FenceNode.parse(duplicateInfo) } else if (node.type === 'roof') { duplicate = RoofNode.parse(duplicateInfo) } else if (node.type === 'roof-segment') { @@ -125,6 +130,8 @@ export function FloatingActionMenu() { if (duplicate) { if (duplicate.type === 'door' || duplicate.type === 'window') { useScene.getState().createNode(duplicate, duplicate.parentId as AnyNodeId) + } else if (duplicate.type === 'fence') { + useScene.getState().createNode(duplicate, duplicate.parentId as AnyNodeId) } else if ( duplicate.type === 'roof' || duplicate.type === 'roof-segment' || @@ -181,6 +188,7 @@ export function FloatingActionMenu() { } if ( duplicate.type === 'item' || + duplicate.type === 'fence' || duplicate.type === 'window' || duplicate.type === 'door' || duplicate.type === 'roof' || diff --git a/packages/editor/src/components/editor/selection-manager.tsx b/packages/editor/src/components/editor/selection-manager.tsx index 18dbcf35..80b252cb 100755 --- a/packages/editor/src/components/editor/selection-manager.tsx +++ b/packages/editor/src/components/editor/selection-manager.tsx @@ -26,6 +26,7 @@ const isNodeInCurrentLevel = (node: AnyNode): boolean => { type SelectableNodeType = | 'wall' + | 'fence' | 'item' | 'building' | 'zone' @@ -184,6 +185,7 @@ const SELECTION_STRATEGIES: Record = { structure: { types: [ 'wall', + 'fence', 'item', 'zone', 'slab', @@ -236,6 +238,7 @@ const SELECTION_STRATEGIES: Record = { } if ( node.type === 'wall' || + node.type === 'fence' || node.type === 'slab' || node.type === 'ceiling' || node.type === 'roof' || @@ -297,6 +300,7 @@ const getSelectionTarget = (node: AnyNode): SelectionTarget | null => { if ( node.type === 'wall' || + node.type === 'fence' || node.type === 'slab' || node.type === 'ceiling' || node.type === 'roof' || @@ -441,6 +445,7 @@ export const SelectionManager = () => { const allTypes = [ 'wall', + 'fence', 'item', 'building', 'zone', @@ -535,6 +540,7 @@ export const SelectionManager = () => { } } else if ( node.type === 'wall' || + node.type === 'fence' || node.type === 'slab' || node.type === 'ceiling' || node.type === 'roof' || @@ -584,6 +590,7 @@ export const SelectionManager = () => { const allTypes = [ 'wall', + 'fence', 'item', 'building', 'slab', @@ -655,6 +662,7 @@ export const SelectionManager = () => { const allTypes = [ 'wall', + 'fence', 'item', 'slab', 'ceiling', diff --git a/packages/editor/src/components/tools/fence/fence-drafting.ts b/packages/editor/src/components/tools/fence/fence-drafting.ts new file mode 100644 index 00000000..f5ec1167 --- /dev/null +++ b/packages/editor/src/components/tools/fence/fence-drafting.ts @@ -0,0 +1,125 @@ +import { FenceNode, useScene, type WallNode } from '@pascal-app/core' +import { useViewer } from '@pascal-app/viewer' +import { sfxEmitter } from '../../../lib/sfx-bus' +import { + type WallPlanPoint, + findWallSnapTarget, + isWallLongEnough, + snapPointTo45Degrees, + snapPointToGrid, +} from '../wall/wall-drafting' + +export type FencePlanPoint = WallPlanPoint + +type SegmentNode = { + start: FencePlanPoint + end: FencePlanPoint +} + +function distanceSquared(a: FencePlanPoint, b: FencePlanPoint): number { + const dx = a[0] - b[0] + const dz = a[1] - b[1] + return dx * dx + dz * dz +} + +function projectPointOntoSegment( + point: FencePlanPoint, + segment: SegmentNode, +): FencePlanPoint | null { + const [x1, z1] = segment.start + const [x2, z2] = segment.end + const dx = x2 - x1 + const dz = z2 - z1 + const lengthSquared = dx * dx + dz * dz + if (lengthSquared < 1e-9) { + return null + } + + const t = ((point[0] - x1) * dx + (point[1] - z1) * dz) / lengthSquared + if (t <= 0 || t >= 1) { + return null + } + + return [x1 + dx * t, z1 + dz * t] +} + +function findFenceSnapTarget( + point: FencePlanPoint, + fences: FenceNode[], + ignoreFenceIds: string[] = [], +): FencePlanPoint | null { + const radiusSquared = 0.35 ** 2 + const ignoredFenceIds = new Set(ignoreFenceIds) + let bestTarget: FencePlanPoint | null = null + let bestDistanceSquared = Number.POSITIVE_INFINITY + + for (const fence of fences) { + if (ignoredFenceIds.has(fence.id)) { + continue + } + + const candidates: Array = [ + fence.start, + fence.end, + projectPointOntoSegment(point, fence), + ] + + for (const candidate of candidates) { + if (!candidate) { + continue + } + + const candidateDistanceSquared = distanceSquared(point, candidate) + if ( + candidateDistanceSquared > radiusSquared || + candidateDistanceSquared >= bestDistanceSquared + ) { + continue + } + + bestTarget = candidate + bestDistanceSquared = candidateDistanceSquared + } + } + + return bestTarget +} + +export function snapFenceDraftPoint(args: { + point: FencePlanPoint + walls: WallNode[] + fences: FenceNode[] + start?: FencePlanPoint + angleSnap?: boolean + ignoreFenceIds?: string[] +}): FencePlanPoint { + const { point, walls, fences, start, angleSnap = false, ignoreFenceIds } = args + const basePoint = start && angleSnap ? snapPointTo45Degrees(start, point) : snapPointToGrid(point) + const fenceSnapTarget = findFenceSnapTarget(basePoint, fences, ignoreFenceIds) + + return fenceSnapTarget ?? findWallSnapTarget(basePoint, walls) ?? basePoint +} + +export function createFenceOnCurrentLevel( + start: FencePlanPoint, + end: FencePlanPoint, +): FenceNode | null { + const currentLevelId = useViewer.getState().selection.levelId + const { createNode, nodes } = useScene.getState() + + if (!(currentLevelId && isWallLongEnough(start, end))) { + return null + } + + const fenceCount = Object.values(nodes).filter((node) => node.type === 'fence').length + const fence = FenceNode.parse({ + name: `Fence ${fenceCount + 1}`, + start, + end, + }) + + createNode(fence, currentLevelId) + sfxEmitter.emit('sfx:structure-build') + + return fence +} diff --git a/packages/editor/src/components/tools/fence/fence-tool.tsx b/packages/editor/src/components/tools/fence/fence-tool.tsx new file mode 100644 index 00000000..d7091fd1 --- /dev/null +++ b/packages/editor/src/components/tools/fence/fence-tool.tsx @@ -0,0 +1,190 @@ +import { + emitter, + type FenceNode, + type GridEvent, + type LevelNode, + useScene, + type WallNode, +} from '@pascal-app/core' +import { useViewer } from '@pascal-app/viewer' +import { useEffect, useRef } from 'react' +import { DoubleSide, type Group, type Mesh, Shape, ShapeGeometry, Vector3 } from 'three' +import { markToolCancelConsumed } from '../../../hooks/use-keyboard' +import { EDITOR_LAYER } from '../../../lib/constants' +import { sfxEmitter } from '../../../lib/sfx-bus' +import { CursorSphere } from '../shared/cursor-sphere' +import { + createFenceOnCurrentLevel, + snapFenceDraftPoint, + type FencePlanPoint, +} from './fence-drafting' + +const FENCE_PREVIEW_HEIGHT = 1.8 + +const updateFencePreview = (mesh: Mesh, start: Vector3, end: Vector3) => { + const direction = new Vector3(end.x - start.x, 0, end.z - start.z) + const length = direction.length() + + if (length < 0.01) { + mesh.visible = false + return + } + + mesh.visible = true + direction.normalize() + + const shape = new Shape() + shape.moveTo(0, 0) + shape.lineTo(length, 0) + shape.lineTo(length, FENCE_PREVIEW_HEIGHT) + shape.lineTo(0, FENCE_PREVIEW_HEIGHT) + shape.closePath() + + const geometry = new ShapeGeometry(shape) + const angle = -Math.atan2(direction.z, direction.x) + + mesh.position.set(start.x, start.y, start.z) + mesh.rotation.y = angle + + if (mesh.geometry) { + mesh.geometry.dispose() + } + mesh.geometry = geometry +} + +const getCurrentLevelElements = (): { walls: WallNode[]; fences: FenceNode[] } => { + const currentLevelId = useViewer.getState().selection.levelId + const { nodes } = useScene.getState() + + if (!currentLevelId) return { walls: [], fences: [] } + + const levelNode = nodes[currentLevelId] + if (!levelNode || levelNode.type !== 'level') return { walls: [], fences: [] } + + const children = (levelNode as LevelNode).children.map((childId) => nodes[childId]) + + return { + walls: children.filter((node): node is WallNode => node?.type === 'wall'), + fences: children.filter((node): node is FenceNode => node?.type === 'fence'), + } +} + +export const FenceTool: React.FC = () => { + const cursorRef = useRef(null) + const previewRef = useRef(null!) + const startingPoint = useRef(new Vector3(0, 0, 0)) + const endingPoint = useRef(new Vector3(0, 0, 0)) + const buildingState = useRef(0) + const shiftPressed = useRef(false) + + useEffect(() => { + let previousFenceEnd: [number, number] | null = null + + const onGridMove = (event: GridEvent) => { + if (!(cursorRef.current && previewRef.current)) return + + const { walls, fences } = getCurrentLevelElements() + const localPoint: FencePlanPoint = [event.localPosition[0], event.localPosition[2]] + + if (buildingState.current === 1) { + const snappedLocal = snapFenceDraftPoint({ + point: localPoint, + walls, + fences, + start: [startingPoint.current.x, startingPoint.current.z], + angleSnap: !shiftPressed.current, + }) + endingPoint.current.set(snappedLocal[0], event.localPosition[1], snappedLocal[1]) + cursorRef.current.position.copy(endingPoint.current) + + const currentFenceEnd: [number, number] = [snappedLocal[0], snappedLocal[1]] + if ( + previousFenceEnd && + (currentFenceEnd[0] !== previousFenceEnd[0] || currentFenceEnd[1] !== previousFenceEnd[1]) + ) { + sfxEmitter.emit('sfx:grid-snap') + } + previousFenceEnd = currentFenceEnd + + updateFencePreview(previewRef.current, startingPoint.current, endingPoint.current) + } else { + const snappedPoint = snapFenceDraftPoint({ point: localPoint, walls, fences }) + cursorRef.current.position.set(snappedPoint[0], event.localPosition[1], snappedPoint[1]) + } + } + + const onGridClick = (event: GridEvent) => { + const { walls, fences } = getCurrentLevelElements() + const localClick: FencePlanPoint = [event.localPosition[0], event.localPosition[2]] + + if (buildingState.current === 0) { + const snappedStart = snapFenceDraftPoint({ point: localClick, walls, fences }) + startingPoint.current.set(snappedStart[0], event.localPosition[1], snappedStart[1]) + endingPoint.current.copy(startingPoint.current) + buildingState.current = 1 + previewRef.current.visible = true + } else { + const snappedEnd = snapFenceDraftPoint({ + point: localClick, + walls, + fences, + start: [startingPoint.current.x, startingPoint.current.z], + angleSnap: !shiftPressed.current, + }) + const dx = snappedEnd[0] - startingPoint.current.x + const dz = snappedEnd[1] - startingPoint.current.z + if (dx * dx + dz * dz < 0.01 * 0.01) return + createFenceOnCurrentLevel([startingPoint.current.x, startingPoint.current.z], snappedEnd) + previewRef.current.visible = false + buildingState.current = 0 + } + } + + const onKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Shift') shiftPressed.current = true + } + + const onKeyUp = (e: KeyboardEvent) => { + if (e.key === 'Shift') shiftPressed.current = false + } + + const onCancel = () => { + if (buildingState.current === 1) { + markToolCancelConsumed() + buildingState.current = 0 + previewRef.current.visible = false + } + } + + emitter.on('grid:move', onGridMove) + emitter.on('grid:click', onGridClick) + emitter.on('tool:cancel', onCancel) + window.addEventListener('keydown', onKeyDown) + window.addEventListener('keyup', onKeyUp) + + return () => { + emitter.off('grid:move', onGridMove) + emitter.off('grid:click', onGridClick) + emitter.off('tool:cancel', onCancel) + window.removeEventListener('keydown', onKeyDown) + window.removeEventListener('keyup', onKeyUp) + } + }, []) + + return ( + + + + + + + + ) +} diff --git a/packages/editor/src/components/tools/fence/move-fence-tool.tsx b/packages/editor/src/components/tools/fence/move-fence-tool.tsx new file mode 100644 index 00000000..e1d75bd0 --- /dev/null +++ b/packages/editor/src/components/tools/fence/move-fence-tool.tsx @@ -0,0 +1,129 @@ +'use client' + +import { type FenceNode, emitter, type GridEvent, sceneRegistry, useScene } from '@pascal-app/core' +import { useViewer } from '@pascal-app/viewer' +import { useCallback, useEffect, useRef, useState } from 'react' +import { markToolCancelConsumed } from '../../../hooks/use-keyboard' +import { sfxEmitter } from '../../../lib/sfx-bus' +import useEditor from '../../../store/use-editor' +import { CursorSphere } from '../shared/cursor-sphere' + +function snap(value: number) { + return Math.round(value * 2) / 2 +} + +export const MoveFenceTool: React.FC<{ node: FenceNode }> = ({ node }) => { + const previousGridPosRef = useRef<[number, number] | null>(null) + const originalStartRef = useRef<[number, number]>([...node.start] as [number, number]) + const originalEndRef = useRef<[number, number]>([...node.end] as [number, number]) + const dragAnchorRef = useRef<[number, number] | null>(null) + const nodeIdRef = useRef(node.id) + + const [cursorLocalPos, setCursorLocalPos] = useState<[number, number, number]>(() => { + const centerX = (node.start[0] + node.end[0]) / 2 + const centerZ = (node.start[1] + node.end[1]) / 2 + return [centerX, 0, centerZ] + }) + + const exitMoveMode = useCallback(() => { + useEditor.getState().setMovingNode(null) + }, []) + + useEffect(() => { + const nodeId = nodeIdRef.current + const originalStart = originalStartRef.current + const originalEnd = originalEndRef.current + const mesh = sceneRegistry.nodes.get(nodeId) + + useScene.temporal.getState().pause() + let wasCommitted = false + + const updatePreview = (nextStart: [number, number], nextEnd: [number, number]) => { + const centerX = (nextStart[0] + nextEnd[0]) / 2 + const centerZ = (nextStart[1] + nextEnd[1]) / 2 + setCursorLocalPos([centerX, 0, centerZ]) + + if (mesh) { + mesh.position.set(centerX, 0, centerZ) + } + } + + const onGridMove = (event: GridEvent) => { + const localX = snap(event.localPosition[0]) + const localZ = snap(event.localPosition[2]) + + if ( + previousGridPosRef.current && + (localX !== previousGridPosRef.current[0] || localZ !== previousGridPosRef.current[1]) + ) { + sfxEmitter.emit('sfx:grid-snap') + } + previousGridPosRef.current = [localX, localZ] + + const anchor = dragAnchorRef.current ?? [localX, localZ] + dragAnchorRef.current = anchor + + const deltaX = localX - anchor[0] + const deltaZ = localZ - anchor[1] + + const nextStart: [number, number] = [originalStart[0] + deltaX, originalStart[1] + deltaZ] + const nextEnd: [number, number] = [originalEnd[0] + deltaX, originalEnd[1] + deltaZ] + + updatePreview(nextStart, nextEnd) + } + + const onGridClick = (event: GridEvent) => { + const anchor = dragAnchorRef.current + const localX = snap(event.localPosition[0]) + const localZ = snap(event.localPosition[2]) + const baseAnchor: [number, number] = anchor ?? [localX, localZ] + const deltaX = localX - baseAnchor[0] + const deltaZ = localZ - baseAnchor[1] + + const nextStart: [number, number] = [originalStart[0] + deltaX, originalStart[1] + deltaZ] + const nextEnd: [number, number] = [originalEnd[0] + deltaX, originalEnd[1] + deltaZ] + + wasCommitted = true + useScene.temporal.getState().resume() + useScene.getState().updateNode(nodeId, { start: nextStart, end: nextEnd }) + useScene.temporal.getState().pause() + + sfxEmitter.emit('sfx:item-place') + useViewer.getState().setSelection({ selectedIds: [nodeId] }) + exitMoveMode() + event.nativeEvent?.stopPropagation?.() + } + + const onCancel = () => { + if (mesh) { + const centerX = (originalStart[0] + originalEnd[0]) / 2 + const centerZ = (originalStart[1] + originalEnd[1]) / 2 + mesh.position.set(centerX, 0, centerZ) + } + useViewer.getState().setSelection({ selectedIds: [nodeId] }) + useScene.temporal.getState().resume() + markToolCancelConsumed() + exitMoveMode() + } + + emitter.on('grid:move', onGridMove) + emitter.on('grid:click', onGridClick) + emitter.on('tool:cancel', onCancel) + + return () => { + if (!wasCommitted) { + useScene.getState().updateNode(nodeId, { start: originalStart, end: originalEnd }) + } + useScene.temporal.getState().resume() + emitter.off('grid:move', onGridMove) + emitter.off('grid:click', onGridClick) + emitter.off('tool:cancel', onCancel) + } + }, [exitMoveMode]) + + return ( + + + + ) +} diff --git a/packages/editor/src/components/tools/item/move-tool.tsx b/packages/editor/src/components/tools/item/move-tool.tsx index 3ea47ddf..20cefbf5 100644 --- a/packages/editor/src/components/tools/item/move-tool.tsx +++ b/packages/editor/src/components/tools/item/move-tool.tsx @@ -1,6 +1,7 @@ import type { BuildingNode, DoorNode, + FenceNode, ItemNode, RoofNode, RoofSegmentNode, @@ -13,6 +14,7 @@ import { sfxEmitter } from '../../../lib/sfx-bus' import useEditor from '../../../store/use-editor' import { MoveBuildingContent } from '../building/move-building-tool' import { MoveDoorTool } from '../door/move-door-tool' +import { MoveFenceTool } from '../fence/move-fence-tool' import { MoveRoofTool } from '../roof/move-roof-tool' import { MoveWindowTool } from '../window/move-window-tool' import type { PlacementState } from './placement-types' @@ -86,6 +88,7 @@ export const MoveTool: React.FC = () => { return if (movingNode.type === 'door') return if (movingNode.type === 'window') return + if (movingNode.type === 'fence') return if (movingNode.type === 'roof' || movingNode.type === 'roof-segment') return if (movingNode.type === 'stair' || movingNode.type === 'stair-segment') diff --git a/packages/editor/src/components/tools/select/box-select-tool.tsx b/packages/editor/src/components/tools/select/box-select-tool.tsx index 5166ca31..79242c28 100644 --- a/packages/editor/src/components/tools/select/box-select-tool.tsx +++ b/packages/editor/src/components/tools/select/box-select-tool.tsx @@ -197,7 +197,7 @@ function collectNodeIdsInBounds(bounds: Bounds): string[] { const node = nodes[childId as AnyNodeId] if (!node) continue - if (node.type === 'wall') { + if (node.type === 'wall' || node.type === 'fence') { const wall = node as WallNode if ( segmentIntersectsBounds(wall.start[0], wall.start[1], wall.end[0], wall.end[1], bounds) diff --git a/packages/editor/src/components/tools/stair/stair-defaults.ts b/packages/editor/src/components/tools/stair/stair-defaults.ts index 6ccd1f5e..e2dd23cf 100644 --- a/packages/editor/src/components/tools/stair/stair-defaults.ts +++ b/packages/editor/src/components/tools/stair/stair-defaults.ts @@ -5,3 +5,5 @@ export const DEFAULT_STAIR_STEP_COUNT = 10 export const DEFAULT_STAIR_ATTACHMENT_SIDE = 'front' as const export const DEFAULT_STAIR_FILL_TO_FLOOR = true export const DEFAULT_STAIR_THICKNESS = 0.25 +export const DEFAULT_STAIR_RAILING_MODE = 'right' as const +export const DEFAULT_STAIR_RAILING_HEIGHT = 0.92 diff --git a/packages/editor/src/components/tools/stair/stair-tool.tsx b/packages/editor/src/components/tools/stair/stair-tool.tsx index a9093611..a3ddfa8f 100644 --- a/packages/editor/src/components/tools/stair/stair-tool.tsx +++ b/packages/editor/src/components/tools/stair/stair-tool.tsx @@ -17,6 +17,8 @@ import { DEFAULT_STAIR_FILL_TO_FLOOR, DEFAULT_STAIR_HEIGHT, DEFAULT_STAIR_LENGTH, + DEFAULT_STAIR_RAILING_HEIGHT, + DEFAULT_STAIR_RAILING_MODE, DEFAULT_STAIR_STEP_COUNT, DEFAULT_STAIR_THICKNESS, DEFAULT_STAIR_WIDTH, @@ -88,6 +90,8 @@ function commitStairPlacement( name, position, rotation, + railingHeight: DEFAULT_STAIR_RAILING_HEIGHT, + railingMode: DEFAULT_STAIR_RAILING_MODE, children: [segment.id], }) diff --git a/packages/editor/src/components/tools/tool-manager.tsx b/packages/editor/src/components/tools/tool-manager.tsx index c806c502..50373250 100644 --- a/packages/editor/src/components/tools/tool-manager.tsx +++ b/packages/editor/src/components/tools/tool-manager.tsx @@ -11,6 +11,7 @@ import { CeilingBoundaryEditor } from './ceiling/ceiling-boundary-editor' import { CeilingHoleEditor } from './ceiling/ceiling-hole-editor' import { CeilingTool } from './ceiling/ceiling-tool' import { DoorTool } from './door/door-tool' +import { FenceTool } from './fence/fence-tool' import { ItemTool } from './item/item-tool' import { MoveTool } from './item/move-tool' import { RoofTool } from './roof/roof-tool' @@ -30,6 +31,7 @@ const tools: Record>> = { }, structure: { wall: WallTool, + fence: FenceTool, slab: SlabTool, ceiling: CeilingTool, roof: RoofTool, diff --git a/packages/editor/src/components/ui/action-menu/structure-tools.tsx b/packages/editor/src/components/ui/action-menu/structure-tools.tsx index 6e731cae..71f081df 100644 --- a/packages/editor/src/components/ui/action-menu/structure-tools.tsx +++ b/packages/editor/src/components/ui/action-menu/structure-tools.tsx @@ -20,6 +20,7 @@ export type ToolConfig = { export const tools: ToolConfig[] = [ { id: 'wall', iconSrc: '/icons/wall.png', label: 'Wall' }, + { id: 'fence', iconSrc: '/icons/build.png', label: 'Fence' }, // { id: 'room', iconSrc: '/icons/room.png', label: 'Room' }, // { id: 'custom-room', iconSrc: '/icons/custom-room.png', label: 'Custom Room' }, { id: 'slab', iconSrc: '/icons/floor.png', label: 'Slab' }, diff --git a/packages/editor/src/components/ui/panels/fence-panel.tsx b/packages/editor/src/components/ui/panels/fence-panel.tsx new file mode 100644 index 00000000..a61ff783 --- /dev/null +++ b/packages/editor/src/components/ui/panels/fence-panel.tsx @@ -0,0 +1,184 @@ +'use client' + +import { type AnyNode, type AnyNodeId, type FenceBaseStyle, type FenceNode, type FenceStyle, useScene } from '@pascal-app/core' +import { useViewer } from '@pascal-app/viewer' +import { useCallback } from 'react' +import { PanelSection } from '../controls/panel-section' +import { SegmentedControl } from '../controls/segmented-control' +import { SliderControl } from '../controls/slider-control' +import { PanelWrapper } from './panel-wrapper' + +const FENCE_STYLE_OPTIONS: { label: string; value: FenceStyle }[] = [ + { label: 'Slat', value: 'slat' }, + { label: 'Rail', value: 'rail' }, + { label: 'Privacy', value: 'privacy' }, +] + +const FENCE_BASE_STYLE_OPTIONS: { label: string; value: FenceBaseStyle }[] = [ + { label: 'Grounded', value: 'grounded' }, + { label: 'Floating', value: 'floating' }, +] + +export function FencePanel() { + const selectedIds = useViewer((s) => s.selection.selectedIds) + const setSelection = useViewer((s) => s.setSelection) + const nodes = useScene((s) => s.nodes) + const updateNode = useScene((s) => s.updateNode) + + const selectedId = selectedIds[0] + const node = selectedId ? (nodes[selectedId as AnyNode['id']] as FenceNode | undefined) : undefined + + const handleUpdate = useCallback( + (updates: Partial) => { + if (!selectedId) return + updateNode(selectedId as AnyNode['id'], updates) + useScene.getState().dirtyNodes.add(selectedId as AnyNodeId) + }, + [selectedId, updateNode], + ) + + const handleUpdateLength = useCallback( + (newLength: number) => { + if (!node || newLength <= 0) return + + const dx = node.end[0] - node.start[0] + const dz = node.end[1] - node.start[1] + const currentLength = Math.sqrt(dx * dx + dz * dz) + if (currentLength === 0) return + + const dirX = dx / currentLength + const dirZ = dz / currentLength + const newEnd: [number, number] = [ + node.start[0] + dirX * newLength, + node.start[1] + dirZ * newLength, + ] + + handleUpdate({ end: newEnd }) + }, + [node, handleUpdate], + ) + + const handleClose = useCallback(() => { + setSelection({ selectedIds: [] }) + }, [setSelection]) + + if (!node || node.type !== 'fence' || selectedIds.length !== 1) return null + + const dx = node.end[0] - node.start[0] + const dz = node.end[1] - node.start[1] + const length = Math.sqrt(dx * dx + dz * dz) + + return ( + + + handleUpdate({ style: value })} + options={FENCE_STYLE_OPTIONS} + value={node.style} + /> + handleUpdate({ baseStyle: value })} + options={FENCE_BASE_STYLE_OPTIONS} + value={node.baseStyle} + /> + + + + + handleUpdate({ height: Math.max(0.4, value) })} + precision={2} + step={0.05} + unit="m" + value={node.height} + /> + handleUpdate({ thickness: Math.max(0.03, value) })} + precision={3} + step={0.005} + unit="m" + value={node.thickness} + /> + + + + handleUpdate({ baseHeight: Math.max(0.04, value) })} + precision={3} + step={0.01} + unit="m" + value={node.baseHeight} + /> + handleUpdate({ topRailHeight: Math.max(0.01, value) })} + precision={3} + step={0.005} + unit="m" + value={node.topRailHeight} + /> + handleUpdate({ postSpacing: Math.max(0.2, value) })} + precision={2} + step={0.05} + unit="m" + value={node.postSpacing} + /> + handleUpdate({ postSize: Math.max(0.01, value) })} + precision={3} + step={0.005} + unit="m" + value={node.postSize} + /> + handleUpdate({ groundClearance: Math.max(0, value) })} + precision={3} + step={0.005} + unit="m" + value={node.groundClearance} + /> + handleUpdate({ edgeInset: Math.max(0.005, value) })} + precision={3} + step={0.005} + unit="m" + value={node.edgeInset} + /> + + + ) +} diff --git a/packages/editor/src/components/ui/panels/panel-manager.tsx b/packages/editor/src/components/ui/panels/panel-manager.tsx index 4455842c..7ff860c0 100755 --- a/packages/editor/src/components/ui/panels/panel-manager.tsx +++ b/packages/editor/src/components/ui/panels/panel-manager.tsx @@ -5,6 +5,7 @@ import { useViewer } from '@pascal-app/viewer' import useEditor from '../../../store/use-editor' import { CeilingPanel } from './ceiling-panel' import { DoorPanel } from './door-panel' +import { FencePanel } from './fence-panel' import { ItemPanel } from './item-panel' import { ReferencePanel } from './reference-panel' import { RoofPanel } from './roof-panel' @@ -47,6 +48,8 @@ export function PanelManager() { return case 'wall': return + case 'fence': + return case 'door': return case 'window': diff --git a/packages/editor/src/components/ui/panels/stair-panel.tsx b/packages/editor/src/components/ui/panels/stair-panel.tsx index a6dd60f9..f48f2ef3 100644 --- a/packages/editor/src/components/ui/panels/stair-panel.tsx +++ b/packages/editor/src/components/ui/panels/stair-panel.tsx @@ -5,6 +5,7 @@ import { type AnyNodeId, type MaterialSchema, type StairNode, + type StairRailingMode, StairNode as StairNodeSchema, type StairSegmentNode, StairSegmentNode as StairSegmentNodeSchema, @@ -19,9 +20,17 @@ import { ActionButton, ActionGroup } from '../controls/action-button' import { MaterialPicker } from '../controls/material-picker' import { MetricControl } from '../controls/metric-control' import { PanelSection } from '../controls/panel-section' +import { SegmentedControl } from '../controls/segmented-control' import { SliderControl } from '../controls/slider-control' import { PanelWrapper } from './panel-wrapper' +const RAILING_MODE_OPTIONS: { label: string; value: StairRailingMode }[] = [ + { label: 'None', value: 'none' }, + { label: 'Left', value: 'left' }, + { label: 'Right', value: 'right' }, + { label: 'Both', value: 'both' }, +] + export function StairPanel() { const selectedIds = useViewer((s) => s.selection.selectedIds) const setSelection = useViewer((s) => s.setSelection) @@ -280,6 +289,26 @@ export function StairPanel() { + + handleUpdate({ railingMode: value })} + options={RAILING_MODE_OPTIONS} + value={node.railingMode ?? 'none'} + /> + {(node.railingMode ?? 'none') !== 'none' && ( + handleUpdate({ railingHeight: value })} + precision={2} + step={0.02} + unit="m" + value={Math.round((node.railingHeight ?? 0.92) * 100) / 100} + /> + )} + + } label="Move" onClick={handleMove} /> diff --git a/packages/editor/src/components/viewer-overlay.tsx b/packages/editor/src/components/viewer-overlay.tsx index 06f19a13..72b89c7f 100755 --- a/packages/editor/src/components/viewer-overlay.tsx +++ b/packages/editor/src/components/viewer-overlay.tsx @@ -62,6 +62,7 @@ const wallModeConfig = { const getNodeName = (node: AnyNode): string => { if ('name' in node && node.name) return node.name if (node.type === 'wall') return 'Wall' + if (node.type === 'fence') return 'Fence' if (node.type === 'item') return (node as { asset: { name: string } }).asset?.name || 'Item' if (node.type === 'slab') return 'Slab' if (node.type === 'ceiling') return 'Ceiling' diff --git a/packages/editor/src/hooks/use-contextual-tools.ts b/packages/editor/src/hooks/use-contextual-tools.ts index 50beff88..e24ec6fe 100644 --- a/packages/editor/src/hooks/use-contextual-tools.ts +++ b/packages/editor/src/hooks/use-contextual-tools.ts @@ -16,7 +16,15 @@ export function useContextualTools() { } // Default tools when nothing is selected - const defaultTools: StructureTool[] = ['wall', 'slab', 'ceiling', 'roof', 'door', 'window'] + const defaultTools: StructureTool[] = [ + 'wall', + 'fence', + 'slab', + 'ceiling', + 'roof', + 'door', + 'window', + ] if (selection.selectedIds.length === 0) { return defaultTools @@ -29,7 +37,7 @@ export function useContextualTools() { // If a wall is selected, prioritize wall-hosted elements if (selectedTypes.has('wall')) { - return ['window', 'door', 'wall'] as StructureTool[] + return ['window', 'door', 'wall', 'fence'] as StructureTool[] } // If a slab is selected, prioritize slab editing diff --git a/packages/editor/src/store/use-editor.tsx b/packages/editor/src/store/use-editor.tsx index dd4dd2f0..6ace07b8 100644 --- a/packages/editor/src/store/use-editor.tsx +++ b/packages/editor/src/store/use-editor.tsx @@ -4,6 +4,7 @@ import type { AssetInput } from '@pascal-app/core' import { type BuildingNode, type DoorNode, + type FenceNode, type ItemNode, type LevelNode, type RoofNode, @@ -33,6 +34,7 @@ export type Mode = 'select' | 'edit' | 'delete' | 'build' // Structure mode tools (building elements) export type StructureTool = | 'wall' + | 'fence' | 'room' | 'custom-room' | 'slab' @@ -85,6 +87,7 @@ type EditorState = { | ItemNode | WindowNode | DoorNode + | FenceNode | RoofNode | RoofSegmentNode | StairNode @@ -96,6 +99,7 @@ type EditorState = { | ItemNode | WindowNode | DoorNode + | FenceNode | RoofNode | RoofSegmentNode | StairNode diff --git a/packages/viewer/src/components/renderers/fence/fence-renderer.tsx b/packages/viewer/src/components/renderers/fence/fence-renderer.tsx new file mode 100644 index 00000000..9a65afc8 --- /dev/null +++ b/packages/viewer/src/components/renderers/fence/fence-renderer.tsx @@ -0,0 +1,135 @@ +import { type FenceNode, useRegistry } from '@pascal-app/core' +import { useMemo, useRef } from 'react' +import { BoxGeometry, Group } from 'three' +import { useNodeEvents } from '../../../hooks/use-node-events' +import { DEFAULT_STAIR_MATERIAL } from '../../../lib/materials' + +type FencePart = { + key: string + position: [number, number, number] + scale: [number, number, number] +} + +function getStyleDefaults(style: FenceNode['style']) { + if (style === 'privacy') { + return { spacingFactor: 0.42, postFactor: 1.35, baseFactor: 1.2, topFactor: 1.2 } + } + + if (style === 'rail') { + return { spacingFactor: 0.68, postFactor: 0.8, baseFactor: 0.85, topFactor: 0.85 } + } + + return { spacingFactor: 0.3, postFactor: 0.55, baseFactor: 1, topFactor: 0.75 } +} + +function createFenceParts(fence: FenceNode): FencePart[] { + const parts: FencePart[] = [] + const length = Math.max( + Math.hypot(fence.end[0] - fence.start[0], fence.end[1] - fence.start[1]), + 0.01, + ) + const panelDepth = Math.max(fence.thickness, 0.03) + const clearance = Math.max(fence.groundClearance, 0) + const styleDefaults = getStyleDefaults(fence.style) + const baseHeight = Math.max(fence.baseHeight * styleDefaults.baseFactor, 0.04) + const topRailHeight = Math.max(fence.topRailHeight * styleDefaults.topFactor, 0.01) + const verticalHeight = Math.max(fence.height - baseHeight - topRailHeight, 0.08) + const postWidth = Math.max(fence.postSize * styleDefaults.postFactor, 0.01) + const spacing = Math.max(fence.postSpacing * styleDefaults.spacingFactor, postWidth * 1.2) + const edgeInset = Math.max(fence.edgeInset ?? 0.015, 0.005) + const isFloating = fence.baseStyle === 'floating' + const baseY = isFloating ? clearance : 0 + const effectiveBaseHeight = isFloating ? baseHeight : baseHeight + clearance * 0.5 + + if (!isFloating) { + parts.push({ + key: 'base', + position: [0, baseY + effectiveBaseHeight / 2, 0], + scale: [length, effectiveBaseHeight, panelDepth * 1.05], + }) + parts.push({ + key: 'mid-rail', + position: [0, baseY + effectiveBaseHeight + verticalHeight * 0.15, 0], + scale: [length, topRailHeight * 0.8, panelDepth * 0.35], + }) + } + + const count = Math.max(2, Math.floor((length - edgeInset * 2) / spacing) + 1) + const step = count > 1 ? (length - edgeInset * 2) / (count - 1) : 0 + const startX = -length / 2 + edgeInset + const verticalY = baseY + effectiveBaseHeight + verticalHeight / 2 + + for (let index = 0; index < count; index += 1) { + const x = count === 1 ? 0 : startX + step * index + let posX = x + const isEdgePost = index === 0 || index === count - 1 + if (count > 1) { + if (index === 0) posX = -length / 2 + edgeInset + postWidth / 2 + else if (index === count - 1) posX = length / 2 - edgeInset - postWidth / 2 + } + const postHeight = + isFloating && isEdgePost + ? effectiveBaseHeight + verticalHeight + topRailHeight + clearance + : verticalHeight + const postY = isFloating && isEdgePost ? postHeight / 2 : verticalY + + parts.push({ + key: `vertical-${index}`, + position: [posX, postY, 0], + scale: [postWidth, postHeight, Math.max(panelDepth * 0.35, 0.012)], + }) + } + + parts.push({ + key: 'top-rail', + position: [0, baseY + effectiveBaseHeight + verticalHeight + topRailHeight / 2, 0], + scale: [length, topRailHeight, Math.max(panelDepth * 0.55, 0.018)], + }) + + if (isFloating) { + parts.push({ + key: 'bottom-rail', + position: [0, baseY + effectiveBaseHeight + topRailHeight / 2, 0], + scale: [length, topRailHeight, Math.max(panelDepth * 0.55, 0.018)], + }) + } + + return parts +} + +export const FenceRenderer = ({ node }: { node: FenceNode }) => { + const ref = useRef(null!) + const handlers = useNodeEvents(node, 'fence') + const geometry = useMemo(() => new BoxGeometry(1, 1, 1), []) + const parts = useMemo(() => createFenceParts(node), [node]) + const rotation = Math.atan2(node.end[1] - node.start[1], node.end[0] - node.start[0]) + const center: [number, number, number] = [ + (node.start[0] + node.end[0]) / 2, + 0, + (node.start[1] + node.end[1]) / 2, + ] + + useRegistry(node.id, 'fence', ref) + + return ( + + {parts.map((part) => ( + + ))} + + ) +} diff --git a/packages/viewer/src/components/renderers/node-renderer.tsx b/packages/viewer/src/components/renderers/node-renderer.tsx index 1347e372..827f6893 100644 --- a/packages/viewer/src/components/renderers/node-renderer.tsx +++ b/packages/viewer/src/components/renderers/node-renderer.tsx @@ -4,6 +4,7 @@ import { type AnyNode, useScene } from '@pascal-app/core' import { BuildingRenderer } from './building/building-renderer' import { CeilingRenderer } from './ceiling/ceiling-renderer' import { DoorRenderer } from './door/door-renderer' +import { FenceRenderer } from './fence/fence-renderer' import { GuideRenderer } from './guide/guide-renderer' import { ItemRenderer } from './item/item-renderer' import { LevelRenderer } from './level/level-renderer' @@ -32,6 +33,7 @@ export const NodeRenderer = ({ nodeId }: { nodeId: AnyNode['id'] }) => { {node.type === 'item' && } {node.type === 'slab' && } {node.type === 'wall' && } + {node.type === 'fence' && } {node.type === 'door' && } {node.type === 'window' && } {node.type === 'zone' && } diff --git a/packages/viewer/src/components/renderers/stair/stair-renderer.tsx b/packages/viewer/src/components/renderers/stair/stair-renderer.tsx index efeb24ad..da228de2 100644 --- a/packages/viewer/src/components/renderers/stair/stair-renderer.tsx +++ b/packages/viewer/src/components/renderers/stair/stair-renderer.tsx @@ -1,10 +1,40 @@ -import { type StairNode, useRegistry, useScene } from '@pascal-app/core' +import { type AnyNodeId, type StairNode, type StairSegmentNode, useRegistry, useScene } from '@pascal-app/core' import { useLayoutEffect, useMemo, useRef } from 'react' -import type * as THREE from 'three' +import * as THREE from 'three' import { useNodeEvents } from '../../../hooks/use-node-events' import { createMaterial, DEFAULT_STAIR_MATERIAL } from '../../../lib/materials' import { NodeRenderer } from '../node-renderer' +type SegmentTransform = { + position: [number, number, number] + rotation: number +} + +type StairRailPathSide = 'left' | 'right' | 'front' + +type StairRailSidePath = { + side: StairRailPathSide + points: [number, number, number][] +} + +type StairSegmentRailPath = { + layout: StairRailLayout + sidePaths: StairRailSidePath[] + connectFromPrevious: boolean +} + +type StairRailLayout = { + center: [number, number] + elevation: number + rotation: number + segment: StairSegmentNode +} + +type LandingChainNextStair = { + nextStairLayout?: StairRailLayout + isTerminalLandingBeforeStair: boolean +} + export const StairRenderer = ({ node }: { node: StairNode }) => { const ref = useRef(null!) @@ -34,6 +64,7 @@ export const StairRenderer = ({ node }: { node: StairNode }) => { + {(node.children ?? []).map((childId) => ( @@ -42,3 +73,471 @@ export const StairRenderer = ({ node }: { node: StairNode }) => { ) } + +function StairRailings({ stair, material }: { stair: StairNode; material: THREE.Material }) { + const nodes = useScene((state) => state.nodes) + + const segments = useMemo( + () => + (stair.children ?? []) + .map((childId) => nodes[childId as AnyNodeId] as StairSegmentNode | undefined) + .filter((node): node is StairSegmentNode => node?.type === 'stair-segment' && node.visible !== false), + [nodes, stair.children], + ) + + const railPaths = useMemo(() => buildStairRailPaths(segments, stair.railingMode ?? 'none'), [segments, stair.railingMode]) + + const railHeight = stair.railingHeight ?? 0.92 + const midRailHeight = Math.max(railHeight * 0.45, 0.35) + const railRadius = 0.022 + const balusterRadius = 0.018 + + if ((stair.railingMode ?? 'none') === 'none' || railPaths.length === 0) { + return null + } + + return ( + + {railPaths.map((segmentPath, index) => ( + + {segmentPath.sidePaths.map((sidePath, sideIndex) => ( + + {sidePath.points.map((point, pointIndex) => ( + + ))} + {sidePath.points.slice(0, -1).map((point, pointIndex) => { + const nextPoint = sidePath.points[pointIndex + 1] + if (!nextPoint) return null + + return ( + + + + + ) + })} + + ))} + + ))} + {railPaths.slice(1).map((segmentPath, index) => { + const previousPath = railPaths[index] + if (!previousPath || !segmentPath.connectFromPrevious) return null + if (previousPath.layout.segment.segmentType === 'landing') return null + if (segmentPath.layout.segment.segmentType === 'landing') return null + + return segmentPath.sidePaths.map((sidePath, sideIndex) => { + const currentPoint = sidePath.points[0] + if (!currentPoint) return null + + const currentWorldPoint = toWorldRailPoint(segmentPath.layout, currentPoint) + const previousSidePath = [...previousPath.sidePaths] + .map((entry) => { + const lastPoint = entry.points[entry.points.length - 1] + return { + entry, + distance: lastPoint ? distance3(toWorldRailPoint(previousPath.layout, lastPoint), currentWorldPoint) : Number.POSITIVE_INFINITY, + } + }) + .sort((left, right) => left.distance - right.distance)[0]?.entry + const previousPoint = + previousSidePath && previousSidePath.points.length + ? previousSidePath.points[previousSidePath.points.length - 1] + : null + + if (!(previousPoint && currentPoint)) { + return null + } + + const previousWorldPoint = toWorldRailPoint(previousPath.layout, previousPoint) + + return ( + + + + + ) + }) + })} + + ) +} + +const BALUSTER_GEOMETRY = new THREE.CylinderGeometry(1, 1, 1, 8) +const RAIL_GEOMETRY = new THREE.CylinderGeometry(1, 1, 1, 8) + +function RailSegment({ + start, + end, + radius, + material, +}: { + start: [number, number, number] + end: [number, number, number] + radius: number + material: THREE.Material +}) { + const startVector = useMemo(() => new THREE.Vector3(...start), [start]) + const endVector = useMemo(() => new THREE.Vector3(...end), [end]) + const direction = useMemo(() => endVector.clone().sub(startVector), [endVector, startVector]) + const length = Math.max(direction.length(), 0.01) + const quaternion = useMemo( + () => new THREE.Quaternion().setFromUnitVectors(new THREE.Vector3(0, 1, 0), direction.clone().normalize()), + [direction], + ) + const midpoint = useMemo(() => startVector.clone().add(endVector).multiplyScalar(0.5), [endVector, startVector]) + + return ( + + ) +} + +function buildStairRailPaths( + segments: StairSegmentNode[], + railingMode: StairNode['railingMode'], +): StairSegmentRailPath[] { + if (!segments.length || railingMode === 'none') return [] + + const layouts = computeStairRailLayouts(segments) + const landingInset = 0.08 + + if (railingMode === 'both') { + const isStraightLineDoubleLandingLayout = + layouts.length === 4 && + layouts[0]?.segment.segmentType === 'stair' && + layouts[1]?.segment.segmentType === 'landing' && + layouts[2]?.segment.segmentType === 'stair' && + layouts[2]?.segment.attachmentSide === 'front' && + layouts[3]?.segment.segmentType === 'landing' && + layouts[3]?.segment.attachmentSide === 'front' + + return layouts.map((layout, index) => { + const previousLayout = index > 0 ? layouts[index - 1] : undefined + const nextLayout = layouts[index + 1] + const { nextStairLayout, isTerminalLandingBeforeStair } = resolveLandingChainNextStair(layouts, index) + const hideLandingRailing = + layout.segment.segmentType === 'landing' && + previousLayout?.segment.segmentType === 'stair' && + nextLayout?.segment.segmentType === 'stair' + const visualTurnSide = + isTerminalLandingBeforeStair && nextStairLayout?.segment.attachmentSide + ? nextStairLayout.segment.attachmentSide + : nextLayout?.segment.attachmentSide + const sideCandidates = + isTerminalLandingBeforeStair && layout.segment.segmentType === 'landing' + ? visualTurnSide === 'left' + ? (['front', 'right'] as const) + : visualTurnSide === 'right' + ? (['front', 'left'] as const) + : (['left', 'right'] as const) + : hideLandingRailing + ? visualTurnSide === 'left' + ? (['front', 'right'] as const) + : visualTurnSide === 'right' + ? (['front', 'left'] as const) + : (['left', 'right'] as const) + : layout.segment.segmentType === 'landing' + ? nextLayout?.segment.segmentType === 'landing' && visualTurnSide === 'left' + ? (['front', 'right'] as const) + : nextLayout?.segment.segmentType === 'landing' && visualTurnSide === 'right' + ? (['front', 'left'] as const) + : visualTurnSide === 'left' + ? (['right'] as const) + : visualTurnSide === 'right' + ? (['left'] as const) + : (['left', 'right'] as const) + : (['left', 'right'] as const) + + return { + layout, + sidePaths: + isStraightLineDoubleLandingLayout && index === 1 + ? (['left', 'right'] as const).map((side) => buildSegmentRailPath(layouts, index, side, landingInset)) + : sideCandidates.map((side) => buildSegmentRailPath(layouts, index, side, landingInset)), + connectFromPrevious: + index > 0 && + !(previousLayout?.segment.segmentType === 'landing' && layout.segment.segmentType === 'landing'), + } + }) + } + + const isStraightLineDoubleLandingLayout = + layouts.length === 4 && + layouts[0]?.segment.segmentType === 'stair' && + layouts[1]?.segment.segmentType === 'landing' && + layouts[2]?.segment.segmentType === 'stair' && + layouts[2]?.segment.attachmentSide === 'front' && + layouts[3]?.segment.segmentType === 'landing' && + layouts[3]?.segment.attachmentSide === 'front' + + return layouts.map((layout, index) => { + const previousLayout = index > 0 ? layouts[index - 1] : undefined + const nextLayout = layouts[index + 1] + const { nextStairLayout, isTerminalLandingBeforeStair } = resolveLandingChainNextStair(layouts, index) + const isMiddleLandingBetweenFlights = + layout.segment.segmentType === 'landing' && + previousLayout?.segment.segmentType === 'stair' && + nextLayout?.segment.segmentType === 'stair' + const nextAttachmentSide = nextLayout?.segment.attachmentSide + const terminalNextAttachmentSide = nextStairLayout?.segment.attachmentSide + const suppressMiddleLandingOnPreferredTurnSide = + isMiddleLandingBetweenFlights && + nextAttachmentSide != null && + nextAttachmentSide !== 'front' && + nextAttachmentSide === railingMode + const suppressLandingRailing = + (layout.segment.segmentType === 'landing' && + nextLayout?.segment.segmentType === 'landing' && + nextAttachmentSide === railingMode) || + suppressMiddleLandingOnPreferredTurnSide + const landingContinuesOnPreferredSide = + layout.segment.segmentType === 'landing' + ? nextAttachmentSide == null || nextAttachmentSide === 'front' || nextAttachmentSide === railingMode + : true + + const sideCandidates = + suppressLandingRailing + ? ([] as StairRailPathSide[]) + : layout.segment.segmentType !== 'landing' + ? [railingMode] + : isTerminalLandingBeforeStair + ? railingMode === 'left' + ? terminalNextAttachmentSide === 'right' + ? (['front', 'left'] as const) + : ([] as StairRailPathSide[]) + : railingMode === 'right' + ? terminalNextAttachmentSide === 'left' + ? (['front', 'right'] as const) + : ([] as StairRailPathSide[]) + : [railingMode] + : isStraightLineDoubleLandingLayout + ? [railingMode] + : isMiddleLandingBetweenFlights && railingMode === 'left' + ? nextAttachmentSide === 'right' + ? (['front', 'left'] as const) + : (['left'] as const) + : isMiddleLandingBetweenFlights && railingMode === 'right' + ? nextAttachmentSide === 'left' + ? (['front', 'right'] as const) + : (['right'] as const) + : nextLayout?.segment.segmentType === 'landing' && + nextAttachmentSide != null && + nextAttachmentSide !== 'front' && + nextAttachmentSide !== railingMode + ? (['front', railingMode] as StairRailPathSide[]) + : [railingMode] + + return { + layout, + sidePaths: sideCandidates.map((side) => buildSegmentRailPath(layouts, index, side, landingInset)), + connectFromPrevious: + index > 0 && + !suppressLandingRailing && + sideCandidates.length > 0 && + (layout.segment.segmentType === 'landing' ? landingContinuesOnPreferredSide : true), + } + }) +} + +function resolveLandingChainNextStair(layouts: StairRailLayout[], index: number): LandingChainNextStair { + const layout = layouts[index] + if (!layout || layout.segment.segmentType !== 'landing') { + return { isTerminalLandingBeforeStair: false } + } + + let cursor = index + while (cursor + 1 < layouts.length && layouts[cursor + 1]?.segment.segmentType === 'landing') { + cursor += 1 + } + + const nextStairLayout = + cursor + 1 < layouts.length && layouts[cursor + 1]?.segment.segmentType === 'stair' + ? layouts[cursor + 1] + : undefined + + return { + nextStairLayout, + isTerminalLandingBeforeStair: Boolean(nextStairLayout) && cursor === index, + } +} + +function computeStairRailLayouts(segments: StairSegmentNode[]): StairRailLayout[] { + const transforms = computeSegmentTransforms(segments) + return segments.map((segment, index) => { + const transform = transforms[index]! + const [centerOffsetX, centerOffsetZ] = rotateXZ(0, segment.length / 2, transform.rotation) + return { + center: [transform.position[0] + centerOffsetX, transform.position[2] + centerOffsetZ], + elevation: transform.position[1], + rotation: transform.rotation, + segment, + } + }) +} + +function buildSegmentRailPath( + layouts: StairRailLayout[], + layoutIndex: number, + side: StairRailPathSide, + landingInset: number, +): StairRailSidePath { + const layout = layouts[layoutIndex]! + const segment = layout.segment + const previousLayout = layoutIndex > 0 ? layouts[layoutIndex - 1] : undefined + const nextLayout = layoutIndex >= 0 ? layouts[layoutIndex + 1] : undefined + const steps = Math.max(1, segment.segmentType === 'landing' ? 1 : segment.stepCount) + const stepDepth = segment.length / steps + const stepHeight = segment.segmentType === 'landing' ? 0 : segment.height / steps + const flightSideOffset = side === 'left' ? segment.width / 2 - 0.045 : -segment.width / 2 + 0.045 + const flightStartX = + previousLayout?.segment.segmentType === 'landing' ? -segment.length / 2 + landingInset : -segment.length / 2 + const flightEndX = + nextLayout?.segment.segmentType === 'landing' ? segment.length / 2 - landingInset : segment.length / 2 + const landingFrontX = + previousLayout?.segment.segmentType === 'stair' && + segment.attachmentSide && + segment.attachmentSide !== 'front' + ? -segment.length / 2 + landingInset + : segment.length / 2 - landingInset + + if (segment.segmentType === 'landing') { + const backX = -segment.length / 2 + landingInset + const frontX = segment.length / 2 - landingInset + const leftZ = segment.width / 2 - landingInset + const rightZ = -segment.width / 2 + landingInset + + return { + side, + points: + side === 'left' + ? [ + [backX, 0, leftZ], + [frontX, 0, leftZ], + ] + : side === 'right' + ? [ + [backX, 0, rightZ], + [frontX, 0, rightZ], + ] + : [ + [landingFrontX, 0, leftZ], + [landingFrontX, 0, rightZ], + ], + } + } + + return { + side, + points: [ + ...(previousLayout?.segment.segmentType === 'landing' ? [] : ([[flightStartX, stepHeight > 0 ? stepHeight : 0, flightSideOffset]] as [number, number, number][])), + ...Array.from({ length: steps }).map( + (_, index) => + [ + -segment.length / 2 + stepDepth * index + stepDepth / 2, + stepHeight * (index + 1), + flightSideOffset, + ] as [number, number, number], + ), + ...(nextLayout?.segment.segmentType === 'landing' + ? [] + : ([[flightEndX, segment.height, flightSideOffset]] as [number, number, number][])), + ], + } +} + +function toWorldRailPoint(layout: StairRailLayout, point: [number, number, number]): [number, number, number] { + const [localX, localY, localZ] = point + const [offsetX, offsetZ] = rotateXZ(localZ, localX, layout.rotation) + return [layout.center[0] + offsetX, layout.elevation + localY, layout.center[1] + offsetZ] +} + +function computeSegmentTransforms(segments: StairSegmentNode[]): SegmentTransform[] { + const transforms: SegmentTransform[] = [] + let currentPos = new THREE.Vector3(0, 0, 0) + let currentRot = 0 + + for (let i = 0; i < segments.length; i++) { + const segment = segments[i]! + + if (i === 0) { + transforms.push({ position: [currentPos.x, currentPos.y, currentPos.z], rotation: currentRot }) + continue + } + + const prev = segments[i - 1]! + const localAttachPos = new THREE.Vector3() + let rotChange = 0 + + switch (segment.attachmentSide) { + case 'front': + localAttachPos.set(0, prev.height, prev.length) + break + case 'left': + localAttachPos.set(prev.width / 2, prev.height, prev.length / 2) + rotChange = Math.PI / 2 + break + case 'right': + localAttachPos.set(-prev.width / 2, prev.height, prev.length / 2) + rotChange = -Math.PI / 2 + break + } + + localAttachPos.applyAxisAngle(new THREE.Vector3(0, 1, 0), currentRot) + currentPos = currentPos.clone().add(localAttachPos) + currentRot += rotChange + + transforms.push({ position: [currentPos.x, currentPos.y, currentPos.z], rotation: currentRot }) + } + + return transforms +} + +function rotateXZ(x: number, z: number, angle: number): [number, number] { + const cos = Math.cos(angle) + const sin = Math.sin(angle) + return [x * cos + z * sin, -x * sin + z * cos] +} + +function distance3(a: [number, number, number], b: [number, number, number]) { + return Math.hypot(a[0] - b[0], a[1] - b[1], a[2] - b[2]) +} diff --git a/packages/viewer/src/components/viewer/selection-manager.tsx b/packages/viewer/src/components/viewer/selection-manager.tsx index 9174d02d..81424157 100644 --- a/packages/viewer/src/components/viewer/selection-manager.tsx +++ b/packages/viewer/src/components/viewer/selection-manager.tsx @@ -29,6 +29,7 @@ type SelectableNodeType = | 'level' | 'zone' | 'wall' + | 'fence' | 'window' | 'door' | 'item' @@ -138,6 +139,13 @@ const isNodeInZone = (node: AnyNode, levelId: string, zoneId: string): boolean = return startIn || endIn } + if (node.type === 'fence') { + const fence = node as { start: [number, number]; end: [number, number] } + const startIn = pointInPolygonWithTolerance(fence.start[0], fence.start[1], zone.polygon) + const endIn = pointInPolygonWithTolerance(fence.end[0], fence.end[1], zone.polygon) + return startIn || endIn + } + if (node.type === 'slab' || node.type === 'ceiling') { const poly = (node as { polygon: [number, number][] }).polygon if (!poly?.length) return false @@ -221,7 +229,7 @@ const getStrategy = (): SelectionStrategy | null => { // Zone selected -> can select/hover contents (walls, items, slabs, ceilings, roofs, windows, doors) return { - types: ['wall', 'item', 'slab', 'ceiling', 'roof', 'roof-segment', 'window', 'door'], + types: ['wall', 'fence', 'item', 'slab', 'ceiling', 'roof', 'roof-segment', 'window', 'door'], handleClick: (node, nativeEvent) => { let nodeToSelect = node if (node.type === 'roof-segment' && node.parentId) { @@ -248,6 +256,7 @@ const getStrategy = (): SelectionStrategy | null => { isValid: (node) => { const validTypes = [ 'wall', + 'fence', 'item', 'slab', 'ceiling', @@ -303,6 +312,7 @@ export const SelectionManager = () => { 'level', 'zone', 'wall', + 'fence', 'item', 'slab', 'ceiling', diff --git a/packages/viewer/src/hooks/use-node-events.ts b/packages/viewer/src/hooks/use-node-events.ts index 46cec9d3..d247ece1 100644 --- a/packages/viewer/src/hooks/use-node-events.ts +++ b/packages/viewer/src/hooks/use-node-events.ts @@ -7,6 +7,8 @@ import { type DoorNode, type EventSuffix, emitter, + type FenceEvent, + type FenceNode, type ItemEvent, type ItemNode, type LevelEvent, @@ -37,6 +39,7 @@ type NodeConfig = { site: { node: SiteNode; event: SiteEvent } item: { node: ItemNode; event: ItemEvent } wall: { node: WallNode; event: WallEvent } + fence: { node: FenceNode; event: FenceEvent } building: { node: BuildingNode; event: BuildingEvent } level: { node: LevelNode; event: LevelEvent } zone: { node: ZoneNode; event: ZoneEvent } From 3a8d9dcde15b3045927f201e475a297a6ac6d7d3 Mon Sep 17 00:00:00 2001 From: sudhir Date: Mon, 13 Apr 2026 13:20:41 +0530 Subject: [PATCH 2/7] feat: added spiral and curved stairs with bug fix for fence --- apps/editor/public/icons/fence.png | Bin 0 -> 49495 bytes packages/core/src/schema/index.ts | 2 +- packages/core/src/schema/nodes/fence.ts | 2 +- packages/core/src/schema/nodes/stair.ts | 34 +- packages/core/src/store/use-scene.ts | 4 +- .../core/src/systems/stair/stair-system.tsx | 5 + .../editor/floating-action-menu.tsx | 57 ++-- .../src/components/editor/floorplan-panel.tsx | 20 +- .../systems/stair/stair-edit-system.tsx | 32 +- .../src/components/tools/door/door-math.ts | 2 +- .../tools/select/box-select-tool.tsx | 2 +- .../components/tools/stair/stair-defaults.ts | 8 + .../src/components/tools/stair/stair-tool.tsx | 19 ++ .../components/tools/window/window-math.ts | 2 +- .../ui/action-menu/structure-tools.tsx | 2 +- .../src/components/ui/panels/stair-panel.tsx | 210 +++++++++++-- .../panels/site-panel/fence-tree-node.tsx | 62 ++++ .../sidebar/panels/site-panel/tree-node.tsx | 3 + .../renderers/fence/fence-renderer.tsx | 2 +- .../renderers/stair/stair-renderer.tsx | 294 +++++++++++++++++- 20 files changed, 683 insertions(+), 79 deletions(-) create mode 100644 apps/editor/public/icons/fence.png create mode 100644 packages/editor/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx diff --git a/apps/editor/public/icons/fence.png b/apps/editor/public/icons/fence.png new file mode 100644 index 0000000000000000000000000000000000000000..854ed7a25f1ee97d3c6256a66e07f840d8c6e390 GIT binary patch literal 49495 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¨$EO|3joW9(fKkZ=`ajGN&pJ)SL>#kh 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 literal 0 HcmV?d00001 diff --git a/packages/core/src/schema/index.ts b/packages/core/src/schema/index.ts index 51e1bffb..26da7c49 100644 --- a/packages/core/src/schema/index.ts +++ b/packages/core/src/schema/index.ts @@ -37,7 +37,7 @@ export { ScanNode } from './nodes/scan' // Nodes export { SiteNode } from './nodes/site' export { SlabNode } from './nodes/slab' -export { StairNode, StairRailingMode } 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' diff --git a/packages/core/src/schema/nodes/fence.ts b/packages/core/src/schema/nodes/fence.ts index 6f7fd213..3a42e9d8 100644 --- a/packages/core/src/schema/nodes/fence.ts +++ b/packages/core/src/schema/nodes/fence.ts @@ -16,7 +16,7 @@ export const FenceNode = BaseNode.extend({ postSpacing: z.number().default(2), postSize: z.number().default(0.1), topRailHeight: z.number().default(0.04), - groundClearance: 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'), diff --git a/packages/core/src/schema/nodes/stair.ts b/packages/core/src/schema/nodes/stair.ts index f17780b7..9e3df380 100644 --- a/packages/core/src/schema/nodes/stair.ts +++ b/packages/core/src/schema/nodes/stair.ts @@ -5,8 +5,12 @@ 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 +export type StairType = z.infer +export type StairTopLandingMode = z.infer export const StairNode = BaseNode.extend({ id: objectId('stair'), @@ -15,6 +19,18 @@ 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 @@ -22,13 +38,25 @@ export const StairNode = BaseNode.extend({ }).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 + - 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 + - children: array of StairSegmentNode IDs for straight stairs `, ) diff --git a/packages/core/src/store/use-scene.ts b/packages/core/src/store/use-scene.ts index d9cdc2d2..83803afa 100644 --- a/packages/core/src/store/use-scene.ts +++ b/packages/core/src/store/use-scene.ts @@ -378,8 +378,8 @@ useScene.temporal.subscribe((state) => { // Mark sibling nodes dirty so they can update their geometry // (e.g. adjacent walls need to recalculate miter/junction geometry) const parent = currentNodes[parentId] - if (parent && 'children' in parent) { - for (const childId of (parent as AnyNode & { children: string[] }).children) { + if (parent && 'children' in parent && Array.isArray(parent.children)) { + for (const childId of parent.children) { markDirty(childId as AnyNodeId) } } diff --git a/packages/core/src/systems/stair/stair-system.tsx b/packages/core/src/systems/stair/stair-system.tsx index 01deba6b..c799e9d1 100644 --- a/packages/core/src/systems/stair/stair-system.tsx +++ b/packages/core/src/systems/stair/stair-system.tsx @@ -304,6 +304,11 @@ function updateMergedStairGeometry( const mergedMesh = group.getObjectByName('merged-stair') as THREE.Mesh | undefined if (!mergedMesh) return + if (stairNode.stairType === 'curved' || stairNode.stairType === 'spiral') { + replaceMeshGeometry(mergedMesh, createEmptyGeometry()) + return + } + const children = stairNode.children ?? [] const segments = children .map((childId) => nodes[childId as AnyNodeId] as StairSegmentNode | undefined) diff --git a/packages/editor/src/components/editor/floating-action-menu.tsx b/packages/editor/src/components/editor/floating-action-menu.tsx index 574b318b..04a84b73 100755 --- a/packages/editor/src/components/editor/floating-action-menu.tsx +++ b/packages/editor/src/components/editor/floating-action-menu.tsx @@ -118,6 +118,9 @@ export function FloatingActionMenu() { } else if (node.type === 'roof-segment') { duplicate = RoofSegmentNode.parse(duplicateInfo) } else if (node.type === 'stair') { + duplicateInfo.children = [] + duplicateInfo.metadata = { ...duplicateInfo.metadata } + delete duplicateInfo.metadata?.isNew duplicate = StairNode.parse(duplicateInfo) } else if (node.type === 'stair-segment') { duplicate = StairSegmentNode.parse(duplicateInfo) @@ -146,7 +149,35 @@ export function FloatingActionMenu() { duplicate.position[2] + 1, ] } - useScene.getState().createNode(duplicate, duplicate.parentId as AnyNodeId) + if (node.type === 'stair' && duplicate.type === 'stair') { + const nodesState = useScene.getState().nodes + const createOps: { node: AnyNode; parentId?: AnyNodeId }[] = [ + { node: duplicate, parentId: duplicate.parentId as AnyNodeId }, + ] + + for (const childId of node.children ?? []) { + const childNode = nodesState[childId] + if (childNode?.type !== 'stair-segment') { + continue + } + + let childDuplicateInfo = structuredClone(childNode) as any + delete childDuplicateInfo.id + childDuplicateInfo.metadata = { ...childDuplicateInfo.metadata } + delete childDuplicateInfo.metadata?.isNew + + try { + const childDuplicate = StairSegmentNode.parse(childDuplicateInfo) + createOps.push({ node: childDuplicate, parentId: duplicate.id as AnyNodeId }) + } catch (e) { + console.error('Failed to duplicate stair segment', e) + } + } + + useScene.getState().createNodes(createOps) + } else { + useScene.getState().createNode(duplicate, duplicate.parentId as AnyNodeId) + } // Duplicate children for roof nodes if (node.type === 'roof' && node.children) { @@ -168,23 +199,6 @@ export function FloatingActionMenu() { } // Duplicate children for stair nodes - if (node.type === 'stair' && node.children) { - const nodesState = useScene.getState().nodes - for (const childId of node.children) { - const childNode = nodesState[childId] - if (childNode && childNode.type === 'stair-segment') { - let childDuplicateInfo = structuredClone(childNode) as any - delete childDuplicateInfo.id - childDuplicateInfo.metadata = { ...childDuplicateInfo.metadata, isNew: true } - try { - const childDuplicate = StairSegmentNode.parse(childDuplicateInfo) - useScene.getState().createNode(childDuplicate, duplicate.id as AnyNodeId) - } catch (e) { - console.error('Failed to duplicate stair segment', e) - } - } - } - } } if ( duplicate.type === 'item' || @@ -193,12 +207,15 @@ export function FloatingActionMenu() { duplicate.type === 'door' || duplicate.type === 'roof' || duplicate.type === 'roof-segment' || - duplicate.type === 'stair' || duplicate.type === 'stair-segment' ) { setMovingNode(duplicate as any) + } else if (duplicate.type === 'stair') { + setSelection({ selectedIds: [duplicate.id as AnyNodeId] }) + } + if (duplicate.type !== 'stair') { + setSelection({ selectedIds: [] }) } - setSelection({ selectedIds: [] }) } }, [node, setMovingNode, setSelection], diff --git a/packages/editor/src/components/editor/floorplan-panel.tsx b/packages/editor/src/components/editor/floorplan-panel.tsx index 37133fdd..b9701612 100644 --- a/packages/editor/src/components/editor/floorplan-panel.tsx +++ b/packages/editor/src/components/editor/floorplan-panel.tsx @@ -8063,6 +8063,7 @@ export function FloorplanPanel() { ...(typeof cloned.metadata === 'object' && cloned.metadata !== null ? cloned.metadata : {}), isNew: true, } + cloned.children = [] try { const duplicate = ItemNodeSchema.parse(cloned) @@ -8191,8 +8192,8 @@ export function FloorplanPanel() { delete cloned.id cloned.metadata = { ...(typeof cloned.metadata === 'object' && cloned.metadata !== null ? cloned.metadata : {}), - isNew: true, } + delete (cloned.metadata as Record).isNew const nextPosition = Array.isArray(cloned.position) && cloned.position.length >= 3 @@ -8207,9 +8208,11 @@ export function FloorplanPanel() { try { const duplicate = StairNodeSchema.parse(cloned) - useScene.getState().createNode(duplicate, stair.parentId as AnyNodeId) - const nodesState = useScene.getState().nodes + const createOps: { node: AnyNode; parentId?: AnyNodeId }[] = [ + { node: duplicate, parentId: stair.parentId as AnyNodeId }, + ] + for (const childId of stair.children ?? []) { const childNode = nodesState[childId] if (childNode?.type !== 'stair-segment') { @@ -8222,19 +8225,20 @@ export function FloorplanPanel() { ...(typeof childClone.metadata === 'object' && childClone.metadata !== null ? childClone.metadata : {}), - isNew: true, } + delete (childClone.metadata as Record).isNew const childDuplicate = StairSegmentNodeSchema.parse(childClone) - useScene.getState().createNode(childDuplicate, duplicate.id as AnyNodeId) + createOps.push({ node: childDuplicate, parentId: duplicate.id as AnyNodeId }) } - setMovingNode(duplicate) - setSelection({ selectedIds: [] }) + useScene.getState().createNodes(createOps) + + setSelection({ selectedIds: [duplicate.id as AnyNodeId] }) } catch (error) { console.error('Failed to duplicate stair', error) } - }, [selectedStairEntry, setMovingNode, setSelection]) + }, [selectedStairEntry, setSelection]) const handleSelectedStairDuplicate = useCallback( (event: ReactMouseEvent) => { event.stopPropagation() diff --git a/packages/editor/src/components/systems/stair/stair-edit-system.tsx b/packages/editor/src/components/systems/stair/stair-edit-system.tsx index 0edd1797..265d57b5 100644 --- a/packages/editor/src/components/systems/stair/stair-edit-system.tsx +++ b/packages/editor/src/components/systems/stair/stair-edit-system.tsx @@ -1,6 +1,6 @@ import { type AnyNodeId, type StairNode, sceneRegistry, useScene } from '@pascal-app/core' import { useViewer } from '@pascal-app/viewer' -import { useEffect, useRef } from 'react' +import { useCallback, useEffect, useRef } from 'react' /** * Imperatively toggles the Three.js visibility of stair objects based on the @@ -17,6 +17,27 @@ import { useEffect, useRef } from 'react' */ export const StairEditSystem = () => { const selectedIds = useViewer((s) => s.selection.selectedIds) + const selectedStairSignature = useScene( + useCallback( + (state) => + selectedIds + .map((id) => { + const node = state.nodes[id as AnyNodeId] + if (!node) return null + if (node.type === 'stair') { + return `${node.id}:${node.stairType}` + } + if (node.type === 'stair-segment' && node.parentId) { + const parent = state.nodes[node.parentId as AnyNodeId] as StairNode | undefined + return parent?.type === 'stair' ? `${parent.id}:${parent.stairType}` : null + } + return null + }) + .filter(Boolean) + .join('|'), + [selectedIds], + ), + ) const prevActiveStairIds = useRef(new Set()) useEffect(() => { @@ -41,14 +62,15 @@ export const StairEditSystem = () => { const group = sceneRegistry.nodes.get(stairId) if (!group) continue + const stairNode = nodes[stairId as AnyNodeId] as StairNode | undefined + const isCurved = stairNode?.stairType === 'curved' || stairNode?.stairType === 'spiral' const mergedMesh = group.getObjectByName('merged-stair') const segmentsWrapper = group.getObjectByName('segments-wrapper') const isActive = activeStairIds.has(stairId) - if (mergedMesh) mergedMesh.visible = !isActive - if (segmentsWrapper) segmentsWrapper.visible = isActive + if (mergedMesh) mergedMesh.visible = !isActive && !isCurved + if (segmentsWrapper) segmentsWrapper.visible = isActive && !isCurved - const stairNode = nodes[stairId as AnyNodeId] as StairNode | undefined if (stairNode?.children?.length) { const wasActive = prevActiveStairIds.current.has(stairId) if (isActive !== wasActive) { @@ -63,7 +85,7 @@ export const StairEditSystem = () => { } prevActiveStairIds.current = activeStairIds - }, [selectedIds]) + }, [selectedIds, selectedStairSignature]) return null } diff --git a/packages/editor/src/components/tools/door/door-math.ts b/packages/editor/src/components/tools/door/door-math.ts index 2fa5d03c..9a2dc9df 100644 --- a/packages/editor/src/components/tools/door/door-math.ts +++ b/packages/editor/src/components/tools/door/door-math.ts @@ -70,7 +70,7 @@ export function hasWallChildOverlap( const newLeft = clampedX - halfW const newRight = clampedX + halfW - for (const childId of wallNode.children) { + for (const childId of Array.isArray(wallNode.children) ? wallNode.children : []) { if (childId === ignoreId) continue const child = nodes[childId as AnyNodeId] if (!child) continue diff --git a/packages/editor/src/components/tools/select/box-select-tool.tsx b/packages/editor/src/components/tools/select/box-select-tool.tsx index 79242c28..8dd700a6 100644 --- a/packages/editor/src/components/tools/select/box-select-tool.tsx +++ b/packages/editor/src/components/tools/select/box-select-tool.tsx @@ -205,7 +205,7 @@ function collectNodeIdsInBounds(bounds: Bounds): string[] { result.push(wall.id) } // Check wall children (doors/windows) - for (const itemId of wall.children) { + for (const itemId of Array.isArray(wall.children) ? wall.children : []) { const child = nodes[itemId as AnyNodeId] if (!child) continue if ( diff --git a/packages/editor/src/components/tools/stair/stair-defaults.ts b/packages/editor/src/components/tools/stair/stair-defaults.ts index e2dd23cf..ace03f5e 100644 --- a/packages/editor/src/components/tools/stair/stair-defaults.ts +++ b/packages/editor/src/components/tools/stair/stair-defaults.ts @@ -1,3 +1,4 @@ +export const DEFAULT_STAIR_TYPE = 'straight' as const export const DEFAULT_STAIR_WIDTH = 1.0 export const DEFAULT_STAIR_LENGTH = 3.0 export const DEFAULT_STAIR_HEIGHT = 2.5 @@ -5,5 +6,12 @@ export const DEFAULT_STAIR_STEP_COUNT = 10 export const DEFAULT_STAIR_ATTACHMENT_SIDE = 'front' as const export const DEFAULT_STAIR_FILL_TO_FLOOR = true export const DEFAULT_STAIR_THICKNESS = 0.25 +export const DEFAULT_CURVED_STAIR_INNER_RADIUS = 0.9 +export const DEFAULT_CURVED_STAIR_SWEEP_ANGLE = Math.PI / 2 +export const DEFAULT_SPIRAL_STAIR_SWEEP_ANGLE = (400 * Math.PI) / 180 +export const DEFAULT_SPIRAL_TOP_LANDING_MODE = 'none' as const +export const DEFAULT_SPIRAL_TOP_LANDING_DEPTH = 0.9 +export const DEFAULT_SPIRAL_SHOW_CENTER_COLUMN = true +export const DEFAULT_SPIRAL_SHOW_STEP_SUPPORTS = true export const DEFAULT_STAIR_RAILING_MODE = 'right' as const export const DEFAULT_STAIR_RAILING_HEIGHT = 0.92 diff --git a/packages/editor/src/components/tools/stair/stair-tool.tsx b/packages/editor/src/components/tools/stair/stair-tool.tsx index a3ddfa8f..2ed9031a 100644 --- a/packages/editor/src/components/tools/stair/stair-tool.tsx +++ b/packages/editor/src/components/tools/stair/stair-tool.tsx @@ -13,6 +13,12 @@ import * as THREE from 'three' import { sfxEmitter } from '../../../lib/sfx-bus' import { CursorSphere } from '../shared/cursor-sphere' import { + DEFAULT_CURVED_STAIR_INNER_RADIUS, + DEFAULT_CURVED_STAIR_SWEEP_ANGLE, + DEFAULT_SPIRAL_SHOW_CENTER_COLUMN, + DEFAULT_SPIRAL_SHOW_STEP_SUPPORTS, + DEFAULT_SPIRAL_TOP_LANDING_DEPTH, + DEFAULT_SPIRAL_TOP_LANDING_MODE, DEFAULT_STAIR_ATTACHMENT_SIDE, DEFAULT_STAIR_FILL_TO_FLOOR, DEFAULT_STAIR_HEIGHT, @@ -21,6 +27,7 @@ import { DEFAULT_STAIR_RAILING_MODE, DEFAULT_STAIR_STEP_COUNT, DEFAULT_STAIR_THICKNESS, + DEFAULT_STAIR_TYPE, DEFAULT_STAIR_WIDTH, } from './stair-defaults' @@ -90,6 +97,18 @@ function commitStairPlacement( name, position, rotation, + stairType: DEFAULT_STAIR_TYPE, + width: DEFAULT_STAIR_WIDTH, + totalRise: DEFAULT_STAIR_HEIGHT, + stepCount: DEFAULT_STAIR_STEP_COUNT, + thickness: DEFAULT_STAIR_THICKNESS, + fillToFloor: DEFAULT_STAIR_FILL_TO_FLOOR, + innerRadius: DEFAULT_CURVED_STAIR_INNER_RADIUS, + sweepAngle: DEFAULT_CURVED_STAIR_SWEEP_ANGLE, + topLandingMode: DEFAULT_SPIRAL_TOP_LANDING_MODE, + topLandingDepth: DEFAULT_SPIRAL_TOP_LANDING_DEPTH, + showCenterColumn: DEFAULT_SPIRAL_SHOW_CENTER_COLUMN, + showStepSupports: DEFAULT_SPIRAL_SHOW_STEP_SUPPORTS, railingHeight: DEFAULT_STAIR_RAILING_HEIGHT, railingMode: DEFAULT_STAIR_RAILING_MODE, children: [segment.id], diff --git a/packages/editor/src/components/tools/window/window-math.ts b/packages/editor/src/components/tools/window/window-math.ts index 842634a2..182611a2 100644 --- a/packages/editor/src/components/tools/window/window-math.ts +++ b/packages/editor/src/components/tools/window/window-math.ts @@ -77,7 +77,7 @@ export function hasWallChildOverlap( const newLeft = clampedX - halfW const newRight = clampedX + halfW - for (const childId of wallNode.children) { + for (const childId of Array.isArray(wallNode.children) ? wallNode.children : []) { if (childId === ignoreId) continue const child = nodes[childId as AnyNodeId] if (!child) continue diff --git a/packages/editor/src/components/ui/action-menu/structure-tools.tsx b/packages/editor/src/components/ui/action-menu/structure-tools.tsx index 71f081df..d0cf44b5 100644 --- a/packages/editor/src/components/ui/action-menu/structure-tools.tsx +++ b/packages/editor/src/components/ui/action-menu/structure-tools.tsx @@ -20,7 +20,6 @@ export type ToolConfig = { export const tools: ToolConfig[] = [ { id: 'wall', iconSrc: '/icons/wall.png', label: 'Wall' }, - { id: 'fence', iconSrc: '/icons/build.png', label: 'Fence' }, // { id: 'room', iconSrc: '/icons/room.png', label: 'Room' }, // { id: 'custom-room', iconSrc: '/icons/custom-room.png', label: 'Custom Room' }, { id: 'slab', iconSrc: '/icons/floor.png', label: 'Slab' }, @@ -29,6 +28,7 @@ export const tools: ToolConfig[] = [ { id: 'stair', iconSrc: '/icons/stairs.png', label: 'Stairs' }, { id: 'door', iconSrc: '/icons/door.png', label: 'Door' }, { id: 'window', iconSrc: '/icons/window.png', label: 'Window' }, + { id: 'fence', iconSrc: '/icons/fence.png', label: 'Fence' }, { id: 'zone', iconSrc: '/icons/zone.png', label: 'Zone' }, ] diff --git a/packages/editor/src/components/ui/panels/stair-panel.tsx b/packages/editor/src/components/ui/panels/stair-panel.tsx index f48f2ef3..c91e9684 100644 --- a/packages/editor/src/components/ui/panels/stair-panel.tsx +++ b/packages/editor/src/components/ui/panels/stair-panel.tsx @@ -6,6 +6,8 @@ import { type MaterialSchema, type StairNode, type StairRailingMode, + type StairTopLandingMode, + type StairType, StairNode as StairNodeSchema, type StairSegmentNode, StairSegmentNode as StairSegmentNodeSchema, @@ -16,12 +18,14 @@ import { Copy, Move, Plus, Trash2 } from 'lucide-react' import { useCallback } from 'react' import { sfxEmitter } from '../../../lib/sfx-bus' import useEditor from '../../../store/use-editor' +import { DEFAULT_SPIRAL_STAIR_SWEEP_ANGLE } from '../../tools/stair/stair-defaults' import { ActionButton, ActionGroup } from '../controls/action-button' import { MaterialPicker } from '../controls/material-picker' import { MetricControl } from '../controls/metric-control' import { PanelSection } from '../controls/panel-section' import { SegmentedControl } from '../controls/segmented-control' import { SliderControl } from '../controls/slider-control' +import { ToggleControl } from '../controls/toggle-control' import { PanelWrapper } from './panel-wrapper' const RAILING_MODE_OPTIONS: { label: string; value: StairRailingMode }[] = [ @@ -31,12 +35,24 @@ const RAILING_MODE_OPTIONS: { label: string; value: StairRailingMode }[] = [ { label: 'Both', value: 'both' }, ] +const STAIR_TYPE_OPTIONS: { label: string; value: StairType }[] = [ + { label: 'Straight', value: 'straight' }, + { label: 'Curved', value: 'curved' }, + { label: 'Spiral', value: 'spiral' }, +] + +const TOP_LANDING_MODE_OPTIONS: { label: string; value: StairTopLandingMode }[] = [ + { label: 'None', value: 'none' }, + { label: 'Integrated', value: 'integrated' }, +] + export function StairPanel() { const selectedIds = useViewer((s) => s.selection.selectedIds) const setSelection = useViewer((s) => s.setSelection) const nodes = useScene((s) => s.nodes) const updateNode = useScene((s) => s.updateNode) const createNode = useScene((s) => s.createNode) + const createNodes = useScene((s) => s.createNodes) const setMovingNode = useEditor((s) => s.setMovingNode) const selectedId = selectedIds[0] @@ -123,7 +139,8 @@ export function StairPanel() { let duplicateInfo = structuredClone(node) as any delete duplicateInfo.id - duplicateInfo.metadata = { ...duplicateInfo.metadata, isNew: true } + duplicateInfo.metadata = { ...duplicateInfo.metadata } + duplicateInfo.children = [] duplicateInfo.position = [ duplicateInfo.position[0] + 1, duplicateInfo.position[1], @@ -132,29 +149,31 @@ export function StairPanel() { try { const duplicate = StairNodeSchema.parse(duplicateInfo) - useScene.getState().createNode(duplicate, duplicate.parentId as AnyNodeId) - // Also duplicate all child segments const nodesState = useScene.getState().nodes const children = node.children || [] + const createOps: { node: AnyNode; parentId?: AnyNodeId }[] = [ + { node: duplicate, parentId: duplicate.parentId as AnyNodeId }, + ] for (const childId of children) { const childNode = nodesState[childId] if (childNode && childNode.type === 'stair-segment') { let childDuplicateInfo = structuredClone(childNode) as any delete childDuplicateInfo.id - childDuplicateInfo.metadata = { ...childDuplicateInfo.metadata, isNew: true } + childDuplicateInfo.metadata = { ...childDuplicateInfo.metadata } const childDuplicate = StairSegmentNodeSchema.parse(childDuplicateInfo) - useScene.getState().createNode(childDuplicate, duplicate.id as AnyNodeId) + createOps.push({ node: childDuplicate, parentId: duplicate.id as AnyNodeId }) } } - setSelection({ selectedIds: [] }) - setMovingNode(duplicate) + createNodes(createOps) + + setSelection({ selectedIds: [duplicate.id as AnyNode['id']] }) } catch (e) { console.error('Failed to duplicate stair', e) } - }, [node, setSelection, setMovingNode]) + }, [createNodes, node, setSelection]) const handleMove = useCallback(() => { if (node) { @@ -188,33 +207,158 @@ export function StairPanel() { title={node.name || 'Staircase'} width={300} > - -
- {segments.map((seg, i) => ( - - ))} -
-
- } - label="Add flight" - onClick={handleAddFlight} + + + handleUpdate( + value === 'spiral' && node.stairType !== 'spiral' + ? { + stairType: value, + sweepAngle: DEFAULT_SPIRAL_STAIR_SWEEP_ANGLE, + position: [node.position[0], 0, node.position[2]], + } + : { stairType: value }, + ) + } + options={STAIR_TYPE_OPTIONS} + value={node.stairType ?? 'straight'} + /> + + + {node.stairType === 'straight' && ( + +
+ {segments.map((seg, i) => ( + + ))} +
+
+ } + label="Add flight" + onClick={handleAddFlight} + /> + } + label="Add landing" + onClick={handleAddLanding} + /> +
+
+ )} + + {(node.stairType === 'curved' || node.stairType === 'spiral') && ( + + handleUpdate({ width: value })} + precision={2} + step={0.05} + unit="m" + value={Math.round((node.width ?? 1) * 100) / 100} /> - } - label="Add landing" - onClick={handleAddLanding} + handleUpdate({ totalRise: value })} + precision={2} + step={0.05} + unit="m" + value={Math.round((node.totalRise ?? 2.5) * 100) / 100} /> -
-
+ handleUpdate({ stepCount: Math.max(2, Math.round(value)) })} + precision={0} + step={1} + unit="" + value={Math.max(2, Math.round(node.stepCount ?? 10))} + /> + {node.stairType !== 'spiral' && ( + handleUpdate({ fillToFloor: checked })} + /> + )} + {(node.stairType === 'spiral' || !(node.fillToFloor ?? true)) && ( + handleUpdate({ thickness: value })} + precision={2} + step={0.01} + unit="m" + value={Math.round((node.thickness ?? 0.25) * 100) / 100} + /> + )} + handleUpdate({ innerRadius: value })} + precision={2} + step={0.05} + unit="m" + value={Math.round((node.innerRadius ?? 0.9) * 100) / 100} + /> + handleUpdate({ sweepAngle: (degrees * Math.PI) / 180 })} + precision={0} + step={1} + unit="°" + value={Math.round(((node.sweepAngle ?? Math.PI / 2) * 180) / Math.PI)} + /> + {node.stairType === 'spiral' && ( + <> + handleUpdate({ topLandingMode: value })} + options={TOP_LANDING_MODE_OPTIONS} + value={node.topLandingMode ?? 'none'} + /> + {(node.topLandingMode ?? 'none') === 'integrated' && ( + handleUpdate({ topLandingDepth: value })} + precision={2} + step={0.05} + unit="m" + value={Math.round((node.topLandingDepth ?? 0.9) * 100) / 100} + /> + )} + handleUpdate({ showCenterColumn: checked })} + /> + handleUpdate({ showStepSupports: checked })} + /> + + )} +
+ )} state.selection.selectedIds) + const isSelected = selectedIds.includes(node.id) + const isHovered = useViewer((state) => state.hoveredId === node.id) + const setSelection = useViewer((state) => state.setSelection) + const setHoveredId = useViewer((state) => state.setHoveredId) + + const handleClick = (e: React.MouseEvent) => { + e.stopPropagation() + const handled = handleTreeSelection(e, node.id, selectedIds, setSelection) + if (!handled && useEditor.getState().phase === 'furnish') { + useEditor.getState().setPhase('structure') + } + } + + return ( + } + depth={depth} + expanded={false} + hasChildren={false} + icon={ + + } + isHovered={isHovered} + isLast={isLast} + isSelected={isSelected} + isVisible={node.visible !== false} + label={ + setIsEditing(true)} + onStopEditing={() => setIsEditing(false)} + /> + } + nodeId={node.id} + onClick={handleClick} + onDoubleClick={() => focusTreeNode(node.id)} + onMouseEnter={() => setHoveredId(node.id)} + onMouseLeave={() => setHoveredId(null)} + onToggle={() => {}} + /> + ) +} diff --git a/packages/editor/src/components/ui/sidebar/panels/site-panel/tree-node.tsx b/packages/editor/src/components/ui/sidebar/panels/site-panel/tree-node.tsx index eb3c88e8..c4106402 100644 --- a/packages/editor/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +++ b/packages/editor/src/components/ui/sidebar/panels/site-panel/tree-node.tsx @@ -57,6 +57,7 @@ import { cn } from '../../../../../lib/utils' import { BuildingTreeNode } from './building-tree-node' import { CeilingTreeNode } from './ceiling-tree-node' import { DoorTreeNode } from './door-tree-node' +import { FenceTreeNode } from './fence-tree-node' import { ItemTreeNode } from './item-tree-node' import { LevelTreeNode } from './level-tree-node' import { RoofTreeNode } from './roof-tree-node' @@ -88,6 +89,8 @@ export function TreeNode({ nodeId, depth = 0, isLast }: TreeNodeProps) { return case 'wall': return + case 'fence': + return case 'roof': return case 'stair': diff --git a/packages/viewer/src/components/renderers/fence/fence-renderer.tsx b/packages/viewer/src/components/renderers/fence/fence-renderer.tsx index 9a65afc8..7065681b 100644 --- a/packages/viewer/src/components/renderers/fence/fence-renderer.tsx +++ b/packages/viewer/src/components/renderers/fence/fence-renderer.tsx @@ -39,7 +39,7 @@ function createFenceParts(fence: FenceNode): FencePart[] { const edgeInset = Math.max(fence.edgeInset ?? 0.015, 0.005) const isFloating = fence.baseStyle === 'floating' const baseY = isFloating ? clearance : 0 - const effectiveBaseHeight = isFloating ? baseHeight : baseHeight + clearance * 0.5 + const effectiveBaseHeight = baseHeight if (!isFloating) { parts.push({ diff --git a/packages/viewer/src/components/renderers/stair/stair-renderer.tsx b/packages/viewer/src/components/renderers/stair/stair-renderer.tsx index da228de2..a9183090 100644 --- a/packages/viewer/src/components/renderers/stair/stair-renderer.tsx +++ b/packages/viewer/src/components/renderers/stair/stair-renderer.tsx @@ -64,6 +64,9 @@ export const StairRenderer = ({ node }: { node: StairNode }) => { + {node.stairType === 'curved' || node.stairType === 'spiral' ? ( + + ) : null} {(node.children ?? []).map((childId) => ( @@ -92,7 +95,84 @@ function StairRailings({ stair, material }: { stair: StairNode; material: THREE. const railRadius = 0.022 const balusterRadius = 0.018 - if ((stair.railingMode ?? 'none') === 'none' || railPaths.length === 0) { + if ((stair.railingMode ?? 'none') === 'none') { + return null + } + + if (stair.stairType === 'curved' || stair.stairType === 'spiral') { + const stepCount = Math.max(2, Math.round(stair.stepCount ?? 10)) + const sweepAngle = stair.sweepAngle ?? (stair.stairType === 'spiral' ? Math.PI * 2 : Math.PI / 2) + const stepSweep = sweepAngle / stepCount + const stepHeight = Math.max(stair.totalRise ?? 2.5, 0.1) / stepCount + const innerRadius = Math.max(stair.stairType === 'spiral' ? 0.05 : 0.2, stair.innerRadius ?? 0.9) + const outerRadius = innerRadius + Math.max(stair.width ?? 1, 0.4) + const leftRadius = sweepAngle >= 0 ? innerRadius + 0.04 : outerRadius - 0.04 + const rightRadius = sweepAngle >= 0 ? outerRadius - 0.04 : innerRadius + 0.04 + const radii = + stair.railingMode === 'both' + ? [leftRadius, rightRadius] + : stair.railingMode === 'left' + ? [leftRadius] + : stair.railingMode === 'right' + ? [rightRadius] + : [] + + return ( + + {radii.map((radius, sideIndex) => { + const sidePoints = Array.from({ length: stepCount }).map((_, index) => { + const angle = -sweepAngle / 2 + stepSweep * index + stepSweep / 2 + return [ + Math.cos(angle) * radius, + stair.stairType === 'spiral' + ? stepHeight * index + Math.max(stair.thickness ?? 0.25, 0.02) + : stepHeight * (index + 1), + Math.sin(angle) * radius, + ] as [number, number, number] + }) + + return ( + + {sidePoints.map((point, pointIndex) => ( + + ))} + {sidePoints.slice(0, -1).map((point, pointIndex) => { + const nextPoint = sidePoints[pointIndex + 1] + if (!nextPoint) return null + + return ( + + + + + ) + })} + + ) + })} + + ) + } + + if (railPaths.length === 0) { return null } @@ -232,6 +312,218 @@ function RailSegment({ ) } +function CurvedStairBody({ stair, material }: { stair: StairNode; material: THREE.Material }) { + const stepCount = Math.max(2, Math.round(stair.stepCount ?? 10)) + const totalRise = Math.max(stair.totalRise ?? 2.5, 0.1) + const stepHeight = totalRise / stepCount + const isSpiral = stair.stairType === 'spiral' + const innerRadius = Math.max(isSpiral ? 0.05 : 0.2, stair.innerRadius ?? 0.9) + const outerRadius = innerRadius + Math.max(stair.width ?? 1, 0.4) + const sweepAngle = stair.sweepAngle ?? (isSpiral ? Math.PI * 2 : Math.PI / 2) + const stepSweep = sweepAngle / stepCount + const thickness = Math.max(stair.thickness ?? 0.25, 0.02) + const fillToFloor = stair.fillToFloor ?? true + const spiralColumnRadius = Math.max(0.05, Math.min(innerRadius * 0.72, innerRadius - 0.03)) + const spiralColumnHeight = totalRise + thickness + const spiralLandingDepth = Math.max(0.3, stair.topLandingDepth ?? Math.max((stair.width ?? 1) * 0.9, 0.8)) + const spiralLandingSweep = + isSpiral && (stair.topLandingMode ?? 'none') === 'integrated' + ? Math.min(Math.PI * 0.75, spiralLandingDepth / Math.max(innerRadius + (stair.width ?? 1) / 2, 0.1)) * + Math.sign(sweepAngle || 1) + : 0 + const spiralLastStepTop = stepHeight * Math.max(stepCount - 1, 0) + thickness + const spiralLandingThickness = + isSpiral && (stair.topLandingMode ?? 'none') === 'integrated' + ? Math.max(0.02, totalRise - spiralLastStepTop) + : 0 + + return ( + + {isSpiral && (stair.showCenterColumn ?? true) ? ( + + + + ) : null} + {Array.from({ length: stepCount }).map((_, index) => { + const currentHeight = stepHeight * (index + 1) + const actualStepHeight = isSpiral ? thickness : fillToFloor ? Math.max(currentHeight, thickness) : thickness + const startAngle = -sweepAngle / 2 + stepSweep * index + const endAngle = startAngle + stepSweep + const stepY = isSpiral ? stepHeight * index : fillToFloor ? 0 : Math.max(currentHeight - thickness, 0) + const midAngle = startAngle + stepSweep / 2 + + return ( + + {isSpiral && (stair.showStepSupports ?? true) ? ( + + + + ) : null} + + + ) + })} + {isSpiral && (stair.topLandingMode ?? 'none') === 'integrated' ? ( + + ) : null} + + ) +} + +function CurvedStepMesh({ + innerRadius, + outerRadius, + startAngle, + endAngle, + stepHeight, + thickness, + positionY, + material, +}: { + innerRadius: number + outerRadius: number + startAngle: number + endAngle: number + stepHeight: number + thickness: number + positionY: number + material: THREE.Material +}) { + const geometry = useMemo( + () => buildCurvedStepGeometry(innerRadius, outerRadius, startAngle, endAngle, Math.max(stepHeight, thickness)), + [endAngle, innerRadius, outerRadius, startAngle, stepHeight, thickness], + ) + + return +} + +function buildCurvedStepGeometry( + innerRadius: number, + outerRadius: number, + startAngle: number, + endAngle: number, + height: number, +) { + const clampedHeight = Math.max(height, 0.02) + const y0 = 0 + const y1 = clampedHeight + const sweepAngle = endAngle - startAngle + const sweepDirection = Math.sign(sweepAngle) || 1 + const segmentCount = Math.max(4, Math.min(24, Math.ceil(Math.abs(sweepAngle) / (Math.PI / 18) + Math.max(0, (outerRadius - innerRadius) * 3)))) + + const positions: number[] = [] + const normals: number[] = [] + + const pointOnArc = (radius: number, angle: number, y: number) => + new THREE.Vector3(Math.cos(angle) * radius, y, Math.sin(angle) * radius) + + const pushTriangle = (a: THREE.Vector3, b: THREE.Vector3, c: THREE.Vector3, normal: THREE.Vector3) => { + const edgeAB = b.clone().sub(a) + const edgeAC = c.clone().sub(a) + const faceNormal = edgeAB.cross(edgeAC) + const ordered = faceNormal.dot(normal) >= 0 ? [a, b, c] : [a, c, b] + for (const point of ordered) { + positions.push(point.x, point.y, point.z) + normals.push(normal.x, normal.y, normal.z) + } + } + + const pushQuad = (a: THREE.Vector3, b: THREE.Vector3, c: THREE.Vector3, d: THREE.Vector3, normal: THREE.Vector3) => { + pushTriangle(a, b, c, normal) + pushTriangle(a, c, d, normal) + } + + const upNormal = new THREE.Vector3(0, 1, 0) + const downNormal = new THREE.Vector3(0, -1, 0) + + for (let index = 0; index < segmentCount; index++) { + const t0 = index / segmentCount + const t1 = (index + 1) / segmentCount + const segStart = startAngle + sweepAngle * t0 + const segEnd = startAngle + sweepAngle * t1 + const midAngle = (segStart + segEnd) / 2 + + const innerStartBottom = pointOnArc(innerRadius, segStart, y0) + const innerEndBottom = pointOnArc(innerRadius, segEnd, y0) + const outerStartBottom = pointOnArc(outerRadius, segStart, y0) + const outerEndBottom = pointOnArc(outerRadius, segEnd, y0) + const innerStartTop = pointOnArc(innerRadius, segStart, y1) + const innerEndTop = pointOnArc(innerRadius, segEnd, y1) + const outerStartTop = pointOnArc(outerRadius, segStart, y1) + const outerEndTop = pointOnArc(outerRadius, segEnd, y1) + + const outerNormal = new THREE.Vector3(Math.cos(midAngle), 0, Math.sin(midAngle)).normalize() + const innerNormal = new THREE.Vector3(-Math.cos(midAngle), 0, -Math.sin(midAngle)).normalize() + + pushQuad(innerStartTop, outerStartTop, outerEndTop, innerEndTop, upNormal) + pushQuad(innerStartBottom, innerEndBottom, outerEndBottom, outerStartBottom, downNormal) + pushQuad(innerStartBottom, innerStartTop, innerEndTop, innerEndBottom, innerNormal) + pushQuad(outerStartBottom, outerEndBottom, outerEndTop, outerStartTop, outerNormal) + } + + const startInnerBottom = pointOnArc(innerRadius, startAngle, y0) + const startOuterBottom = pointOnArc(outerRadius, startAngle, y0) + const startInnerTop = pointOnArc(innerRadius, startAngle, y1) + const startOuterTop = pointOnArc(outerRadius, startAngle, y1) + const endInnerBottom = pointOnArc(innerRadius, endAngle, y0) + const endOuterBottom = pointOnArc(outerRadius, endAngle, y0) + const endInnerTop = pointOnArc(innerRadius, endAngle, y1) + const endOuterTop = pointOnArc(outerRadius, endAngle, y1) + const startNormal = new THREE.Vector3( + sweepDirection * Math.sin(startAngle), + 0, + -sweepDirection * Math.cos(startAngle), + ).normalize() + const endNormal = new THREE.Vector3( + -sweepDirection * Math.sin(endAngle), + 0, + sweepDirection * Math.cos(endAngle), + ).normalize() + + pushQuad(startInnerBottom, startOuterBottom, startOuterTop, startInnerTop, startNormal) + pushQuad(endInnerBottom, endInnerTop, endOuterTop, endOuterBottom, endNormal) + + const geometry = new THREE.BufferGeometry() + geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3)) + geometry.setAttribute('normal', new THREE.Float32BufferAttribute(normals, 3)) + geometry.computeVertexNormals() + return geometry +} + function buildStairRailPaths( segments: StairSegmentNode[], railingMode: StairNode['railingMode'], From 522fddca2ed5c2ed2605be822f6042000f40f6ae Mon Sep 17 00:00:00 2001 From: sudhir Date: Mon, 13 Apr 2026 14:01:55 +0530 Subject: [PATCH 3/7] feat:fence are linked to each other ... so moving one move the other sharing the same coordinate --- packages/core/src/index.ts | 1 + .../core/src/store/use-live-fence-segments.ts | 34 ++++ .../editor/floating-action-menu.tsx | 2 + .../tools/fence/move-fence-tool.tsx | 154 +++++++++++++++--- .../renderers/fence/fence-renderer.tsx | 17 +- 5 files changed, 180 insertions(+), 28 deletions(-) create mode 100644 packages/core/src/store/use-live-fence-segments.ts diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index a91c8259..688ea465 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -44,6 +44,7 @@ export { type ItemInteractiveState, useInteractive, } from './store/use-interactive' +export { default as useLiveFenceSegments, type LiveFenceSegment } from './store/use-live-fence-segments' export { default as useLiveTransforms, type LiveTransform } from './store/use-live-transforms' export { clearSceneHistory, default as useScene } from './store/use-scene' export { CeilingSystem } from './systems/ceiling/ceiling-system' diff --git a/packages/core/src/store/use-live-fence-segments.ts b/packages/core/src/store/use-live-fence-segments.ts new file mode 100644 index 00000000..dc1bd0d8 --- /dev/null +++ b/packages/core/src/store/use-live-fence-segments.ts @@ -0,0 +1,34 @@ +import { create } from 'zustand' + +export type LiveFenceSegment = { + start: [number, number] + end: [number, number] +} + +type LiveFenceSegmentState = { + segments: Map + set: (nodeId: string, segment: LiveFenceSegment) => void + get: (nodeId: string) => LiveFenceSegment | undefined + clear: (nodeId: string) => void + clearAll: () => void +} + +const useLiveFenceSegments = create((set, get) => ({ + segments: new Map(), + set: (nodeId, segment) => + set((state) => { + const next = new Map(state.segments) + next.set(nodeId, segment) + return { segments: next } + }), + get: (nodeId) => get().segments.get(nodeId), + clear: (nodeId) => + set((state) => { + const next = new Map(state.segments) + next.delete(nodeId) + return { segments: next } + }), + clearAll: () => set({ segments: new Map() }), +})) + +export default useLiveFenceSegments diff --git a/packages/editor/src/components/editor/floating-action-menu.tsx b/packages/editor/src/components/editor/floating-action-menu.tsx index 04a84b73..f44206ae 100755 --- a/packages/editor/src/components/editor/floating-action-menu.tsx +++ b/packages/editor/src/components/editor/floating-action-menu.tsx @@ -113,6 +113,8 @@ export function FloatingActionMenu() { duplicate = ItemNode.parse(duplicateInfo) } else if (node.type === 'fence') { duplicate = FenceNode.parse(duplicateInfo) + duplicate.start = [duplicate.start[0] + 1, duplicate.start[1] + 1] + duplicate.end = [duplicate.end[0] + 1, duplicate.end[1] + 1] } else if (node.type === 'roof') { duplicate = RoofNode.parse(duplicateInfo) } else if (node.type === 'roof-segment') { diff --git a/packages/editor/src/components/tools/fence/move-fence-tool.tsx b/packages/editor/src/components/tools/fence/move-fence-tool.tsx index e1d75bd0..236059ad 100644 --- a/packages/editor/src/components/tools/fence/move-fence-tool.tsx +++ b/packages/editor/src/components/tools/fence/move-fence-tool.tsx @@ -1,6 +1,6 @@ 'use client' -import { type FenceNode, emitter, type GridEvent, sceneRegistry, useScene } from '@pascal-app/core' +import { type FenceNode, emitter, type GridEvent, useLiveFenceSegments, useScene } from '@pascal-app/core' import { useViewer } from '@pascal-app/viewer' import { useCallback, useEffect, useRef, useState } from 'react' import { markToolCancelConsumed } from '../../../hooks/use-keyboard' @@ -12,12 +12,85 @@ function snap(value: number) { return Math.round(value * 2) / 2 } +function samePoint(a: [number, number], b: [number, number]) { + return a[0] === b[0] && a[1] === b[1] +} + +type LinkedFenceSnapshot = { + id: FenceNode['id'] + start: [number, number] + end: [number, number] +} + +function getLinkedFenceSnapshots(args: { + fenceId: FenceNode['id'] + originalStart: [number, number] + originalEnd: [number, number] +}) { + const { fenceId, originalStart, originalEnd } = args + const { nodes } = useScene.getState() + const snapshots: LinkedFenceSnapshot[] = [] + + for (const node of Object.values(nodes)) { + if (!(node?.type === 'fence' && node.id !== fenceId)) { + continue + } + + if ( + !samePoint(node.start, originalStart) && + !samePoint(node.start, originalEnd) && + !samePoint(node.end, originalStart) && + !samePoint(node.end, originalEnd) + ) { + continue + } + + snapshots.push({ + id: node.id, + start: [...node.start] as [number, number], + end: [...node.end] as [number, number], + }) + } + + return snapshots +} + +function getLinkedFenceUpdates( + linkedFences: LinkedFenceSnapshot[], + originalStart: [number, number], + originalEnd: [number, number], + nextStart: [number, number], + nextEnd: [number, number], +) { + return linkedFences.map((fence) => ({ + id: fence.id, + start: samePoint(fence.start, originalStart) + ? nextStart + : samePoint(fence.start, originalEnd) + ? nextEnd + : fence.start, + end: samePoint(fence.end, originalStart) + ? nextStart + : samePoint(fence.end, originalEnd) + ? nextEnd + : fence.end, + })) +} + export const MoveFenceTool: React.FC<{ node: FenceNode }> = ({ node }) => { const previousGridPosRef = useRef<[number, number] | null>(null) const originalStartRef = useRef<[number, number]>([...node.start] as [number, number]) const originalEndRef = useRef<[number, number]>([...node.end] as [number, number]) + const linkedOriginalsRef = useRef( + getLinkedFenceSnapshots({ + fenceId: node.id, + originalStart: node.start, + originalEnd: node.end, + }), + ) const dragAnchorRef = useRef<[number, number] | null>(null) const nodeIdRef = useRef(node.id) + const previewRef = useRef<{ start: [number, number]; end: [number, number] } | null>(null) const [cursorLocalPos, setCursorLocalPos] = useState<[number, number, number]>(() => { const centerX = (node.start[0] + node.end[0]) / 2 @@ -33,18 +106,35 @@ export const MoveFenceTool: React.FC<{ node: FenceNode }> = ({ node }) => { const nodeId = nodeIdRef.current const originalStart = originalStartRef.current const originalEnd = originalEndRef.current - const mesh = sceneRegistry.nodes.get(nodeId) + const { updateNode } = useScene.getState() + const liveSegments = useLiveFenceSegments.getState() useScene.temporal.getState().pause() let wasCommitted = false - const updatePreview = (nextStart: [number, number], nextEnd: [number, number]) => { + const applyPreview = (nextStart: [number, number], nextEnd: [number, number]) => { + previewRef.current = { start: nextStart, end: nextEnd } const centerX = (nextStart[0] + nextEnd[0]) / 2 const centerZ = (nextStart[1] + nextEnd[1]) / 2 setCursorLocalPos([centerX, 0, centerZ]) - - if (mesh) { - mesh.position.set(centerX, 0, centerZ) + liveSegments.set(nodeId, { start: nextStart, end: nextEnd }) + updateNode(nodeId, { start: nextStart, end: nextEnd }) + + for (const linkedFence of getLinkedFenceUpdates( + linkedOriginalsRef.current, + originalStart, + originalEnd, + nextStart, + nextEnd, + )) { + liveSegments.set(linkedFence.id, { + start: linkedFence.start, + end: linkedFence.end, + }) + updateNode(linkedFence.id, { + start: linkedFence.start, + end: linkedFence.end, + }) } } @@ -69,23 +159,31 @@ export const MoveFenceTool: React.FC<{ node: FenceNode }> = ({ node }) => { const nextStart: [number, number] = [originalStart[0] + deltaX, originalStart[1] + deltaZ] const nextEnd: [number, number] = [originalEnd[0] + deltaX, originalEnd[1] + deltaZ] - updatePreview(nextStart, nextEnd) + applyPreview(nextStart, nextEnd) } const onGridClick = (event: GridEvent) => { - const anchor = dragAnchorRef.current - const localX = snap(event.localPosition[0]) - const localZ = snap(event.localPosition[2]) - const baseAnchor: [number, number] = anchor ?? [localX, localZ] - const deltaX = localX - baseAnchor[0] - const deltaZ = localZ - baseAnchor[1] - - const nextStart: [number, number] = [originalStart[0] + deltaX, originalStart[1] + deltaZ] - const nextEnd: [number, number] = [originalEnd[0] + deltaX, originalEnd[1] + deltaZ] + const preview = previewRef.current ?? { start: originalStart, end: originalEnd } wasCommitted = true useScene.temporal.getState().resume() - useScene.getState().updateNode(nodeId, { start: nextStart, end: nextEnd }) + updateNode(nodeId, { start: preview.start, end: preview.end }) + for (const linkedFence of getLinkedFenceUpdates( + linkedOriginalsRef.current, + originalStart, + originalEnd, + preview.start, + preview.end, + )) { + updateNode(linkedFence.id, { + start: linkedFence.start, + end: linkedFence.end, + }) + } + liveSegments.clear(nodeId) + for (const linkedFence of linkedOriginalsRef.current) { + liveSegments.clear(linkedFence.id) + } useScene.temporal.getState().pause() sfxEmitter.emit('sfx:item-place') @@ -95,10 +193,14 @@ export const MoveFenceTool: React.FC<{ node: FenceNode }> = ({ node }) => { } const onCancel = () => { - if (mesh) { - const centerX = (originalStart[0] + originalEnd[0]) / 2 - const centerZ = (originalStart[1] + originalEnd[1]) / 2 - mesh.position.set(centerX, 0, centerZ) + liveSegments.clear(nodeId) + updateNode(nodeId, { start: originalStart, end: originalEnd }) + for (const linkedFence of linkedOriginalsRef.current) { + liveSegments.clear(linkedFence.id) + updateNode(linkedFence.id, { + start: linkedFence.start, + end: linkedFence.end, + }) } useViewer.getState().setSelection({ selectedIds: [nodeId] }) useScene.temporal.getState().resume() @@ -112,7 +214,15 @@ export const MoveFenceTool: React.FC<{ node: FenceNode }> = ({ node }) => { return () => { if (!wasCommitted) { - useScene.getState().updateNode(nodeId, { start: originalStart, end: originalEnd }) + liveSegments.clear(nodeId) + updateNode(nodeId, { start: originalStart, end: originalEnd }) + for (const linkedFence of linkedOriginalsRef.current) { + liveSegments.clear(linkedFence.id) + updateNode(linkedFence.id, { + start: linkedFence.start, + end: linkedFence.end, + }) + } } useScene.temporal.getState().resume() emitter.off('grid:move', onGridMove) diff --git a/packages/viewer/src/components/renderers/fence/fence-renderer.tsx b/packages/viewer/src/components/renderers/fence/fence-renderer.tsx index 7065681b..d8541f63 100644 --- a/packages/viewer/src/components/renderers/fence/fence-renderer.tsx +++ b/packages/viewer/src/components/renderers/fence/fence-renderer.tsx @@ -1,4 +1,4 @@ -import { type FenceNode, useRegistry } from '@pascal-app/core' +import { type FenceNode, useLiveFenceSegments, useRegistry } from '@pascal-app/core' import { useMemo, useRef } from 'react' import { BoxGeometry, Group } from 'three' import { useNodeEvents } from '../../../hooks/use-node-events' @@ -100,13 +100,18 @@ function createFenceParts(fence: FenceNode): FencePart[] { export const FenceRenderer = ({ node }: { node: FenceNode }) => { const ref = useRef(null!) const handlers = useNodeEvents(node, 'fence') + const liveSegment = useLiveFenceSegments((state) => state.segments.get(node.id)) + const activeNode = liveSegment ? { ...node, start: liveSegment.start, end: liveSegment.end } : node const geometry = useMemo(() => new BoxGeometry(1, 1, 1), []) - const parts = useMemo(() => createFenceParts(node), [node]) - const rotation = Math.atan2(node.end[1] - node.start[1], node.end[0] - node.start[0]) + const parts = useMemo(() => createFenceParts(activeNode), [activeNode]) + const rotation = Math.atan2( + activeNode.end[1] - activeNode.start[1], + activeNode.end[0] - activeNode.start[0], + ) const center: [number, number, number] = [ - (node.start[0] + node.end[0]) / 2, + (activeNode.start[0] + activeNode.end[0]) / 2, 0, - (node.start[1] + node.end[1]) / 2, + (activeNode.start[1] + activeNode.end[1]) / 2, ] useRegistry(node.id, 'fence', ref) @@ -115,7 +120,7 @@ export const FenceRenderer = ({ node }: { node: FenceNode }) => { From 086cf6b43570117db674fe1b08a2c7171ae2811d Mon Sep 17 00:00:00 2001 From: sudhir Date: Mon, 13 Apr 2026 15:00:35 +0530 Subject: [PATCH 4/7] fix: update stair railing logic to include front-side attachments for terminal landings --- .../src/components/renderers/stair/stair-renderer.tsx | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/viewer/src/components/renderers/stair/stair-renderer.tsx b/packages/viewer/src/components/renderers/stair/stair-renderer.tsx index a9183090..d90d5a3c 100644 --- a/packages/viewer/src/components/renderers/stair/stair-renderer.tsx +++ b/packages/viewer/src/components/renderers/stair/stair-renderer.tsx @@ -630,17 +630,21 @@ function buildStairRailPaths( const sideCandidates = suppressLandingRailing ? ([] as StairRailPathSide[]) - : layout.segment.segmentType !== 'landing' + : layout.segment.segmentType !== 'landing' ? [railingMode] : isTerminalLandingBeforeStair ? railingMode === 'left' ? terminalNextAttachmentSide === 'right' ? (['front', 'left'] as const) - : ([] as StairRailPathSide[]) + : terminalNextAttachmentSide === 'front' || terminalNextAttachmentSide == null + ? (['left'] as const) + : ([] as StairRailPathSide[]) : railingMode === 'right' ? terminalNextAttachmentSide === 'left' ? (['front', 'right'] as const) - : ([] as StairRailPathSide[]) + : terminalNextAttachmentSide === 'front' || terminalNextAttachmentSide == null + ? (['right'] as const) + : ([] as StairRailPathSide[]) : [railingMode] : isStraightLineDoubleLandingLayout ? [railingMode] From 7b2b5869f616131bfbfa4f790cd8360a450c43fa Mon Sep 17 00:00:00 2001 From: sudhir Date: Mon, 13 Apr 2026 22:50:19 +0530 Subject: [PATCH 5/7] Integrate fence rendering into the fence system --- packages/core/src/index.ts | 2 +- .../core/src/store/use-live-fence-segments.ts | 34 ---- .../core/src/systems/fence/fence-system.tsx | 145 ++++++++++++++++++ .../tools/fence/move-fence-tool.tsx | 98 +++++------- .../renderers/fence/fence-renderer.tsx | 140 ++--------------- .../viewer/src/components/viewer/index.tsx | 2 + 6 files changed, 200 insertions(+), 221 deletions(-) delete mode 100644 packages/core/src/store/use-live-fence-segments.ts create mode 100644 packages/core/src/systems/fence/fence-system.tsx diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 688ea465..1325c1ea 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -44,8 +44,8 @@ export { type ItemInteractiveState, useInteractive, } from './store/use-interactive' -export { default as useLiveFenceSegments, type LiveFenceSegment } from './store/use-live-fence-segments' export { default as useLiveTransforms, type LiveTransform } from './store/use-live-transforms' +export { FenceSystem } from './systems/fence/fence-system' export { clearSceneHistory, default as useScene } from './store/use-scene' export { CeilingSystem } from './systems/ceiling/ceiling-system' export { DoorSystem } from './systems/door/door-system' diff --git a/packages/core/src/store/use-live-fence-segments.ts b/packages/core/src/store/use-live-fence-segments.ts deleted file mode 100644 index dc1bd0d8..00000000 --- a/packages/core/src/store/use-live-fence-segments.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { create } from 'zustand' - -export type LiveFenceSegment = { - start: [number, number] - end: [number, number] -} - -type LiveFenceSegmentState = { - segments: Map - set: (nodeId: string, segment: LiveFenceSegment) => void - get: (nodeId: string) => LiveFenceSegment | undefined - clear: (nodeId: string) => void - clearAll: () => void -} - -const useLiveFenceSegments = create((set, get) => ({ - segments: new Map(), - set: (nodeId, segment) => - set((state) => { - const next = new Map(state.segments) - next.set(nodeId, segment) - return { segments: next } - }), - get: (nodeId) => get().segments.get(nodeId), - clear: (nodeId) => - set((state) => { - const next = new Map(state.segments) - next.delete(nodeId) - return { segments: next } - }), - clearAll: () => set({ segments: new Map() }), -})) - -export default useLiveFenceSegments diff --git a/packages/core/src/systems/fence/fence-system.tsx b/packages/core/src/systems/fence/fence-system.tsx new file mode 100644 index 00000000..7f1de3bd --- /dev/null +++ b/packages/core/src/systems/fence/fence-system.tsx @@ -0,0 +1,145 @@ +import { useFrame } from '@react-three/fiber' +import * as THREE from 'three' +import { mergeGeometries } from 'three/examples/jsm/utils/BufferGeometryUtils.js' +import { sceneRegistry } from '../../hooks/scene-registry/scene-registry' +import type { AnyNodeId, FenceNode } from '../../schema' +import useScene from '../../store/use-scene' + +type FencePart = { + position: [number, number, number] + scale: [number, number, number] +} + +function getStyleDefaults(style: FenceNode['style']) { + if (style === 'privacy') { + return { spacingFactor: 0.42, postFactor: 1.35, baseFactor: 1.2, topFactor: 1.2 } + } + + if (style === 'rail') { + return { spacingFactor: 0.68, postFactor: 0.8, baseFactor: 0.85, topFactor: 0.85 } + } + + return { spacingFactor: 0.3, postFactor: 0.55, baseFactor: 1, topFactor: 0.75 } +} + +function createFenceParts(fence: FenceNode): FencePart[] { + const parts: FencePart[] = [] + const length = Math.max( + Math.hypot(fence.end[0] - fence.start[0], fence.end[1] - fence.start[1]), + 0.01, + ) + const panelDepth = Math.max(fence.thickness, 0.03) + const clearance = Math.max(fence.groundClearance, 0) + const styleDefaults = getStyleDefaults(fence.style) + const baseHeight = Math.max(fence.baseHeight * styleDefaults.baseFactor, 0.04) + const topRailHeight = Math.max(fence.topRailHeight * styleDefaults.topFactor, 0.01) + const verticalHeight = Math.max(fence.height - baseHeight - topRailHeight, 0.08) + const postWidth = Math.max(fence.postSize * styleDefaults.postFactor, 0.01) + const spacing = Math.max(fence.postSpacing * styleDefaults.spacingFactor, postWidth * 1.2) + const edgeInset = Math.max(fence.edgeInset ?? 0.015, 0.005) + const isFloating = fence.baseStyle === 'floating' + const baseY = isFloating ? clearance : 0 + const effectiveBaseHeight = baseHeight + + if (!isFloating) { + parts.push({ + position: [0, baseY + effectiveBaseHeight / 2, 0], + scale: [length, effectiveBaseHeight, panelDepth * 1.05], + }) + parts.push({ + position: [0, baseY + effectiveBaseHeight + verticalHeight * 0.15, 0], + scale: [length, topRailHeight * 0.8, panelDepth * 0.35], + }) + } + + const count = Math.max(2, Math.floor((length - edgeInset * 2) / spacing) + 1) + const step = count > 1 ? (length - edgeInset * 2) / (count - 1) : 0 + const startX = -length / 2 + edgeInset + const verticalY = baseY + effectiveBaseHeight + verticalHeight / 2 + + for (let index = 0; index < count; index += 1) { + const x = count === 1 ? 0 : startX + step * index + let posX = x + const isEdgePost = index === 0 || index === count - 1 + if (count > 1) { + if (index === 0) posX = -length / 2 + edgeInset + postWidth / 2 + else if (index === count - 1) posX = length / 2 - edgeInset - postWidth / 2 + } + const postHeight = + isFloating && isEdgePost + ? effectiveBaseHeight + verticalHeight + topRailHeight + clearance + : verticalHeight + const postY = isFloating && isEdgePost ? postHeight / 2 : verticalY + + parts.push({ + position: [posX, postY, 0], + scale: [postWidth, postHeight, Math.max(panelDepth * 0.35, 0.012)], + }) + } + + parts.push({ + position: [0, baseY + effectiveBaseHeight + verticalHeight + topRailHeight / 2, 0], + scale: [length, topRailHeight, Math.max(panelDepth * 0.55, 0.018)], + }) + + if (isFloating) { + parts.push({ + position: [0, baseY + effectiveBaseHeight + topRailHeight / 2, 0], + scale: [length, topRailHeight, Math.max(panelDepth * 0.55, 0.018)], + }) + } + + return parts +} + +function generateFenceGeometry(fence: FenceNode) { + const parts = createFenceParts(fence) + const geometries = parts.map((part) => { + const geometry = new THREE.BoxGeometry(1, 1, 1) + geometry.scale(part.scale[0], part.scale[1], part.scale[2]) + geometry.translate(part.position[0], part.position[1], part.position[2]) + return geometry + }) + + const merged = mergeGeometries(geometries, false) ?? new THREE.BufferGeometry() + geometries.forEach((geometry) => geometry.dispose()) + merged.computeVertexNormals() + return merged +} + +function updateFenceGeometry(fenceId: FenceNode['id']) { + const node = useScene.getState().nodes[fenceId] + if (!node || node.type !== 'fence') return + + const mesh = sceneRegistry.nodes.get(fenceId) as THREE.Mesh | undefined + if (!mesh) return + + const newGeometry = generateFenceGeometry(node) + mesh.geometry.dispose() + mesh.geometry = newGeometry + + const centerX = (node.start[0] + node.end[0]) / 2 + const centerZ = (node.start[1] + node.end[1]) / 2 + const angle = Math.atan2(node.end[1] - node.start[1], node.end[0] - node.start[0]) + mesh.position.set(centerX, 0, centerZ) + mesh.rotation.set(0, -angle, 0) +} + +export const FenceSystem = () => { + const dirtyNodes = useScene((state) => state.dirtyNodes) + const clearDirty = useScene((state) => state.clearDirty) + + useFrame(() => { + if (dirtyNodes.size === 0) return + + const nodes = useScene.getState().nodes + dirtyNodes.forEach((id) => { + const node = nodes[id] + if (!node || node.type !== 'fence') return + updateFenceGeometry(id as FenceNode['id']) + clearDirty(id as AnyNodeId) + }) + }, 4) + + return null +} diff --git a/packages/editor/src/components/tools/fence/move-fence-tool.tsx b/packages/editor/src/components/tools/fence/move-fence-tool.tsx index 236059ad..4fb36354 100644 --- a/packages/editor/src/components/tools/fence/move-fence-tool.tsx +++ b/packages/editor/src/components/tools/fence/move-fence-tool.tsx @@ -1,6 +1,6 @@ 'use client' -import { type FenceNode, emitter, type GridEvent, useLiveFenceSegments, useScene } from '@pascal-app/core' +import { type AnyNodeId, type FenceNode, emitter, type GridEvent, useScene } from '@pascal-app/core' import { useViewer } from '@pascal-app/viewer' import { useCallback, useEffect, useRef, useState } from 'react' import { markToolCancelConsumed } from '../../../hooks/use-keyboard' @@ -106,36 +106,37 @@ export const MoveFenceTool: React.FC<{ node: FenceNode }> = ({ node }) => { const nodeId = nodeIdRef.current const originalStart = originalStartRef.current const originalEnd = originalEndRef.current - const { updateNode } = useScene.getState() - const liveSegments = useLiveFenceSegments.getState() useScene.temporal.getState().pause() let wasCommitted = false + const applyNodePreview = (updates: Array<{ id: FenceNode['id']; start: [number, number]; end: [number, number] }>) => { + useScene.getState().updateNodes( + updates.map((entry) => ({ + id: entry.id as AnyNodeId, + data: { start: entry.start, end: entry.end }, + })), + ) + for (const entry of updates) { + useScene.getState().markDirty(entry.id as AnyNodeId) + } + } + const applyPreview = (nextStart: [number, number], nextEnd: [number, number]) => { previewRef.current = { start: nextStart, end: nextEnd } const centerX = (nextStart[0] + nextEnd[0]) / 2 const centerZ = (nextStart[1] + nextEnd[1]) / 2 setCursorLocalPos([centerX, 0, centerZ]) - liveSegments.set(nodeId, { start: nextStart, end: nextEnd }) - updateNode(nodeId, { start: nextStart, end: nextEnd }) - - for (const linkedFence of getLinkedFenceUpdates( - linkedOriginalsRef.current, - originalStart, - originalEnd, - nextStart, - nextEnd, - )) { - liveSegments.set(linkedFence.id, { - start: linkedFence.start, - end: linkedFence.end, - }) - updateNode(linkedFence.id, { - start: linkedFence.start, - end: linkedFence.end, - }) - } + applyNodePreview([ + { id: nodeId, start: nextStart, end: nextEnd }, + ...getLinkedFenceUpdates( + linkedOriginalsRef.current, + originalStart, + originalEnd, + nextStart, + nextEnd, + ), + ]) } const onGridMove = (event: GridEvent) => { @@ -167,23 +168,16 @@ export const MoveFenceTool: React.FC<{ node: FenceNode }> = ({ node }) => { wasCommitted = true useScene.temporal.getState().resume() - updateNode(nodeId, { start: preview.start, end: preview.end }) - for (const linkedFence of getLinkedFenceUpdates( - linkedOriginalsRef.current, - originalStart, - originalEnd, - preview.start, - preview.end, - )) { - updateNode(linkedFence.id, { - start: linkedFence.start, - end: linkedFence.end, - }) - } - liveSegments.clear(nodeId) - for (const linkedFence of linkedOriginalsRef.current) { - liveSegments.clear(linkedFence.id) - } + applyNodePreview([ + { id: nodeId, start: preview.start, end: preview.end }, + ...getLinkedFenceUpdates( + linkedOriginalsRef.current, + originalStart, + originalEnd, + preview.start, + preview.end, + ), + ]) useScene.temporal.getState().pause() sfxEmitter.emit('sfx:item-place') @@ -193,15 +187,10 @@ export const MoveFenceTool: React.FC<{ node: FenceNode }> = ({ node }) => { } const onCancel = () => { - liveSegments.clear(nodeId) - updateNode(nodeId, { start: originalStart, end: originalEnd }) - for (const linkedFence of linkedOriginalsRef.current) { - liveSegments.clear(linkedFence.id) - updateNode(linkedFence.id, { - start: linkedFence.start, - end: linkedFence.end, - }) - } + applyNodePreview([ + { id: nodeId, start: originalStart, end: originalEnd }, + ...linkedOriginalsRef.current, + ]) useViewer.getState().setSelection({ selectedIds: [nodeId] }) useScene.temporal.getState().resume() markToolCancelConsumed() @@ -214,15 +203,10 @@ export const MoveFenceTool: React.FC<{ node: FenceNode }> = ({ node }) => { return () => { if (!wasCommitted) { - liveSegments.clear(nodeId) - updateNode(nodeId, { start: originalStart, end: originalEnd }) - for (const linkedFence of linkedOriginalsRef.current) { - liveSegments.clear(linkedFence.id) - updateNode(linkedFence.id, { - start: linkedFence.start, - end: linkedFence.end, - }) - } + applyNodePreview([ + { id: nodeId, start: originalStart, end: originalEnd }, + ...linkedOriginalsRef.current, + ]) } useScene.temporal.getState().resume() emitter.off('grid:move', onGridMove) diff --git a/packages/viewer/src/components/renderers/fence/fence-renderer.tsx b/packages/viewer/src/components/renderers/fence/fence-renderer.tsx index d8541f63..008e083d 100644 --- a/packages/viewer/src/components/renderers/fence/fence-renderer.tsx +++ b/packages/viewer/src/components/renderers/fence/fence-renderer.tsx @@ -1,140 +1,22 @@ -import { type FenceNode, useLiveFenceSegments, useRegistry } from '@pascal-app/core' -import { useMemo, useRef } from 'react' -import { BoxGeometry, Group } from 'three' +import { type FenceNode, useRegistry, useScene } from '@pascal-app/core' +import { useLayoutEffect, useMemo, useRef } from 'react' +import type { Mesh } from 'three' import { useNodeEvents } from '../../../hooks/use-node-events' import { DEFAULT_STAIR_MATERIAL } from '../../../lib/materials' -type FencePart = { - key: string - position: [number, number, number] - scale: [number, number, number] -} - -function getStyleDefaults(style: FenceNode['style']) { - if (style === 'privacy') { - return { spacingFactor: 0.42, postFactor: 1.35, baseFactor: 1.2, topFactor: 1.2 } - } - - if (style === 'rail') { - return { spacingFactor: 0.68, postFactor: 0.8, baseFactor: 0.85, topFactor: 0.85 } - } - - return { spacingFactor: 0.3, postFactor: 0.55, baseFactor: 1, topFactor: 0.75 } -} - -function createFenceParts(fence: FenceNode): FencePart[] { - const parts: FencePart[] = [] - const length = Math.max( - Math.hypot(fence.end[0] - fence.start[0], fence.end[1] - fence.start[1]), - 0.01, - ) - const panelDepth = Math.max(fence.thickness, 0.03) - const clearance = Math.max(fence.groundClearance, 0) - const styleDefaults = getStyleDefaults(fence.style) - const baseHeight = Math.max(fence.baseHeight * styleDefaults.baseFactor, 0.04) - const topRailHeight = Math.max(fence.topRailHeight * styleDefaults.topFactor, 0.01) - const verticalHeight = Math.max(fence.height - baseHeight - topRailHeight, 0.08) - const postWidth = Math.max(fence.postSize * styleDefaults.postFactor, 0.01) - const spacing = Math.max(fence.postSpacing * styleDefaults.spacingFactor, postWidth * 1.2) - const edgeInset = Math.max(fence.edgeInset ?? 0.015, 0.005) - const isFloating = fence.baseStyle === 'floating' - const baseY = isFloating ? clearance : 0 - const effectiveBaseHeight = baseHeight - - if (!isFloating) { - parts.push({ - key: 'base', - position: [0, baseY + effectiveBaseHeight / 2, 0], - scale: [length, effectiveBaseHeight, panelDepth * 1.05], - }) - parts.push({ - key: 'mid-rail', - position: [0, baseY + effectiveBaseHeight + verticalHeight * 0.15, 0], - scale: [length, topRailHeight * 0.8, panelDepth * 0.35], - }) - } - - const count = Math.max(2, Math.floor((length - edgeInset * 2) / spacing) + 1) - const step = count > 1 ? (length - edgeInset * 2) / (count - 1) : 0 - const startX = -length / 2 + edgeInset - const verticalY = baseY + effectiveBaseHeight + verticalHeight / 2 - - for (let index = 0; index < count; index += 1) { - const x = count === 1 ? 0 : startX + step * index - let posX = x - const isEdgePost = index === 0 || index === count - 1 - if (count > 1) { - if (index === 0) posX = -length / 2 + edgeInset + postWidth / 2 - else if (index === count - 1) posX = length / 2 - edgeInset - postWidth / 2 - } - const postHeight = - isFloating && isEdgePost - ? effectiveBaseHeight + verticalHeight + topRailHeight + clearance - : verticalHeight - const postY = isFloating && isEdgePost ? postHeight / 2 : verticalY - - parts.push({ - key: `vertical-${index}`, - position: [posX, postY, 0], - scale: [postWidth, postHeight, Math.max(panelDepth * 0.35, 0.012)], - }) - } - - parts.push({ - key: 'top-rail', - position: [0, baseY + effectiveBaseHeight + verticalHeight + topRailHeight / 2, 0], - scale: [length, topRailHeight, Math.max(panelDepth * 0.55, 0.018)], - }) - - if (isFloating) { - parts.push({ - key: 'bottom-rail', - position: [0, baseY + effectiveBaseHeight + topRailHeight / 2, 0], - scale: [length, topRailHeight, Math.max(panelDepth * 0.55, 0.018)], - }) - } - - return parts -} - export const FenceRenderer = ({ node }: { node: FenceNode }) => { - const ref = useRef(null!) + const ref = useRef(null!) const handlers = useNodeEvents(node, 'fence') - const liveSegment = useLiveFenceSegments((state) => state.segments.get(node.id)) - const activeNode = liveSegment ? { ...node, start: liveSegment.start, end: liveSegment.end } : node - const geometry = useMemo(() => new BoxGeometry(1, 1, 1), []) - const parts = useMemo(() => createFenceParts(activeNode), [activeNode]) - const rotation = Math.atan2( - activeNode.end[1] - activeNode.start[1], - activeNode.end[0] - activeNode.start[0], - ) - const center: [number, number, number] = [ - (activeNode.start[0] + activeNode.end[0]) / 2, - 0, - (activeNode.start[1] + activeNode.end[1]) / 2, - ] + const material = useMemo(() => DEFAULT_STAIR_MATERIAL, []) useRegistry(node.id, 'fence', ref) + useLayoutEffect(() => { + useScene.getState().markDirty(node.id) + }, [node.id]) return ( - - {parts.map((part) => ( - - ))} - + + + ) } diff --git a/packages/viewer/src/components/viewer/index.tsx b/packages/viewer/src/components/viewer/index.tsx index 87c6e952..bfb8bb55 100644 --- a/packages/viewer/src/components/viewer/index.tsx +++ b/packages/viewer/src/components/viewer/index.tsx @@ -3,6 +3,7 @@ import { CeilingSystem, DoorSystem, + FenceSystem, ItemSystem, RoofSystem, SlabSystem, @@ -139,6 +140,7 @@ const Viewer: React.FC = ({ {/* Core systems */} + From 36da6f39983b1011b27c86981c622606b65316d3 Mon Sep 17 00:00:00 2001 From: Aymeric Rabot Date: Mon, 13 Apr 2026 22:37:36 -0400 Subject: [PATCH 6/7] 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 --- .../panels/site-panel/fence-tree-node.tsx | 21 +++++++++++-------- .../sidebar/panels/site-panel/tree-node.tsx | 4 ++-- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/packages/editor/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx b/packages/editor/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx index 83f79b4f..0e2206fa 100644 --- a/packages/editor/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +++ b/packages/editor/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx @@ -1,4 +1,4 @@ -import { type FenceNode } from '@pascal-app/core' +import { type AnyNodeId, type FenceNode, useScene } from '@pascal-app/core' import { useViewer } from '@pascal-app/viewer' import Image from 'next/image' import { useState } from 'react' @@ -8,22 +8,25 @@ import { focusTreeNode, handleTreeSelection, TreeNodeWrapper } from './tree-node import { TreeNodeActions } from './tree-node-actions' interface FenceTreeNodeProps { - node: FenceNode + nodeId: AnyNodeId depth: number isLast?: boolean } -export function FenceTreeNode({ node, depth, isLast }: FenceTreeNodeProps) { +export function FenceTreeNode({ nodeId, depth, isLast }: FenceTreeNodeProps) { + const node = useScene((state) => state.nodes[nodeId]) as FenceNode | undefined const [isEditing, setIsEditing] = useState(false) const selectedIds = useViewer((state) => state.selection.selectedIds) - const isSelected = selectedIds.includes(node.id) - const isHovered = useViewer((state) => state.hoveredId === node.id) + const isSelected = selectedIds.includes(nodeId) + const isHovered = useViewer((state) => state.hoveredId === nodeId) const setSelection = useViewer((state) => state.setSelection) const setHoveredId = useViewer((state) => state.setHoveredId) + if (!node) return null + const handleClick = (e: React.MouseEvent) => { e.stopPropagation() - const handled = handleTreeSelection(e, node.id, selectedIds, setSelection) + const handled = handleTreeSelection(e, nodeId, selectedIds, setSelection) if (!handled && useEditor.getState().phase === 'furnish') { useEditor.getState().setPhase('structure') } @@ -51,10 +54,10 @@ export function FenceTreeNode({ node, depth, isLast }: FenceTreeNodeProps) { onStopEditing={() => setIsEditing(false)} /> } - nodeId={node.id} + nodeId={nodeId} onClick={handleClick} - onDoubleClick={() => focusTreeNode(node.id)} - onMouseEnter={() => setHoveredId(node.id)} + onDoubleClick={() => focusTreeNode(nodeId)} + onMouseEnter={() => setHoveredId(nodeId)} onMouseLeave={() => setHoveredId(null)} onToggle={() => {}} /> diff --git a/packages/editor/src/components/ui/sidebar/panels/site-panel/tree-node.tsx b/packages/editor/src/components/ui/sidebar/panels/site-panel/tree-node.tsx index b07d1f14..eabf547e 100644 --- a/packages/editor/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +++ b/packages/editor/src/components/ui/sidebar/panels/site-panel/tree-node.tsx @@ -88,9 +88,9 @@ export function TreeNode({ nodeId, depth = 0, isLast }: TreeNodeProps) { case 'slab': return case 'wall': - return + return case 'fence': - return + return case 'roof': return case 'stair': From 7d296f997165f22925dcd7565e60ed8aa0919a1a Mon Sep 17 00:00:00 2001 From: Aymeric Rabot Date: Mon, 13 Apr 2026 22:41:54 -0400 Subject: [PATCH 7/7] feat: update fence icon with new isometric design Co-Authored-By: Claude Opus 4.6 --- apps/editor/public/icons/fence.png | Bin 49495 -> 105533 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/apps/editor/public/icons/fence.png b/apps/editor/public/icons/fence.png index 854ed7a25f1ee97d3c6256a66e07f840d8c6e390..47f677f589573e7c750948caf388f4d2293f1e96 100644 GIT binary patch literal 105533 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 z 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
*_zZZLnnm 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%0Dc&JB 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 literal 49495 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¨$EO|3joW9(fKkZ=`ajGN&pJ)SL>#kh zj2_fQR1CR7h0MGf_Q(}U#zA18YM3_oin2v5UtnmW^X~~z4PZ1<(I}d#K(@){n%gsf z4;!WhJB2zLZJWD@JD#kl0pn?qiWcb9k74@&le_t2NxsLd-Gr?k4?g+$o?e$8gmVsR7(ef4$M?M#mGl5Dt?F)qTqrqAc8f_UxFE!+2CReHSI)(wfu|=I zXlXrPz%1|Ed&)*n&F5J;Lc+OkFNk`x-F~NKt}Gp1`*>Y9=Pb*YJ0W;uCmWvb<*gT9 z%cFGyEJ3xw|h@g|1WX1$VlxiV~Fs8z06js|+_r zi8z-cJptVa{rn|5J2S9KI`8zI%&gg^7aw(JJlQ~M-BF((lIomWQkq-y~=UIz-dFc*->$8 z`dH>Lnh|ge`rsXsCn({@^M;Hlhy9aBJyhVH2{sDWC4!oDREL`U^DbkcHQo=LXf#=c=EvKW7-li+C`={&e z*R&pI>u+(-q|Xz2-G2S&`VZw=o!fOy0b3br%0B-Xg*Gd_CEea?PYr2nvmNMaqz5Hg zT#VmvDSKGZ@Yi8O7;E>e03MN&6)vI{)<6$n0P{*BC0)bk(f&2W16X`RiA6&bdm|W9 zJIBg8n5XYOH(Ois>AlTfI+H{#cXe@iUusgq#ZCTJ8liasJ3;=#7f`$1j#TW7bpi7v z&g@Ti;a@DxPzGYr)poebA^-s2ANT8DpB6kTTi;jKx~H9A5r}SxVlTn?matq4bU$Qp zwJC%9Hs67(qU&rNfNuG?DUm`ujHFZ@^?O(pENTNV6@`Bi8<@xz*bGU#Z7pbwv<*Bp zTL|zU@*hVpzI#l6G`Loq)$%i6Yu)e4PAol$Oh2hz$y#bRx4G5cKXrU-y*$Kx`zx>6 zvJF1vogy%@nvgQE?12#T8;Qg)Yj?!vQ6h#RUO2CX^zsh5?Ed|bZ0~~NyondBf-yYl zKc0A7TZiIpG+J5H>6+dcqU;`d_&xv*J`4{OpkfaA)qt|M8sXc$OvjR!>i*({&H^&( z(4jTP>;2w6luQQGNj58*H`H!sCd9L!N@qmLd0|jsBBezx z#${_M%Nv_yzuq;?Oy`?qk-H zE0)bq>;fZLVSEO}1zumoI0sdJ2T?fyE;MZ|JTO<6-yUS_Z)_(@8v)(DYokQffHz1d zZM@b-KQVbRhyz#w$ARQ0`I9*zy4Dx~B70Z>u+HG(qkju-p#B^>T_jvK6~A*UiJZ(y zXIY5L_ob*oRZcpgPS#&SS)t-}^)J!eO+2apQHo)qEd|WX6{QB$_e+Jj7?eZ{%8uF% z7tk>r7_>;hd?nk%Ph<%DVca-G3Mh_vu0QECN30no?(nQ&bl|`>G_B0XGJa+}YK#m9 z8G2lj==&lTYgth<3t{V{w-D7;DSrTwGj~UFbz5re8qAtO|^4 zKflKZQKP>ks+J|u#r@HYF8EH3Va6aeK2XNl?X(wp{=RjpS9N@(OPqL#E$#=w&v8pq z!QO8LUR)3R7KNLp$z^VJ4*u#1dgR0_&I>{Og^{BC1utv97X#!N;~G_|rY(w#VqI|@ z4LraZ1@~bgt&Gwb!ZjInRN+DP&EPOCiV`{fgT2R)f+@%RZxcpU?igK z--wl5`v*$-w=G#5L}dYh@p$@sy5XX>T`8Ldt=}OR$9#YX!*Tq|!+Q8g&R$=L1fYO+ zJ+I&PD!a(&xrvkE!+jrcNg_~(xxQMC`km(zBBE*qy5qs2MEQzabsU3R zQTD^ERi_lyEp^QNu}r)~!EK>-r_cBxs?q!Oq=Wy}od<3_4TkGo?*O=!aFOJMEFVIU zqxfws)bymc{)6O5XzB?E{ zpnus1ONHhaw-R-e9ZXyN<0rZijIW(9NYbm%glUH0n=6(Lq*>U9xPvLj!_YO!19Av* zxD}?#N5clfZ_-;6uGrjRj3nI-?N$PN?XK zF`B^Q2F~wnq(R&h%6Q_Ru>#^#2vR_MC#isM`$2qU!>L>U&^w|T|5^}j`b2%Hl1z%3 zvvTA3?c)yzQT|E#J7HS4!&GvuGDH7*dREzQ1zxeeyMJVU@iD>xdlTHyD4M`Ik|cJ8 zr%;x)2>G53+y9)G&FW9~;N0V88Y~KWSY`02!t|pLas&qG^{YyN?~v2p^P-hC!MBS( zHRAV%;5%^$>H+*kw2on#p|)Y6a8Uu;!K4 zYm=g$hFY6BSX(qLecAMpz9^xFGbjz$Q#_Lg57G7gHn<_&|M#zzZ1+C5| z3617oPf-1Kh}v2Xk8p|}PX0r{lNBuYvh3%t!cix0@>H(YM&*Y^DO!OuXl?d(d6`K%$btUy6$sI z1<er3QLW)bc>AHkrF#?3C0lm|F`7fZ zfWN7$BCG4HdD;7Lvw`~HA~s(-(}P(EyoGIiq966UI7_6lPKD=Q!lst|%l2;q5$dA-u7=T66zBc=Ug=pfx zgf*uN-1`>Y=l>^S@Yg?7-_BMZ3EY0nDKs!NIw|+X4xy(AbH^kvtHw`6cR;cTy)Mc? zXM%!DVxkZ?Q@i-&A?`-LKhVwqOmB(ZrP;G zt&3CQv1Ssv z?j1;Lf+1XhS_+xfjgJ=rQi0FUIX;4?C_sI}Huuy*^ zao%SDJuoG6?8fuz?|d_$%ddX&dIz#Ge!ukQ8yLf$HtZ5r)=H{D2w+V05X$7n8jWz? zp-mSfhCX|JEgw9(kv}vbY>R`qe_ef*Vv!P;Ww7AeTa@K{t-$Fkt=3Uyprw+lXFgpK zmge_7!CYx3%d=m(o4q?-ocPi4f3W~|G-Upt8(sE+UPu36O&kJp@(Yowz2#PnnYDdB7eQWGU|qiY&`r2n^5uKdKa(nDWDj)=vyk zqwd`xWo-6TF@@dUF*rJG_$|)F@`EHbr99UO-q4MQVzq-4SEYP*qi0pV?TYQk8}^0i zL(8g?`hc6Fn_}+VF-PBhYq!nz$>Qrt{>%vhGguPOG2Fk#US3DIK}+yAa$i)VWTm52 z5Foat;xS@<(gNFbqbT$)u*^eYuqu7}db|GEtW2RaJ8XG6yyWugya9uCoq~RO_9@h*-SY|dBS8yZfA!_c7tq3D{ zK3}9eA4GdT!4g`kfrLdC(?urov8rnaKNh6XRk8Jy(naBZ!9}6dVf(ikL=X3&79$De zn=YQ%k2r=wt*H#o^2fB&(wehHv;Ma2xb1B)iM;JX74r7>Hh#U-1bp2hrqg<&X-qW- zc3QR*U7q%~kM40^?KwGHRIs~HVLGyo?zfywm+k3ZFSaLG-vsmW2*`VX2HSGoZQN2j zWS3vqJKh=$xx(Q!v$b1Vh++FKP8Z`6T8fDu$aWEW3V%bHmeE2~MC@{3l7I(^&XfE| zkIq<&aG}a_8Ik#f(Bm-jzuEZ$X1;G{E14l}3tLhuKRIMrWSQX6Fm&0&Ngnt$pX{iSw## zcR=!6T1*rkrl7PEc}vwG66{(-M7oOc?b-Q*%TX#;6hu`Hbf*qW?FRO=(8&8X^RP0a z1>JAKlN;b-Fdoy5!hBqmPUJSU&e#25<(7m1Q*8_fQ}C_)OW9w2LOApuTYN%ok!4dxkzx}k5;ZUhBny$%r+remQOw;;G1*iDylmY(p`Ey&${>?cOR9rIOw zGbht0K8Xxs>u`L~0+wS0CA%D_cyHgDWjP=K`rEOD2aeC5GdVT-`iAvz#jE_?N7v(~ zWcMyyY+l70(h3IgeZcEDEdI4-72vUl;l1)M(4c1Sa- z$@AO~T!q}Ia?Dh+p8b5*UVO$cBJ<$(I%N@Il)AoqjuoFHqT5=HMl6(a$dIwR+FVEP zXB60hOaUer#lI4C3P=SONPZ@-o0wh$Yj{)9!-MO4<3xD%f*R6ALHqKsM>6_eJ{hms zo?#EK-51N}4Z4*Km5tZ}KtJkh>Ksrw8|x=2K#zi2xZ`t7FG!TWIC<~fp(Mt z0$%54=;TfEN#fhKI6nJHtfMAacYdIG&K0lS7fRA5cHwkD(|k74xSEsimWfA**mcsXzr&lHw=z@|XbZVoupQq=}tkMf&a}BAhACNrbL!cTr02&wZ~_d`X|5 zZbmLIKP4UoJ>v0rkT{ke+ftCY7I^=1i=zH5_9mePe(az0>3dMd^!v{hlP*Gt`vd&R zzut1m`S&*PM%!hO*HZ9yH?ZTX57zbhUEmlxe;QA;rV=3>aO?y~189QQVPCppLTft# zgbMbbJ(zj80j;+uPv$WJbkf2Nuot}tK=Ygg=3ZvD7?i6ZV|ThFDTFVJa41o|S1M)> z9-F^u&t4Lx@7G_9O28GFn^@d3lI-8UsfD&|Z~X-OIH%B7et5Xze!Rl8i)v@d^lMoN zsS=t&Lfey8htHQol&(a$2q1fG?R>t{`t*E%z7^m7es%UN)UY{2!cX?tBWCt?XUIKY z?3BRDleR{1f<~!m>_Q*pV1fkDWD${Gro3YYK+WgfF9--JI1>pMB1tY!3NM2wp*kqz zbU`O+dF~8zTMihU%yyWVY$Wyd8{Jrayg=`^ao=8cSEKc)QDGa+u%I;0jNI}VZi#mA z-l-s{x()B%!to@-yMG<;q&Ns?vnlWR?~xS9gec>Nyw_jga*NxxJ*k22lXralC= zouzbpO>Xm(y=L>lY2yYScQf+<3pBbu zKolqP#PKIPACv7cODYri{R$(k9OCKsc=qh@bn58% z*!^xY8dvS_*0uQdtfpq$eK(T&dXUJF|Id5>T2KYD=D?8=snEfi9@nH*sheD>JA4BgL)Xn3mILU=g=;))RzEx1NVP43-^T>6!rwhK*LiwSCd z{2Lc%dqfifYX)kxO8T6M8-7cbm@lV)x!13f$=>(u7mVDVv$_2*a+sP8J>FqG4Qh9L zeFKks!_OYG5u4U;SMD7j)5`z0ts^gDhl_X5>F+raP<8)hyHyc1a8~M)w*2_q>ein4tGSPUtLNn>w zyVD+K!|Et)z*zS&y6`;zdFA=qbJaC-+qrwS@HG)_&dW@7h5z4y+-g7B%fDWg?|l!K z(|8NScyHftk^kjp-;9`5^Pb)$Rtujt)OWqM&2C&x*-o|?-$$|tHrIYN+)xp+Zd_UF zked%6l)d}QFutnqkU>Rd+kX3`b_gE)9>8FS&shVojUCg!iH<-aB##pot66I!uIv*W?btm5IDuS3Oslq|oA&@XEt* z_VO(`SQa~U;*SW%wNcN5pi3S5A|GHtVPU$KBlSjztdt+SZ*B`4gy9vkAT@ zFpR+<8NB*9JF7}MuPxn)5XT35QEjb&*8dHzMLa=KGQp&atlkpve7Ivo)JJOE91_b|TQj z*4*$_N>y;w*l|FYHoyK;7O6?7?E_uR_|W}ni?p`w*}Mp<0b}`)hGNr9&x{RpHTm*M zn+7@9Q90OTptfdwd4^q4P|^H|HW^_nXu?-6IpVIOLLpDiJ7}ft^QsOH!+@LVi|G`Mb zL$3x3)ATvo| z^K|R~)KPHMsD|EvNdGASe~Dk7`Jd2#n`2qrjOSSm6=MCL(4LAWx2iQHd;xmh0bbb} ug7oMe#vB*#Q26(E9AmgO=nI7Z=>ZVp7Im3g@$Q6v0+5wdlBg0h4*q|%uM1`X