Clamp spiral stair openings at full sweep
This commit is contained in:
@@ -74,34 +74,7 @@ function normalizeExistingMetadata(
|
|||||||
return holes.map((_, index) => metadata?.[index] ?? { source: 'manual' })
|
return holes.map((_, index) => metadata?.[index] ?? { source: 'manual' })
|
||||||
}
|
}
|
||||||
|
|
||||||
function expandPolygonFromCentroid(polygon: Point2D[], offset: number) {
|
// (Removing expandPolygonRadially in favor of geometric expansion inside the polygon generators)
|
||||||
if (Math.abs(offset) < 1e-6) {
|
|
||||||
return polygon.map(([x, z]) => [x, z] as Point2D)
|
|
||||||
}
|
|
||||||
|
|
||||||
const centroid = polygon.reduce(
|
|
||||||
(acc, [x, z]) => {
|
|
||||||
acc.x += x
|
|
||||||
acc.z += z
|
|
||||||
return acc
|
|
||||||
},
|
|
||||||
{ x: 0, z: 0 },
|
|
||||||
)
|
|
||||||
centroid.x /= Math.max(polygon.length, 1)
|
|
||||||
centroid.z /= Math.max(polygon.length, 1)
|
|
||||||
|
|
||||||
return polygon.map(([x, z]) => {
|
|
||||||
const dx = x - centroid.x
|
|
||||||
const dz = z - centroid.z
|
|
||||||
const length = Math.hypot(dx, dz)
|
|
||||||
if (length < 1e-6) {
|
|
||||||
return [x, z] as Point2D
|
|
||||||
}
|
|
||||||
|
|
||||||
const scale = Math.max(0.1, (length + offset) / length)
|
|
||||||
return [centroid.x + dx * scale, centroid.z + dz * scale] as Point2D
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function rotateXZ(x: number, z: number, angle: number): [number, number] {
|
function rotateXZ(x: number, z: number, angle: number): [number, number] {
|
||||||
const cos = Math.cos(angle)
|
const cos = Math.cos(angle)
|
||||||
@@ -424,17 +397,20 @@ function buildUnionPolygonsFromRects(rects: AxisAlignedRect[]): Point2D[][] {
|
|||||||
return polygons
|
return polygons
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCurvedOpeningPolygon(stair: StairNode): Point2D[] {
|
function getCurvedOpeningPolygon(stair: StairNode, offset: number = 0): Point2D[] {
|
||||||
const width = Math.max(stair.width ?? 1, 0.4)
|
const width = Math.max(stair.width ?? 1, 0.4)
|
||||||
const innerRadius = Math.max(0.2, stair.innerRadius ?? 0.9)
|
const innerRadius = Math.max(0.01, (stair.innerRadius ?? 0.9) - offset)
|
||||||
const outerRadius = innerRadius + width
|
const outerRadius = (stair.innerRadius ?? 0.9) + width + offset
|
||||||
const totalSweep = stair.sweepAngle ?? Math.PI / 2
|
const totalSweep = stair.sweepAngle ?? Math.PI / 2
|
||||||
const openingSweep =
|
const baseOpeningSweep =
|
||||||
Math.sign(totalSweep || 1) *
|
Math.abs(totalSweep) *
|
||||||
Math.max(
|
Math.max(
|
||||||
Math.abs(totalSweep) * CURVED_STAIR_SLAB_OPENING_RATIO,
|
CURVED_STAIR_SLAB_OPENING_RATIO,
|
||||||
Math.abs(totalSweep) / Math.max(stair.stepCount ?? 1, 1),
|
1 / Math.max(stair.stepCount ?? 1, 1),
|
||||||
)
|
)
|
||||||
|
const angleOffset = offset / Math.max(innerRadius, 0.1)
|
||||||
|
const openingSweep = Math.sign(totalSweep || 1) * Math.min(Math.abs(totalSweep), baseOpeningSweep + angleOffset * 2)
|
||||||
|
|
||||||
const startAngle = totalSweep / 2 - openingSweep
|
const startAngle = totalSweep / 2 - openingSweep
|
||||||
const endAngle = totalSweep / 2
|
const endAngle = totalSweep / 2
|
||||||
const segmentCount = Math.max(
|
const segmentCount = Math.max(
|
||||||
@@ -466,8 +442,8 @@ function getCurvedOpeningPolygon(stair: StairNode): Point2D[] {
|
|||||||
return [...outerPoints, ...innerPoints]
|
return [...outerPoints, ...innerPoints]
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSpiralOpeningPolygon(stair: StairNode): Point2D[] {
|
function getSpiralOpeningPolygon(stair: StairNode, offset: number = 0): Point2D[] {
|
||||||
const radius = Math.max(0.05, stair.innerRadius ?? 0.9) + Math.max(stair.width ?? 1, 0.4)
|
const radius = Math.max(0.05, stair.innerRadius ?? 0.9) + Math.max(stair.width ?? 1, 0.4) + offset
|
||||||
const segmentCount = 48
|
const segmentCount = 48
|
||||||
|
|
||||||
return Array.from({ length: segmentCount }).map((_, index) => {
|
return Array.from({ length: segmentCount }).map((_, index) => {
|
||||||
@@ -476,6 +452,22 @@ function getSpiralOpeningPolygon(stair: StairNode): Point2D[] {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getSpiralLandingPolygon(stair: StairNode, offset: number = 0): Point2D[] {
|
||||||
|
const width = Math.max(stair.width ?? 1, 0.4)
|
||||||
|
const outerRadius = Math.max(0.05, (stair.innerRadius ?? 0.9) + width)
|
||||||
|
const depth = Math.max(stair.topLandingDepth ?? 0.9, 0.1)
|
||||||
|
const halfWidth = width / 2
|
||||||
|
|
||||||
|
const localPoints: Point2D[] = [
|
||||||
|
[outerRadius - offset, -halfWidth - offset],
|
||||||
|
[outerRadius + depth + offset, -halfWidth - offset],
|
||||||
|
[outerRadius + depth + offset, halfWidth + offset],
|
||||||
|
[outerRadius - offset, halfWidth + offset],
|
||||||
|
]
|
||||||
|
|
||||||
|
return localPoints.map(([x, z]) => toWorldPlanPoint(stair, x, z))
|
||||||
|
}
|
||||||
|
|
||||||
function getStraightOpeningPolygonsForSurface(
|
function getStraightOpeningPolygonsForSurface(
|
||||||
stair: StairNode,
|
stair: StairNode,
|
||||||
nodes: Record<string, AnyNode>,
|
nodes: Record<string, AnyNode>,
|
||||||
@@ -570,11 +562,16 @@ function getStairOpeningPolygons(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (stair.stairType === 'curved') {
|
if (stair.stairType === 'curved') {
|
||||||
return [getCurvedOpeningPolygon(stair)]
|
return [getCurvedOpeningPolygon(stair, Math.max((stair.openingOffset ?? 0) - STAIR_SLAB_OPENING_TIGHTENING, 0.15))]
|
||||||
}
|
}
|
||||||
|
|
||||||
if (stair.stairType === 'spiral') {
|
if (stair.stairType === 'spiral') {
|
||||||
return [getSpiralOpeningPolygon(stair)]
|
const offset = Math.max((stair.openingOffset ?? 0) - STAIR_SLAB_OPENING_TIGHTENING, 0.15)
|
||||||
|
const polygons = [getSpiralOpeningPolygon(stair, offset)]
|
||||||
|
if (stair.topLandingMode === 'integrated') {
|
||||||
|
polygons.push(getSpiralLandingPolygon(stair, offset))
|
||||||
|
}
|
||||||
|
return polygons
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof targetElevation === 'number') {
|
if (typeof targetElevation === 'number') {
|
||||||
@@ -703,13 +700,7 @@ export function syncAutoStairOpenings(nodes: Record<string, AnyNode>) {
|
|||||||
nodes,
|
nodes,
|
||||||
getTargetSlabElevationForStair(stair, slab, slabLevelId, nodes),
|
getTargetSlabElevationForStair(stair, slab, slabLevelId, nodes),
|
||||||
).map((polygon) => ({
|
).map((polygon) => ({
|
||||||
polygon:
|
|
||||||
stair.stairType === 'straight'
|
|
||||||
? polygon
|
|
||||||
: expandPolygonFromCentroid(
|
|
||||||
polygon,
|
polygon,
|
||||||
Math.max((stair.openingOffset ?? 0) - STAIR_SLAB_OPENING_TIGHTENING, 0),
|
|
||||||
),
|
|
||||||
metadata: {
|
metadata: {
|
||||||
source: 'stair' as const,
|
source: 'stair' as const,
|
||||||
stairId: stair.id,
|
stairId: stair.id,
|
||||||
@@ -758,13 +749,7 @@ export function syncAutoStairOpenings(nodes: Record<string, AnyNode>) {
|
|||||||
nodes,
|
nodes,
|
||||||
getTargetCeilingElevationForStair(stair, ceiling, ceilingLevelId, nodes),
|
getTargetCeilingElevationForStair(stair, ceiling, ceilingLevelId, nodes),
|
||||||
).map((polygon) => ({
|
).map((polygon) => ({
|
||||||
polygon:
|
|
||||||
stair.stairType === 'straight'
|
|
||||||
? polygon
|
|
||||||
: expandPolygonFromCentroid(
|
|
||||||
polygon,
|
polygon,
|
||||||
Math.max((stair.openingOffset ?? 0) - STAIR_SLAB_OPENING_TIGHTENING, 0),
|
|
||||||
),
|
|
||||||
metadata: {
|
metadata: {
|
||||||
source: 'stair' as const,
|
source: 'stair' as const,
|
||||||
stairId: stair.id,
|
stairId: stair.id,
|
||||||
|
|||||||
@@ -83,6 +83,14 @@ function getNormalizedFloorplanStairSweepAngle(stair: StairNode) {
|
|||||||
return baseSweepAngle
|
return baseSweepAngle
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function clampFloorplanCircularSweepAngle(sweepAngle: number) {
|
||||||
|
if (Math.abs(sweepAngle) >= Math.PI * 2) {
|
||||||
|
return Math.sign(sweepAngle || 1) * (Math.PI * 2 - 0.001)
|
||||||
|
}
|
||||||
|
|
||||||
|
return sweepAngle
|
||||||
|
}
|
||||||
|
|
||||||
function getFloorplanStairStepCount(stair: StairNode, minimum: number) {
|
function getFloorplanStairStepCount(stair: StairNode, minimum: number) {
|
||||||
return Math.max(minimum, Math.round(stair.stepCount ?? 10))
|
return Math.max(minimum, Math.round(stair.stepCount ?? 10))
|
||||||
}
|
}
|
||||||
@@ -139,7 +147,10 @@ export const FloorplanStairLayer = memo(function FloorplanStairLayer({
|
|||||||
const sectorStartAngle = -stair.rotation - normalizedSweepAngle / 2
|
const sectorStartAngle = -stair.rotation - normalizedSweepAngle / 2
|
||||||
const sectorEndAngle = sectorStartAngle + normalizedSweepAngle
|
const sectorEndAngle = sectorStartAngle + normalizedSweepAngle
|
||||||
const spiralLandingSweep = getFloorplanSpiralLandingSweep(stair, normalizedSweepAngle)
|
const spiralLandingSweep = getFloorplanSpiralLandingSweep(stair, normalizedSweepAngle)
|
||||||
const visualSectorEndAngle = sectorEndAngle + spiralLandingSweep
|
const visualSweepAngle = clampFloorplanCircularSweepAngle(
|
||||||
|
normalizedSweepAngle + spiralLandingSweep,
|
||||||
|
)
|
||||||
|
const visualSectorEndAngle = sectorStartAngle + visualSweepAngle
|
||||||
const stairCenter = {
|
const stairCenter = {
|
||||||
x: stair.position[0],
|
x: stair.position[0],
|
||||||
y: stair.position[2],
|
y: stair.position[2],
|
||||||
|
|||||||
@@ -209,6 +209,14 @@ function getNormalizedFloorplanStairSweepAngle(stair: StairNode) {
|
|||||||
return baseSweepAngle
|
return baseSweepAngle
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function clampFloorplanCircularSweepAngle(sweepAngle: number) {
|
||||||
|
if (Math.abs(sweepAngle) >= Math.PI * 2) {
|
||||||
|
return Math.sign(sweepAngle || 1) * (Math.PI * 2 - 0.001)
|
||||||
|
}
|
||||||
|
|
||||||
|
return sweepAngle
|
||||||
|
}
|
||||||
|
|
||||||
function getFloorplanSpiralLandingSweep(stair: StairNode, sweepAngle: number) {
|
function getFloorplanSpiralLandingSweep(stair: StairNode, sweepAngle: number) {
|
||||||
if (
|
if (
|
||||||
(stair.stairType ?? 'straight') !== 'spiral' ||
|
(stair.stairType ?? 'straight') !== 'spiral' ||
|
||||||
@@ -230,8 +238,11 @@ function getFloorplanSpiralLandingSweep(stair: StairNode, sweepAngle: number) {
|
|||||||
function getFloorplanCurvedStairHitPolygon(stair: StairNode): Point2D[] {
|
function getFloorplanCurvedStairHitPolygon(stair: StairNode): Point2D[] {
|
||||||
const stairType = stair.stairType ?? 'straight'
|
const stairType = stair.stairType ?? 'straight'
|
||||||
const sweepAngle = getNormalizedFloorplanStairSweepAngle(stair)
|
const sweepAngle = getNormalizedFloorplanStairSweepAngle(stair)
|
||||||
|
const visualSweepAngle = clampFloorplanCircularSweepAngle(
|
||||||
|
sweepAngle + getFloorplanSpiralLandingSweep(stair, sweepAngle),
|
||||||
|
)
|
||||||
const startAngle = -stair.rotation - sweepAngle / 2
|
const startAngle = -stair.rotation - sweepAngle / 2
|
||||||
const endAngle = startAngle + sweepAngle + getFloorplanSpiralLandingSweep(stair, sweepAngle)
|
const endAngle = startAngle + visualSweepAngle
|
||||||
const center = {
|
const center = {
|
||||||
x: stair.position[0],
|
x: stair.position[0],
|
||||||
y: stair.position[2],
|
y: stair.position[2],
|
||||||
|
|||||||
Reference in New Issue
Block a user