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