Refine 2D door swing rendering
This commit is contained in:
@@ -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 |
@@ -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 />
|
||||||
|
|||||||
@@ -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],
|
||||||
|
|||||||
Reference in New Issue
Block a user