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