Refine 2D door swing rendering

This commit is contained in:
sudhir
2026-04-29 17:51:58 +05:30
parent 3c185144e0
commit 411ae2e905
12 changed files with 2434 additions and 322 deletions
@@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" role="img" aria-label="Office chair floor plan">
<g fill="#ffffff" stroke="#3f3f3f" stroke-linecap="round" stroke-linejoin="round" stroke-width="3" transform="rotate(180 128 128)">
<path d="M78 72c0-9 100-9 100 0v11c-27-7-73-7-100 0z"/>
<path d="M89 88c22-9 56-9 78 0"/>
<path d="M78 106c10-16 90-16 100 0l7 62c-4 19-15 31-57 31s-53-12-57-31z"/>
<path d="M54 110c-9 0-15 7-15 17v38c0 10 6 17 15 17h7l6-72z"/>
<path d="M202 110c9 0 15 7 15 17v38c0 10-6 17-15 17h-7l-6-72z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 572 B

@@ -0,0 +1,11 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 256" role="img" aria-label="Sofa floor plan">
<g fill="#ffffff" stroke="#3f3f3f" stroke-linecap="round" stroke-linejoin="round" stroke-width="3">
<path d="M70 45h372c9 0 16 7 16 16v140c0 16-13 29-29 29H83c-16 0-29-13-29-29V61c0-9 7-16 16-16z"/>
<path d="M80 45v141"/>
<path d="M432 45v141"/>
<path d="M256 45v141"/>
<path d="M80 186h352"/>
<rect x="99" y="126" width="141" height="62" rx="8"/>
<rect x="272" y="126" width="141" height="62" rx="8"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 550 B

+7 -7
View File
@@ -46,23 +46,23 @@ export {
LIBRARY_MATERIAL_REF_PREFIX,
MATERIAL_CATALOG,
MATERIAL_CATEGORIES,
type MaterialCategory,
type MaterialCatalogItem,
type MaterialCategory,
toLibraryMaterialRef,
} from './material-library'
export { baseMaterial, glassMaterial } from './materials'
export * from './schema'
export {
type ControlValue,
type ItemInteractiveState,
useInteractive,
} from './store/use-interactive'
export {
getSceneHistoryPauseDepth,
pauseSceneHistory,
resetSceneHistoryPauseDepth,
resumeSceneHistory,
} from './store/history-control'
export {
type ControlValue,
type ItemInteractiveState,
useInteractive,
} from './store/use-interactive'
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'
@@ -70,7 +70,7 @@ export { DoorSystem } from './systems/door/door-system'
export { FenceSystem } from './systems/fence/fence-system'
export { ItemSystem } from './systems/item/item-system'
export { RoofSystem } from './systems/roof/roof-system'
export { SlabSystem } from './systems/slab/slab-system'
export { getRenderableSlabPolygon, SlabSystem } from './systems/slab/slab-system'
export { StairSystem } from './systems/stair/stair-system'
export {
getClampedWallCurveOffset,
@@ -63,7 +63,7 @@ const SLAB_OUTSET = 0.05
const AUTO_SLAB_INSET = 0.02
const AUTO_SLAB_SIMPLIFY_TOLERANCE = 0.08
function getRenderableSlabPolygon(slabNode: SlabNode): Array<[number, number]> {
export function getRenderableSlabPolygon(slabNode: SlabNode): Array<[number, number]> {
return slabNode.autoFromWalls
? simplifyClosedPolygon(
insetPolygonFromCentroid(slabNode.polygon, AUTO_SLAB_INSET),
@@ -199,13 +199,7 @@ function generatePoolGeometry(slabNode: SlabNode): THREE.BufferGeometry {
uvs.push((x - bounds.min.x) / floorWidth, (z - bounds.min.y) / floorHeight)
}
const pushWallVertex = (
x: number,
y: number,
z: number,
u: number,
v: number,
) => {
const pushWallVertex = (x: number, y: number, z: number, u: number, v: number) => {
positions.push(x, y, z)
uvs.push(u, v)
}
@@ -15,6 +15,7 @@ export type FloorplanActionMenuEntry = {
position: SvgPoint | null
onDelete: FloorplanActionMenuHandler
onMove: FloorplanActionMenuHandler
onAddHole?: FloorplanActionMenuHandler
onDuplicate?: FloorplanActionMenuHandler
}
@@ -76,6 +77,7 @@ export const FloorplanActionMenuLayer = memo(function FloorplanActionMenuLayer({
}}
>
<NodeActionMenu
onAddHole={entry.onAddHole}
onDelete={entry.onDelete}
onDuplicate={entry.onDuplicate}
onMove={entry.onMove}
@@ -18,7 +18,10 @@ type FloorplanDraftLayerProps = {
draftAnchorPoints: Array<{ x: number; y: number; isPrimary: boolean }>
draftFill: string
draftStroke: string
polygonDraftStroke?: string
polygonDraftStrokeWidth?: string
anchorFill: string
unitsPerPixel: number
}
export const FloorplanDraftLayer = memo(function FloorplanDraftLayer({
@@ -30,8 +33,15 @@ export const FloorplanDraftLayer = memo(function FloorplanDraftLayer({
draftAnchorPoints,
draftFill,
draftStroke,
polygonDraftStroke,
polygonDraftStrokeWidth = '0.08',
anchorFill,
unitsPerPixel,
}: FloorplanDraftLayerProps) {
const primaryAnchorRadius = 6 * unitsPerPixel
const secondaryAnchorRadius = 5 * unitsPerPixel
const activePolygonDraftStroke = polygonDraftStroke ?? draftStroke
return (
<>
{draftPolygonPoints && (
@@ -69,21 +79,21 @@ export const FloorplanDraftLayer = memo(function FloorplanDraftLayer({
<polyline
fill="none"
points={polygonDraftPolylinePoints}
stroke={draftStroke}
stroke={activePolygonDraftStroke}
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="0.08"
strokeWidth={polygonDraftStrokeWidth}
vectorEffect="non-scaling-stroke"
/>
)}
{polygonDraftClosingSegment && (
<line
stroke={draftStroke}
stroke={activePolygonDraftStroke}
strokeDasharray="0.16 0.1"
strokeLinecap="round"
strokeOpacity={0.75}
strokeWidth="0.05"
strokeWidth={polygonDraftStrokeWidth}
vectorEffect="non-scaling-stroke"
x1={polygonDraftClosingSegment.x1}
x2={polygonDraftClosingSegment.x2}
@@ -100,7 +110,7 @@ export const FloorplanDraftLayer = memo(function FloorplanDraftLayer({
fillOpacity={0.95}
key={`polygon-draft-${index}`}
pointerEvents="none"
r={point.isPrimary ? 0.12 : 0.1}
r={point.isPrimary ? primaryAnchorRadius : secondaryAnchorRadius}
vectorEffect="non-scaling-stroke"
/>
))}
@@ -1,7 +1,7 @@
'use client'
import { memo } from 'react'
import type { Point2D, RoofNode, RoofSegmentNode } from '@pascal-app/core'
import { memo } from 'react'
import { toSvgX, toSvgY } from '../svg-paths'
type FloorplanLineSegment = {
@@ -20,14 +20,27 @@ type FloorplanRoofEntry = {
segments: FloorplanRoofSegmentEntry[]
}
type FloorplanRoofPalette = {
roofFill: string
roofActiveFill: string
roofSelectedFill: string
roofStroke: string
roofActiveStroke: string
roofSelectedStroke: string
roofRidgeStroke: string
roofSelectedRidgeStroke: string
}
type FloorplanRoofLayerProps = {
highlightedIdSet: ReadonlySet<string>
palette: FloorplanRoofPalette
roofEntries: FloorplanRoofEntry[]
selectedIdSet: ReadonlySet<string>
}
export const FloorplanRoofLayer = memo(function FloorplanRoofLayer({
highlightedIdSet,
palette,
roofEntries,
selectedIdSet,
}: FloorplanRoofLayerProps) {
@@ -59,18 +72,18 @@ export const FloorplanRoofLayer = memo(function FloorplanRoofLayer({
<polygon
fill={
isSegmentActive
? 'rgba(14, 165, 233, 0.2)'
? palette.roofSelectedFill
: isRoofActive
? 'rgba(14, 165, 233, 0.14)'
: 'rgba(14, 165, 233, 0.08)'
? palette.roofActiveFill
: palette.roofFill
}
points={points}
stroke={
isSegmentActive
? '#0369a1'
? palette.roofSelectedStroke
: isRoofActive
? '#0ea5e9'
: 'rgba(14, 165, 233, 0.65)'
? palette.roofActiveStroke
: palette.roofStroke
}
strokeWidth={isSegmentActive ? '2.25' : isRoofActive ? '1.75' : '1.1'}
vectorEffect="non-scaling-stroke"
@@ -78,7 +91,9 @@ export const FloorplanRoofLayer = memo(function FloorplanRoofLayer({
{ridgeLine ? (
<line
fill="none"
stroke={isSegmentActive ? '#0f172a' : 'rgba(3, 105, 161, 0.75)'}
stroke={
isSegmentActive ? palette.roofSelectedRidgeStroke : palette.roofRidgeStroke
}
strokeWidth={isSegmentActive ? '2' : '1.4'}
vectorEffect="non-scaling-stroke"
x1={toSvgX(ridgeLine.start.x)}
@@ -1,7 +1,11 @@
'use client'
import type { Point2D, StairNode, StairSegmentNode } from '@pascal-app/core'
import { memo, type MouseEvent as ReactMouseEvent, type PointerEvent as ReactPointerEvent } from 'react'
import {
memo,
type MouseEvent as ReactMouseEvent,
type PointerEvent as ReactPointerEvent,
} from 'react'
import {
buildSvgAnnularSectorPath,
buildSvgArcPath,
@@ -38,6 +42,12 @@ type FloorplanStairEntry = {
type FloorplanPalette = {
deleteFill: string
deleteStroke: string
stairFill: string
stairSelectedFill: string
stairStroke: string
stairAccent: string
stairTread: string
stairSelectedTread: string
}
type FloorplanStairLayerProps = {
@@ -64,8 +74,7 @@ function clamp(value: number, min: number, max: number) {
function getNormalizedFloorplanStairSweepAngle(stair: StairNode) {
const stairType = stair.stairType ?? 'straight'
const baseSweepAngle =
stair.sweepAngle ?? (stairType === 'spiral' ? Math.PI * 2 : Math.PI / 2)
const baseSweepAngle = stair.sweepAngle ?? (stairType === 'spiral' ? Math.PI * 2 : Math.PI / 2)
if (Math.abs(baseSweepAngle) >= Math.PI * 2) {
return Math.sign(baseSweepAngle || 1) * (Math.PI * 2 - 0.001)
@@ -74,6 +83,25 @@ function getNormalizedFloorplanStairSweepAngle(stair: StairNode) {
return baseSweepAngle
}
function getFloorplanStairStepCount(stair: StairNode, minimum: number) {
return Math.max(minimum, Math.round(stair.stepCount ?? 10))
}
function getFloorplanSpiralLandingSweep(stair: StairNode, sweepAngle: number) {
if (stair.stairType !== 'spiral' || (stair.topLandingMode ?? 'none') !== 'integrated') {
return 0
}
const innerRadius = Math.max(0.05, stair.innerRadius ?? 0.9)
const width = Math.max(stair.width ?? 1, 0.4)
const landingDepth = Math.max(0.3, stair.topLandingDepth ?? Math.max(width * 0.9, 0.8))
return (
Math.min(Math.PI * 0.75, landingDepth / Math.max(innerRadius + width / 2, 0.1)) *
Math.sign(sweepAngle || 1)
)
}
export const FloorplanStairLayer = memo(function FloorplanStairLayer({
canFocusStairs,
canSelectStairs,
@@ -108,8 +136,10 @@ export const FloorplanStairLayer = memo(function FloorplanStairLayer({
stairSelected || stairHighlighted || segmentSelected || segmentHighlighted
const stairType = stair.stairType ?? 'straight'
const normalizedSweepAngle = getNormalizedFloorplanStairSweepAngle(stair)
const sectorStartAngle = stair.rotation - normalizedSweepAngle / 2
const sectorStartAngle = -stair.rotation - normalizedSweepAngle / 2
const sectorEndAngle = sectorStartAngle + normalizedSweepAngle
const spiralLandingSweep = getFloorplanSpiralLandingSweep(stair, normalizedSweepAngle)
const visualSectorEndAngle = sectorEndAngle + spiralLandingSweep
const stairCenter = {
x: stair.position[0],
y: stair.position[2],
@@ -124,37 +154,37 @@ export const FloorplanStairLayer = memo(function FloorplanStairLayer({
? palette.deleteStroke
: isSelectionActive
? '#2563eb'
: 'rgba(31, 41, 55, 0.9)'
: palette.stairStroke
const curvedAccent = isDeleteHovered
? palette.deleteStroke
: isSelectionActive
? '#1d4ed8'
: 'rgba(23, 23, 23, 0.96)'
: palette.stairAccent
const curvedFill = isDeleteHovered
? palette.deleteFill
: isSelectionActive
? 'rgba(59, 130, 246, 0.16)'
: '#ffffff'
? palette.stairSelectedFill
: palette.stairFill
const straightAccent = isDeleteHovered
? palette.deleteStroke
: isSelectionActive
? '#1d4ed8'
: 'rgba(23, 23, 23, 0.96)'
: palette.stairAccent
const straightStroke = isDeleteHovered
? palette.deleteStroke
: isSelectionActive
? '#1d4ed8'
: 'rgba(23, 23, 23, 0.88)'
: palette.stairStroke
const straightTread = isDeleteHovered
? palette.deleteStroke
: isSelectionActive
? 'rgba(37, 99, 235, 0.78)'
: 'rgba(38, 38, 38, 0.62)'
? palette.stairSelectedTread
: palette.stairTread
const straightFill = isDeleteHovered
? palette.deleteFill
: isSelectionActive
? 'rgba(59, 130, 246, 0.08)'
: 'rgba(255, 255, 255, 0.02)'
? palette.stairSelectedFill
: palette.stairFill
const curvedOuterLineWidth = isSelectionActive ? '2' : '1.4'
const curvedInnerLineWidth = isSelectionActive ? '1.7' : '1.2'
const stairSymbol =
@@ -166,13 +196,18 @@ export const FloorplanStairLayer = memo(function FloorplanStairLayer({
innerRadius,
outerRadius,
sectorStartAngle,
sectorEndAngle,
visualSectorEndAngle,
)}
fill={curvedFill}
pointerEvents="none"
/>
<path
d={buildSvgArcPath(stairCenter, outerRadius, sectorStartAngle, sectorEndAngle)}
d={buildSvgArcPath(
stairCenter,
outerRadius,
sectorStartAngle,
visualSectorEndAngle,
)}
fill="none"
pointerEvents="none"
stroke={curvedStroke}
@@ -180,15 +215,20 @@ export const FloorplanStairLayer = memo(function FloorplanStairLayer({
vectorEffect="non-scaling-stroke"
/>
<path
d={buildSvgArcPath(stairCenter, innerRadius, sectorStartAngle, sectorEndAngle)}
d={buildSvgArcPath(
stairCenter,
innerRadius,
sectorStartAngle,
visualSectorEndAngle,
)}
fill="none"
pointerEvents="none"
stroke={curvedStroke}
strokeWidth={curvedInnerLineWidth}
vectorEffect="non-scaling-stroke"
/>
{Array.from({ length: Math.max(6, stair.stepCount) }, (_, index) => {
const stepCount = Math.max(6, stair.stepCount)
{Array.from({ length: getFloorplanStairStepCount(stair, 6) + 1 }, (_, index) => {
const stepCount = getFloorplanStairStepCount(stair, 6)
const stepSweep = normalizedSweepAngle / stepCount
const angle = sectorStartAngle + stepSweep * index
const innerPoint = getArcPlanPoint(stairCenter, innerRadius, angle)
@@ -199,9 +239,9 @@ export const FloorplanStairLayer = memo(function FloorplanStairLayer({
<line
key={`${stair.id}:spiral-step:${index}`}
pointerEvents="none"
stroke={index === stepCount - 1 ? curvedAccent : curvedStroke}
stroke={index === stepCount ? curvedAccent : curvedStroke}
strokeDasharray={index >= dashedFromIndex ? '0.1 0.08' : undefined}
strokeWidth={index === stepCount - 1 ? '1.8' : '1.15'}
strokeWidth={index === stepCount ? '1.8' : '1.15'}
vectorEffect="non-scaling-stroke"
x1={toSvgX(innerPoint.x)}
x2={toSvgX(outerPoint.x)}
@@ -213,7 +253,7 @@ export const FloorplanStairLayer = memo(function FloorplanStairLayer({
<circle
cx={toSvgX(stairCenter.x)}
cy={toSvgY(stairCenter.y)}
fill="#ffffff"
fill={curvedFill}
pointerEvents="none"
r={Math.max(innerRadius * 0.18, 0.06)}
stroke={curvedAccent}
@@ -221,7 +261,9 @@ export const FloorplanStairLayer = memo(function FloorplanStairLayer({
vectorEffect="non-scaling-stroke"
/>
{(() => {
const directionAngle = sectorStartAngle + normalizedSweepAngle * 0.86
const directionAngle =
visualSectorEndAngle -
(normalizedSweepAngle / getFloorplanStairStepCount(stair, 6)) * 0.8
const arrowPoint = getArcPlanPoint(stairCenter, centerlineRadius, directionAngle)
const tangentAngle =
directionAngle + (normalizedSweepAngle >= 0 ? Math.PI / 2 : -Math.PI / 2)
@@ -269,8 +311,8 @@ export const FloorplanStairLayer = memo(function FloorplanStairLayer({
strokeWidth={curvedInnerLineWidth}
vectorEffect="non-scaling-stroke"
/>
{Array.from({ length: Math.max(4, stair.stepCount) + 1 }, (_, index) => {
const stepCount = Math.max(4, stair.stepCount)
{Array.from({ length: getFloorplanStairStepCount(stair, 4) + 1 }, (_, index) => {
const stepCount = getFloorplanStairStepCount(stair, 4)
const stepSweep = normalizedSweepAngle / stepCount
const angle = sectorStartAngle + stepSweep * index
const innerPoint = getArcPlanPoint(stairCenter, innerRadius, angle)
@@ -294,8 +336,10 @@ export const FloorplanStairLayer = memo(function FloorplanStairLayer({
d={buildSvgArcPath(
stairCenter,
centerlineRadius,
sectorStartAngle + (normalizedSweepAngle / Math.max(4, stair.stepCount)) * 0.55,
sectorEndAngle - (normalizedSweepAngle / Math.max(4, stair.stepCount)) * 0.55,
sectorStartAngle +
(normalizedSweepAngle / getFloorplanStairStepCount(stair, 4)) * 0.55,
sectorEndAngle -
(normalizedSweepAngle / getFloorplanStairStepCount(stair, 4)) * 0.55,
)}
fill="none"
pointerEvents="none"
@@ -305,7 +349,7 @@ export const FloorplanStairLayer = memo(function FloorplanStairLayer({
vectorEffect="non-scaling-stroke"
/>
{(() => {
const stepCount = Math.max(4, stair.stepCount)
const stepCount = getFloorplanStairStepCount(stair, 4)
const stepSweep = normalizedSweepAngle / stepCount
const arrowAngle = sectorEndAngle - stepSweep * 0.8
const arrowPoint = getArcPlanPoint(stairCenter, centerlineRadius, arrowAngle)
File diff suppressed because it is too large Load Diff
@@ -384,7 +384,7 @@ export const CATALOG_ITEMS: AssetInput[] = [
tags: ['chair', 'seating', 'ergonomic', 'swivel', 'office', 'desk', 'mesh', 'leather', 'modern', 'task', 'workspace', 'professional', 'computer'],
thumbnail: 'https://byrpxoiotywskoojsrzd.supabase.co/storage/v1/object/public/items/system/office-chair/thumbnail.png',
src: 'https://byrpxoiotywskoojsrzd.supabase.co/storage/v1/object/public/items/system/office-chair/model.glb',
floorPlanUrl: 'https://byrpxoiotywskoojsrzd.supabase.co/storage/v1/object/public/items/system/office-chair/floor-plan.png',
floorPlanUrl: '/items/office-chair/floor-plan.svg',
dimensions: [1, 1.2, 1],
offset: [0.01, 0, 0.03],
rotation: [0, 0, 0],
@@ -438,7 +438,7 @@ export const CATALOG_ITEMS: AssetInput[] = [
tags: ['floor', 'seating'],
thumbnail: 'https://byrpxoiotywskoojsrzd.supabase.co/storage/v1/object/public/items/system/sofa/thumbnail.png',
src: 'https://byrpxoiotywskoojsrzd.supabase.co/storage/v1/object/public/items/system/sofa/model.glb',
floorPlanUrl: 'https://byrpxoiotywskoojsrzd.supabase.co/storage/v1/object/public/items/system/sofa/floor-plan.png',
floorPlanUrl: '/items/sofa/floor-plan.svg',
dimensions: [2.5, 0.8, 1.5],
offset: [0, 0, 0.04],
rotation: [0, 0, 0],
@@ -2,7 +2,18 @@
import { Icon as IconifyIcon } from '@iconify/react'
import { useViewer } from '@pascal-app/viewer'
import { Check, ChevronsLeft, ChevronsRight, Columns2, Eye, Footprints, Moon, Sun } from 'lucide-react'
import {
Check,
ChevronsLeft,
ChevronsRight,
Columns2,
Eye,
EyeOff,
Footprints,
Grid2X2,
Moon,
Sun,
} from 'lucide-react'
import { useCallback } from 'react'
import { cn } from '../../lib/utils'
import useEditor from '../../store/use-editor'
@@ -271,6 +282,35 @@ function GridSnapToggle() {
)
}
function GridVisibilityToggle() {
const showGrid = useViewer((s) => s.showGrid)
const setShowGrid = useViewer((s) => s.setShowGrid)
return (
<Tooltip>
<TooltipTrigger asChild>
<button
aria-label={`Grid: ${showGrid ? 'Visible' : 'Hidden'}`}
aria-pressed={showGrid}
className={cn(
TOOLBAR_BTN,
'w-auto gap-1.5 px-2.5',
showGrid
? 'bg-white/10 text-foreground/90'
: 'opacity-60 grayscale hover:opacity-100 hover:grayscale-0',
)}
onClick={() => setShowGrid(!showGrid)}
type="button"
>
<Grid2X2 className="h-3.5 w-3.5" />
{showGrid ? <Eye className="h-3.5 w-3.5" /> : <EyeOff className="h-3.5 w-3.5" />}
</button>
</TooltipTrigger>
<TooltipContent side="bottom">Grid: {showGrid ? 'Visible' : 'Hidden'}</TooltipContent>
</Tooltip>
)
}
// ── Wall mode toggle ────────────────────────────────────────────────────────
const wallModeOrder = ['cutaway', 'up', 'down'] as const
@@ -383,6 +423,7 @@ export function ViewerToolbarRight() {
<LevelModeToggle />
<WallModeToggle />
<GridSnapToggle />
<GridVisibilityToggle />
<div className="my-1.5 w-px bg-border/50" />
<UnitToggle />
<ThemeToggle />
+21 -4
View File
@@ -200,8 +200,7 @@ function getFloorplanArcPoint(center: Point2D, radius: number, angle: number): P
function getNormalizedFloorplanStairSweepAngle(stair: StairNode) {
const stairType = stair.stairType ?? 'straight'
const baseSweepAngle =
stair.sweepAngle ?? (stairType === 'spiral' ? Math.PI * 2 : Math.PI / 2)
const baseSweepAngle = stair.sweepAngle ?? (stairType === 'spiral' ? Math.PI * 2 : Math.PI / 2)
if (Math.abs(baseSweepAngle) >= Math.PI * 2) {
return Math.sign(baseSweepAngle || 1) * (Math.PI * 2 - 0.001)
@@ -210,11 +209,29 @@ function getNormalizedFloorplanStairSweepAngle(stair: StairNode) {
return baseSweepAngle
}
function getFloorplanSpiralLandingSweep(stair: StairNode, sweepAngle: number) {
if (
(stair.stairType ?? 'straight') !== 'spiral' ||
(stair.topLandingMode ?? 'none') !== 'integrated'
) {
return 0
}
const innerRadius = Math.max(0.05, stair.innerRadius ?? 0.9)
const width = Math.max(stair.width ?? 1, 0.4)
const landingDepth = Math.max(0.3, stair.topLandingDepth ?? Math.max(width * 0.9, 0.8))
return (
Math.min(Math.PI * 0.75, landingDepth / Math.max(innerRadius + width / 2, 0.1)) *
Math.sign(sweepAngle || 1)
)
}
function getFloorplanCurvedStairHitPolygon(stair: StairNode): Point2D[] {
const stairType = stair.stairType ?? 'straight'
const sweepAngle = getNormalizedFloorplanStairSweepAngle(stair)
const startAngle = stair.rotation - sweepAngle / 2
const endAngle = startAngle + sweepAngle
const startAngle = -stair.rotation - sweepAngle / 2
const endAngle = startAngle + sweepAngle + getFloorplanSpiralLandingSweep(stair, sweepAngle)
const center = {
x: stair.position[0],
y: stair.position[2],