Fix floorplan labels and door/fence geometry
This commit is contained in:
+91
-68
@@ -128,12 +128,26 @@ type FloorplanMeasurementsLayerProps = {
|
|||||||
className: string
|
className: string
|
||||||
measurements: LinearMeasurementOverlay[]
|
measurements: LinearMeasurementOverlay[]
|
||||||
palette: FloorplanMeasurementPalette
|
palette: FloorplanMeasurementPalette
|
||||||
|
sceneRotationDeg?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeReadableScreenAngle(angleDeg: number) {
|
||||||
|
let normalized = ((((angleDeg + 180) % 360) + 360) % 360) - 180
|
||||||
|
|
||||||
|
if (normalized > 90) {
|
||||||
|
normalized -= 180
|
||||||
|
} else if (normalized <= -90) {
|
||||||
|
normalized += 180
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalized
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FloorplanMeasurementsLayer = memo(function FloorplanMeasurementsLayer({
|
export const FloorplanMeasurementsLayer = memo(function FloorplanMeasurementsLayer({
|
||||||
className,
|
className,
|
||||||
measurements,
|
measurements,
|
||||||
palette,
|
palette,
|
||||||
|
sceneRotationDeg = 0,
|
||||||
}: FloorplanMeasurementsLayerProps) {
|
}: FloorplanMeasurementsLayerProps) {
|
||||||
if (measurements.length === 0) {
|
if (measurements.length === 0) {
|
||||||
return null
|
return null
|
||||||
@@ -141,81 +155,90 @@ export const FloorplanMeasurementsLayer = memo(function FloorplanMeasurementsLay
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{measurements.map((measurement) => (
|
{measurements.map((measurement) =>
|
||||||
<g
|
(() => {
|
||||||
className={className}
|
const screenLabelAngleDeg = normalizeReadableScreenAngle(
|
||||||
key={measurement.id}
|
measurement.labelAngleDeg + sceneRotationDeg,
|
||||||
pointerEvents="none"
|
)
|
||||||
style={{ userSelect: 'none' }}
|
const localLabelAngleDeg = screenLabelAngleDeg - sceneRotationDeg
|
||||||
>
|
|
||||||
<FloorplanMeasurementLine
|
return (
|
||||||
dashed={measurement.dashedExtensions ?? true}
|
<g
|
||||||
isSelected={measurement.isSelected}
|
className={className}
|
||||||
palette={palette}
|
key={measurement.id}
|
||||||
segment={measurement.extensionStart}
|
pointerEvents="none"
|
||||||
stroke={measurement.extensionStroke}
|
style={{ userSelect: 'none' }}
|
||||||
/>
|
>
|
||||||
<FloorplanMeasurementLine
|
<FloorplanMeasurementLine
|
||||||
isSelected={measurement.isSelected}
|
dashed={measurement.dashedExtensions ?? true}
|
||||||
palette={palette}
|
|
||||||
path={measurement.dimensionPathStart}
|
|
||||||
segment={measurement.dimensionLineStart}
|
|
||||||
stroke={measurement.stroke}
|
|
||||||
/>
|
|
||||||
<FloorplanMeasurementLine
|
|
||||||
isSelected={measurement.isSelected}
|
|
||||||
palette={palette}
|
|
||||||
path={measurement.dimensionPathEnd}
|
|
||||||
segment={measurement.dimensionLineEnd}
|
|
||||||
stroke={measurement.stroke}
|
|
||||||
/>
|
|
||||||
<FloorplanMeasurementLine
|
|
||||||
dashed={measurement.dashedExtensions ?? true}
|
|
||||||
isSelected={measurement.isSelected}
|
|
||||||
palette={palette}
|
|
||||||
segment={measurement.extensionEnd}
|
|
||||||
stroke={measurement.extensionStroke}
|
|
||||||
/>
|
|
||||||
{measurement.showTicks !== false ? (
|
|
||||||
<>
|
|
||||||
<FloorplanMeasurementTick
|
|
||||||
angleDeg={measurement.labelAngleDeg}
|
|
||||||
isSelected={measurement.isSelected}
|
isSelected={measurement.isSelected}
|
||||||
palette={palette}
|
palette={palette}
|
||||||
stroke={measurement.stroke}
|
segment={measurement.extensionStart}
|
||||||
x={measurement.dimensionLineStart.x1}
|
stroke={measurement.extensionStroke}
|
||||||
y={measurement.dimensionLineStart.y1}
|
|
||||||
/>
|
/>
|
||||||
<FloorplanMeasurementTick
|
<FloorplanMeasurementLine
|
||||||
angleDeg={measurement.labelAngleDeg}
|
|
||||||
isSelected={measurement.isSelected}
|
isSelected={measurement.isSelected}
|
||||||
palette={palette}
|
palette={palette}
|
||||||
|
path={measurement.dimensionPathStart}
|
||||||
|
segment={measurement.dimensionLineStart}
|
||||||
stroke={measurement.stroke}
|
stroke={measurement.stroke}
|
||||||
x={measurement.dimensionLineEnd.x2}
|
|
||||||
y={measurement.dimensionLineEnd.y2}
|
|
||||||
/>
|
/>
|
||||||
</>
|
<FloorplanMeasurementLine
|
||||||
) : null}
|
isSelected={measurement.isSelected}
|
||||||
<text
|
palette={palette}
|
||||||
dominantBaseline="central"
|
path={measurement.dimensionPathEnd}
|
||||||
fill={measurement.labelFill ?? palette.measurementStroke}
|
segment={measurement.dimensionLineEnd}
|
||||||
fillOpacity={
|
stroke={measurement.stroke}
|
||||||
measurement.isSelected
|
/>
|
||||||
? FLOORPLAN_MEASUREMENT_LABEL_OPACITY
|
<FloorplanMeasurementLine
|
||||||
: FLOORPLAN_MEASUREMENT_LABEL_OPACITY * 0.4
|
dashed={measurement.dashedExtensions ?? true}
|
||||||
}
|
isSelected={measurement.isSelected}
|
||||||
fontFamily="ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace"
|
palette={palette}
|
||||||
fontSize={FLOORPLAN_MEASUREMENT_LABEL_FONT_SIZE}
|
segment={measurement.extensionEnd}
|
||||||
fontWeight="600"
|
stroke={measurement.extensionStroke}
|
||||||
textAnchor="middle"
|
/>
|
||||||
transform={`rotate(${measurement.labelAngleDeg} ${measurement.labelX} ${measurement.labelY}) translate(0, -0.04)`}
|
{measurement.showTicks !== false ? (
|
||||||
x={measurement.labelX}
|
<>
|
||||||
y={measurement.labelY}
|
<FloorplanMeasurementTick
|
||||||
>
|
angleDeg={localLabelAngleDeg}
|
||||||
{measurement.label}
|
isSelected={measurement.isSelected}
|
||||||
</text>
|
palette={palette}
|
||||||
</g>
|
stroke={measurement.stroke}
|
||||||
))}
|
x={measurement.dimensionLineStart.x1}
|
||||||
|
y={measurement.dimensionLineStart.y1}
|
||||||
|
/>
|
||||||
|
<FloorplanMeasurementTick
|
||||||
|
angleDeg={localLabelAngleDeg}
|
||||||
|
isSelected={measurement.isSelected}
|
||||||
|
palette={palette}
|
||||||
|
stroke={measurement.stroke}
|
||||||
|
x={measurement.dimensionLineEnd.x2}
|
||||||
|
y={measurement.dimensionLineEnd.y2}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
<text
|
||||||
|
dominantBaseline="central"
|
||||||
|
fill={measurement.labelFill ?? palette.measurementStroke}
|
||||||
|
fillOpacity={
|
||||||
|
measurement.isSelected
|
||||||
|
? FLOORPLAN_MEASUREMENT_LABEL_OPACITY
|
||||||
|
: FLOORPLAN_MEASUREMENT_LABEL_OPACITY * 0.4
|
||||||
|
}
|
||||||
|
fontFamily="ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace"
|
||||||
|
fontSize={FLOORPLAN_MEASUREMENT_LABEL_FONT_SIZE}
|
||||||
|
fontWeight="600"
|
||||||
|
textAnchor="middle"
|
||||||
|
transform={`rotate(${localLabelAngleDeg} ${measurement.labelX} ${measurement.labelY}) translate(0, -0.04)`}
|
||||||
|
x={measurement.labelX}
|
||||||
|
y={measurement.labelY}
|
||||||
|
>
|
||||||
|
{measurement.label}
|
||||||
|
</text>
|
||||||
|
</g>
|
||||||
|
)
|
||||||
|
})(),
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -482,7 +482,10 @@ export function FloatingActionMenu() {
|
|||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
onMove={
|
onMove={
|
||||||
node && node.type !== 'wall' && !DELETE_ONLY_TYPES.includes(node.type)
|
node &&
|
||||||
|
node.type !== 'wall' &&
|
||||||
|
node.type !== 'fence' &&
|
||||||
|
!DELETE_ONLY_TYPES.includes(node.type)
|
||||||
? handleMove
|
? handleMove
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import {
|
import {
|
||||||
type AnyNodeId,
|
type AnyNodeId,
|
||||||
DEFAULT_WALL_HEIGHT,
|
DEFAULT_WALL_HEIGHT,
|
||||||
|
type FenceNode,
|
||||||
getWallCurveFrameAt,
|
getWallCurveFrameAt,
|
||||||
getWallThickness,
|
getWallThickness,
|
||||||
isCurvedWall,
|
isCurvedWall,
|
||||||
@@ -11,9 +12,8 @@ import {
|
|||||||
type WallNode,
|
type WallNode,
|
||||||
} from '@pascal-app/core'
|
} from '@pascal-app/core'
|
||||||
import { useViewer } from '@pascal-app/viewer'
|
import { useViewer } from '@pascal-app/viewer'
|
||||||
import { createPortal, type ThreeEvent } from '@react-three/fiber'
|
import { createPortal, type ThreeEvent, useThree } from '@react-three/fiber'
|
||||||
import { useEffect, useMemo, useState } from 'react'
|
import { useEffect, useMemo, useState } from 'react'
|
||||||
import { useThree } from '@react-three/fiber'
|
|
||||||
import {
|
import {
|
||||||
BufferGeometry,
|
BufferGeometry,
|
||||||
ConeGeometry,
|
ConeGeometry,
|
||||||
@@ -34,7 +34,6 @@ const ARROW_COLOR = '#8381ed'
|
|||||||
const ARROW_HOVER_COLOR = '#a5b4fc'
|
const ARROW_HOVER_COLOR = '#a5b4fc'
|
||||||
|
|
||||||
type WallMoveHandle = {
|
type WallMoveHandle = {
|
||||||
direction: [number, number]
|
|
||||||
key: string
|
key: string
|
||||||
position: [number, number, number]
|
position: [number, number, number]
|
||||||
rotationY: number
|
rotationY: number
|
||||||
@@ -91,13 +90,13 @@ export function WallMoveSideHandles() {
|
|||||||
const curvingFence = useEditor((state) => state.curvingFence)
|
const curvingFence = useEditor((state) => state.curvingFence)
|
||||||
|
|
||||||
const selectedId = selectedIds.length === 1 ? selectedIds[0] : null
|
const selectedId = selectedIds.length === 1 ? selectedIds[0] : null
|
||||||
const wall = useScene((state) => {
|
const selectedNode = useScene((state) => {
|
||||||
const node = selectedId ? state.nodes[selectedId as AnyNodeId] : null
|
const node = selectedId ? state.nodes[selectedId as AnyNodeId] : null
|
||||||
return node?.type === 'wall' ? node : null
|
return node?.type === 'wall' || node?.type === 'fence' ? node : null
|
||||||
})
|
})
|
||||||
|
|
||||||
const shouldRender =
|
const shouldRender =
|
||||||
Boolean(wall) &&
|
Boolean(selectedNode) &&
|
||||||
!isFloorplanHovered &&
|
!isFloorplanHovered &&
|
||||||
mode !== 'delete' &&
|
mode !== 'delete' &&
|
||||||
!movingNode &&
|
!movingNode &&
|
||||||
@@ -106,9 +105,13 @@ export function WallMoveSideHandles() {
|
|||||||
!curvingWall &&
|
!curvingWall &&
|
||||||
!curvingFence
|
!curvingFence
|
||||||
|
|
||||||
if (!shouldRender || !wall) return null
|
if (!shouldRender || !selectedNode) return null
|
||||||
|
|
||||||
return <WallMoveSideHandlesForWall wall={wall} />
|
return selectedNode.type === 'wall' ? (
|
||||||
|
<WallMoveSideHandlesForWall wall={selectedNode} />
|
||||||
|
) : (
|
||||||
|
<WallMoveSideHandlesForFence fence={selectedNode} />
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function WallMoveSideHandlesForWall({ wall }: { wall: WallNode }) {
|
function WallMoveSideHandlesForWall({ wall }: { wall: WallNode }) {
|
||||||
@@ -163,10 +166,7 @@ function WallMoveArrowHandle({ wall, handle }: { wall: WallNode; handle: WallMov
|
|||||||
const arrowGeometry = useMemo(() => createArrowHandleGeometry(), [])
|
const arrowGeometry = useMemo(() => createArrowHandleGeometry(), [])
|
||||||
const { camera } = useThree()
|
const { camera } = useThree()
|
||||||
|
|
||||||
const zoom =
|
const zoom = camera instanceof OrthographicCamera ? 1 / camera.zoom : 1
|
||||||
camera instanceof OrthographicCamera
|
|
||||||
? 1 / camera.zoom
|
|
||||||
: 1
|
|
||||||
|
|
||||||
const scale = (isHovered ? 1.12 : 1) * zoom
|
const scale = (isHovered ? 1.12 : 1) * zoom
|
||||||
|
|
||||||
@@ -195,11 +195,7 @@ function WallMoveArrowHandle({ wall, handle }: { wall: WallNode; handle: WallMov
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<group
|
<group position={handle.position} rotation={[0, handle.rotationY, 0]} scale={scale}>
|
||||||
position={handle.position}
|
|
||||||
rotation={[0, handle.rotationY, 0]}
|
|
||||||
scale={scale}
|
|
||||||
>
|
|
||||||
<mesh
|
<mesh
|
||||||
frustumCulled={false}
|
frustumCulled={false}
|
||||||
onPointerDown={activateWallMove}
|
onPointerDown={activateWallMove}
|
||||||
@@ -231,6 +227,71 @@ function WallMoveArrowHandle({ wall, handle }: { wall: WallNode; handle: WallMov
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function FenceMoveArrowHandle({ fence, handle }: { fence: FenceNode; handle: WallMoveHandle }) {
|
||||||
|
const [isHovered, setIsHovered] = useState(false)
|
||||||
|
const arrowGeometry = useMemo(() => createArrowHandleGeometry(), [])
|
||||||
|
const { camera } = useThree()
|
||||||
|
|
||||||
|
const zoom = camera instanceof OrthographicCamera ? 1 / camera.zoom : 1
|
||||||
|
const scale = (isHovered ? 1.12 : 1) * zoom
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (document.body.style.cursor === 'grab' || document.body.style.cursor === 'grabbing') {
|
||||||
|
document.body.style.cursor = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => () => arrowGeometry.dispose(), [arrowGeometry])
|
||||||
|
|
||||||
|
const activateFenceMove = (event: ThreeEvent<PointerEvent>) => {
|
||||||
|
event.stopPropagation()
|
||||||
|
event.nativeEvent.preventDefault()
|
||||||
|
document.body.style.cursor = 'grabbing'
|
||||||
|
|
||||||
|
sfxEmitter.emit('sfx:item-pick')
|
||||||
|
useEditor.getState().setMovingNode(fence)
|
||||||
|
useEditor.getState().setMovingWallEndpoint(null)
|
||||||
|
useEditor.getState().setMovingFenceEndpoint(null)
|
||||||
|
useEditor.getState().setCurvingWall(null)
|
||||||
|
useEditor.getState().setCurvingFence(null)
|
||||||
|
useViewer.getState().setSelection({ selectedIds: [] })
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<group position={handle.position} rotation={[0, handle.rotationY, 0]} scale={scale}>
|
||||||
|
<mesh
|
||||||
|
frustumCulled={false}
|
||||||
|
onPointerDown={activateFenceMove}
|
||||||
|
onPointerEnter={(event) => {
|
||||||
|
event.stopPropagation()
|
||||||
|
setIsHovered(true)
|
||||||
|
document.body.style.cursor = 'grab'
|
||||||
|
}}
|
||||||
|
onPointerLeave={(event) => {
|
||||||
|
event.stopPropagation()
|
||||||
|
setIsHovered(false)
|
||||||
|
if (document.body.style.cursor === 'grab') {
|
||||||
|
document.body.style.cursor = ''
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
renderOrder={1002}
|
||||||
|
>
|
||||||
|
<primitive attach="geometry" object={arrowGeometry} />
|
||||||
|
<meshBasicMaterial
|
||||||
|
color={isHovered ? ARROW_HOVER_COLOR : ARROW_COLOR}
|
||||||
|
depthTest
|
||||||
|
depthWrite
|
||||||
|
opacity={1}
|
||||||
|
side={DoubleSide}
|
||||||
|
transparent={false}
|
||||||
|
/>
|
||||||
|
</mesh>
|
||||||
|
</group>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function getWallMoveHandles(wall: WallNode): WallMoveHandle[] {
|
function getWallMoveHandles(wall: WallNode): WallMoveHandle[] {
|
||||||
const dx = wall.end[0] - wall.start[0]
|
const dx = wall.end[0] - wall.start[0]
|
||||||
const dz = wall.end[1] - wall.start[1]
|
const dz = wall.end[1] - wall.start[1]
|
||||||
@@ -257,6 +318,77 @@ function getWallMoveHandles(wall: WallNode): WallMoveHandle[] {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function WallMoveSideHandlesForFence({ fence }: { fence: FenceNode }) {
|
||||||
|
const [levelObject, setLevelObject] = useState<Object3D | null>(() =>
|
||||||
|
fence.parentId ? (sceneRegistry.nodes.get(fence.parentId) ?? null) : null,
|
||||||
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let frameId = 0
|
||||||
|
|
||||||
|
const resolveLevelObject = () => {
|
||||||
|
const nextLevelObject = fence.parentId
|
||||||
|
? (sceneRegistry.nodes.get(fence.parentId) ?? null)
|
||||||
|
: null
|
||||||
|
setLevelObject((currentLevelObject) => {
|
||||||
|
if (currentLevelObject === nextLevelObject) {
|
||||||
|
return currentLevelObject
|
||||||
|
}
|
||||||
|
return nextLevelObject
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!nextLevelObject) {
|
||||||
|
frameId = window.requestAnimationFrame(resolveLevelObject)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resolveLevelObject()
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (frameId) {
|
||||||
|
window.cancelAnimationFrame(frameId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [fence.parentId])
|
||||||
|
|
||||||
|
const handles = useMemo(() => getFenceMoveHandles(fence), [fence])
|
||||||
|
|
||||||
|
if (!levelObject || handles.length === 0) return null
|
||||||
|
|
||||||
|
return createPortal(
|
||||||
|
<group>
|
||||||
|
{handles.map((handle) => (
|
||||||
|
<FenceMoveArrowHandle fence={fence} handle={handle} key={handle.key} />
|
||||||
|
))}
|
||||||
|
</group>,
|
||||||
|
levelObject,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFenceMoveHandles(fence: FenceNode): WallMoveHandle[] {
|
||||||
|
const dx = fence.end[0] - fence.start[0]
|
||||||
|
const dz = fence.end[1] - fence.start[1]
|
||||||
|
const length = Math.hypot(dx, dz)
|
||||||
|
|
||||||
|
if (length < 1e-6) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
const midpoint: [number, number] = [
|
||||||
|
(fence.start[0] + fence.end[0]) / 2,
|
||||||
|
(fence.start[1] + fence.end[1]) / 2,
|
||||||
|
]
|
||||||
|
const normal: [number, number] = [-dz / length, dx / length]
|
||||||
|
const fenceHeight = fence.height ?? 1.8
|
||||||
|
const handleHeight = Math.max(fenceHeight - HANDLE_TOP_INSET, HANDLE_MIN_HEIGHT)
|
||||||
|
const offset = Math.max((fence.thickness ?? 0.1) / 2 + HANDLE_OFFSET, HANDLE_MIN_OFFSET)
|
||||||
|
|
||||||
|
return [
|
||||||
|
buildWallMoveHandle('front', midpoint, normal, offset, handleHeight),
|
||||||
|
buildWallMoveHandle('back', midpoint, [-normal[0], -normal[1]], offset, handleHeight),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
function buildWallMoveHandle(
|
function buildWallMoveHandle(
|
||||||
key: string,
|
key: string,
|
||||||
midpoint: [number, number],
|
midpoint: [number, number],
|
||||||
@@ -265,7 +397,6 @@ function buildWallMoveHandle(
|
|||||||
height: number,
|
height: number,
|
||||||
): WallMoveHandle {
|
): WallMoveHandle {
|
||||||
return {
|
return {
|
||||||
direction,
|
|
||||||
key,
|
key,
|
||||||
position: [midpoint[0] + direction[0] * offset, height, midpoint[1] + direction[1] * offset],
|
position: [midpoint[0] + direction[0] * offset, height, midpoint[1] + direction[1] * offset],
|
||||||
rotationY: Math.atan2(-direction[1], direction[0]),
|
rotationY: Math.atan2(-direction[1], direction[0]),
|
||||||
|
|||||||
@@ -20,17 +20,17 @@ export type ToolConfig = {
|
|||||||
|
|
||||||
export const tools: ToolConfig[] = [
|
export const tools: ToolConfig[] = [
|
||||||
{ id: 'wall', iconSrc: '/icons/wall.png', label: 'Wall' },
|
{ id: 'wall', iconSrc: '/icons/wall.png', label: 'Wall' },
|
||||||
|
{ id: 'door', iconSrc: '/icons/door.png', label: 'Door' },
|
||||||
|
{ id: 'window', iconSrc: '/icons/window.png', label: 'Window' },
|
||||||
|
{ id: 'stair', iconSrc: '/icons/stairs.png', label: 'Stairs' },
|
||||||
|
{ id: 'roof', iconSrc: '/icons/roof.png', label: 'Gable Roof' },
|
||||||
|
{ id: 'fence', iconSrc: '/icons/fence.png', label: 'Fence' },
|
||||||
|
{ id: 'column', iconSrc: '/icons/column.png', label: 'Column' },
|
||||||
|
{ id: 'elevator', iconSrc: '/icons/elevator.png', label: 'Elevator' },
|
||||||
// { id: 'room', iconSrc: '/icons/room.png', label: 'Room' },
|
// { id: 'room', iconSrc: '/icons/room.png', label: 'Room' },
|
||||||
// { id: 'custom-room', iconSrc: '/icons/custom-room.png', label: 'Custom Room' },
|
// { id: 'custom-room', iconSrc: '/icons/custom-room.png', label: 'Custom Room' },
|
||||||
{ id: 'slab', iconSrc: '/icons/floor.png', label: 'Slab' },
|
{ id: 'slab', iconSrc: '/icons/floor.png', label: 'Slab' },
|
||||||
{ id: 'ceiling', iconSrc: '/icons/ceiling.png', label: 'Ceiling' },
|
{ id: 'ceiling', iconSrc: '/icons/ceiling.png', label: 'Ceiling' },
|
||||||
{ id: 'column', iconSrc: '/icons/column.png', label: 'Column' },
|
|
||||||
{ id: 'elevator', iconSrc: '/icons/elevator.png', label: 'Elevator' },
|
|
||||||
{ id: 'roof', iconSrc: '/icons/roof.png', label: 'Gable Roof' },
|
|
||||||
{ id: 'stair', iconSrc: '/icons/stairs.png', label: 'Stairs' },
|
|
||||||
{ id: 'door', iconSrc: '/icons/door.png', label: 'Door' },
|
|
||||||
{ id: 'window', iconSrc: '/icons/window.png', label: 'Window' },
|
|
||||||
{ id: 'fence', iconSrc: '/icons/fence.png', label: 'Fence' },
|
|
||||||
{ id: 'zone', iconSrc: '/icons/zone.png', label: 'Zone' },
|
{ id: 'zone', iconSrc: '/icons/zone.png', label: 'Zone' },
|
||||||
{ id: 'spawn', iconSrc: '/icons/site.png', label: 'Spawn Point' },
|
{ id: 'spawn', iconSrc: '/icons/site.png', label: 'Spawn Point' },
|
||||||
{ id: 'shelf', iconSrc: '/icons/shelf.png', label: 'Shelf' },
|
{ id: 'shelf', iconSrc: '/icons/shelf.png', label: 'Shelf' },
|
||||||
|
|||||||
@@ -80,6 +80,25 @@ const foldingDoorSegments: DoorNode['segments'] = [
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const hingedDoorSegments: DoorNode['segments'] = [
|
||||||
|
{
|
||||||
|
type: 'panel',
|
||||||
|
heightRatio: 0.4,
|
||||||
|
columnRatios: [1],
|
||||||
|
dividerThickness: 0.03,
|
||||||
|
panelDepth: 0.01,
|
||||||
|
panelInset: 0.04,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'panel',
|
||||||
|
heightRatio: 0.6,
|
||||||
|
columnRatios: [1],
|
||||||
|
dividerThickness: 0.03,
|
||||||
|
panelDepth: 0.01,
|
||||||
|
panelInset: 0.04,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
const defaultDoorDimensions: Record<DoorNode['doorType'], { width: number; height: number }> = {
|
const defaultDoorDimensions: Record<DoorNode['doorType'], { width: number; height: number }> = {
|
||||||
hinged: { width: 0.9, height: 2.1 },
|
hinged: { width: 0.9, height: 2.1 },
|
||||||
double: { width: 1.5, height: 2.1 },
|
double: { width: 1.5, height: 2.1 },
|
||||||
@@ -93,6 +112,19 @@ const defaultDoorDimensions: Record<DoorNode['doorType'], { width: number; heigh
|
|||||||
'garage-tiltup': { width: 2.7, height: 2.4 },
|
'garage-tiltup': { width: 2.7, height: 2.4 },
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const defaultDoorSegmentsByType: Record<DoorNode['doorType'], DoorNode['segments']> = {
|
||||||
|
hinged: hingedDoorSegments,
|
||||||
|
double: hingedDoorSegments,
|
||||||
|
french: frenchDoorSegments,
|
||||||
|
folding: foldingDoorSegments,
|
||||||
|
pocket: foldingDoorSegments,
|
||||||
|
barn: foldingDoorSegments,
|
||||||
|
sliding: frenchDoorSegments,
|
||||||
|
'garage-sectional': foldingDoorSegments,
|
||||||
|
'garage-rollup': foldingDoorSegments,
|
||||||
|
'garage-tiltup': foldingDoorSegments,
|
||||||
|
}
|
||||||
|
|
||||||
function isSameDoorValue(current: unknown, next: unknown): boolean {
|
function isSameDoorValue(current: unknown, next: unknown): boolean {
|
||||||
if (typeof current === 'number' && typeof next === 'number') {
|
if (typeof current === 'number' && typeof next === 'number') {
|
||||||
return Math.abs(current - next) < 1e-6
|
return Math.abs(current - next) < 1e-6
|
||||||
@@ -367,7 +399,12 @@ export default function DoorPanel() {
|
|||||||
const typeMode = isOpening ? 'opening' : isGarageDoor ? 'garage' : 'door'
|
const typeMode = isOpening ? 'opening' : isGarageDoor ? 'garage' : 'door'
|
||||||
const supportsHingeSide = doorType === 'hinged'
|
const supportsHingeSide = doorType === 'hinged'
|
||||||
const supportsHandleSide = doorType === 'hinged'
|
const supportsHandleSide = doorType === 'hinged'
|
||||||
const supportsTopShape = !isGarageDoor
|
const supportsTopShape =
|
||||||
|
!isGarageDoor &&
|
||||||
|
doorType !== 'folding' &&
|
||||||
|
doorType !== 'pocket' &&
|
||||||
|
doorType !== 'barn' &&
|
||||||
|
doorType !== 'sliding'
|
||||||
const maxDoorWidth = isGarageDoor ? 6 : 3
|
const maxDoorWidth = isGarageDoor ? 6 : 3
|
||||||
|
|
||||||
const setOpeningTopRadius = (index: number, value: number, commit = false) => {
|
const setOpeningTopRadius = (index: number, value: number, commit = false) => {
|
||||||
@@ -382,6 +419,7 @@ export default function DoorPanel() {
|
|||||||
|
|
||||||
const getDoorTypeUpdates = (nextDoorType: DoorNode['doorType']): Partial<DoorNode> => {
|
const getDoorTypeUpdates = (nextDoorType: DoorNode['doorType']): Partial<DoorNode> => {
|
||||||
const dimensions = defaultDoorDimensions[nextDoorType]
|
const dimensions = defaultDoorDimensions[nextDoorType]
|
||||||
|
const segments = structuredClone(defaultDoorSegmentsByType[nextDoorType])
|
||||||
const dimensionUpdates = {
|
const dimensionUpdates = {
|
||||||
width: dimensions.width,
|
width: dimensions.width,
|
||||||
height: dimensions.height,
|
height: dimensions.height,
|
||||||
@@ -395,10 +433,10 @@ export default function DoorPanel() {
|
|||||||
leafCount: 2,
|
leafCount: 2,
|
||||||
...dimensionUpdates,
|
...dimensionUpdates,
|
||||||
handleSide: 'right',
|
handleSide: 'right',
|
||||||
|
segments,
|
||||||
...(nextDoorType === 'french'
|
...(nextDoorType === 'french'
|
||||||
? {
|
? {
|
||||||
contentPadding: [0.045, 0.055],
|
contentPadding: [0.045, 0.055],
|
||||||
segments: frenchDoorSegments,
|
|
||||||
}
|
}
|
||||||
: {}),
|
: {}),
|
||||||
}
|
}
|
||||||
@@ -410,13 +448,14 @@ export default function DoorPanel() {
|
|||||||
doorType: nextDoorType,
|
doorType: nextDoorType,
|
||||||
leafCount: 4,
|
leafCount: 4,
|
||||||
...dimensionUpdates,
|
...dimensionUpdates,
|
||||||
|
openingShape: 'rectangle',
|
||||||
handle: true,
|
handle: true,
|
||||||
handleSide: 'right',
|
handleSide: 'right',
|
||||||
trackStyle: 'visible',
|
trackStyle: 'visible',
|
||||||
operationState: Math.max(node.operationState ?? 0, 0.65),
|
operationState: Math.max(node.operationState ?? 0, 0.65),
|
||||||
threshold: false,
|
threshold: false,
|
||||||
contentPadding: [0.03, 0.04],
|
contentPadding: [0.03, 0.04],
|
||||||
segments: foldingDoorSegments,
|
segments,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -426,6 +465,7 @@ export default function DoorPanel() {
|
|||||||
doorType: nextDoorType,
|
doorType: nextDoorType,
|
||||||
leafCount: 1,
|
leafCount: 1,
|
||||||
...dimensionUpdates,
|
...dimensionUpdates,
|
||||||
|
openingShape: 'rectangle',
|
||||||
handle: true,
|
handle: true,
|
||||||
handleSide: 'right',
|
handleSide: 'right',
|
||||||
trackStyle: 'pocket',
|
trackStyle: 'pocket',
|
||||||
@@ -433,7 +473,7 @@ export default function DoorPanel() {
|
|||||||
operationState: node.operationState ?? 0,
|
operationState: node.operationState ?? 0,
|
||||||
threshold: false,
|
threshold: false,
|
||||||
contentPadding: [0.035, 0.045],
|
contentPadding: [0.035, 0.045],
|
||||||
segments: foldingDoorSegments,
|
segments,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -443,6 +483,7 @@ export default function DoorPanel() {
|
|||||||
doorType: nextDoorType,
|
doorType: nextDoorType,
|
||||||
leafCount: 1,
|
leafCount: 1,
|
||||||
...dimensionUpdates,
|
...dimensionUpdates,
|
||||||
|
openingShape: 'rectangle',
|
||||||
handle: true,
|
handle: true,
|
||||||
handleSide: 'right',
|
handleSide: 'right',
|
||||||
trackStyle: 'visible',
|
trackStyle: 'visible',
|
||||||
@@ -450,7 +491,7 @@ export default function DoorPanel() {
|
|||||||
operationState: node.operationState ?? 0,
|
operationState: node.operationState ?? 0,
|
||||||
threshold: false,
|
threshold: false,
|
||||||
contentPadding: [0.035, 0.045],
|
contentPadding: [0.035, 0.045],
|
||||||
segments: foldingDoorSegments,
|
segments,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -460,6 +501,7 @@ export default function DoorPanel() {
|
|||||||
doorType: nextDoorType,
|
doorType: nextDoorType,
|
||||||
leafCount: 2,
|
leafCount: 2,
|
||||||
...dimensionUpdates,
|
...dimensionUpdates,
|
||||||
|
openingShape: 'rectangle',
|
||||||
handle: true,
|
handle: true,
|
||||||
handleSide: 'right',
|
handleSide: 'right',
|
||||||
trackStyle: 'visible',
|
trackStyle: 'visible',
|
||||||
@@ -467,7 +509,7 @@ export default function DoorPanel() {
|
|||||||
operationState: node.operationState ?? 0,
|
operationState: node.operationState ?? 0,
|
||||||
threshold: false,
|
threshold: false,
|
||||||
contentPadding: [0.03, 0.04],
|
contentPadding: [0.03, 0.04],
|
||||||
segments: frenchDoorSegments,
|
segments,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -484,7 +526,7 @@ export default function DoorPanel() {
|
|||||||
operationState: 0,
|
operationState: 0,
|
||||||
garagePanelCount: Math.max(3, Math.min(8, node.garagePanelCount ?? 4)),
|
garagePanelCount: Math.max(3, Math.min(8, node.garagePanelCount ?? 4)),
|
||||||
contentPadding: [0.04, 0.04],
|
contentPadding: [0.04, 0.04],
|
||||||
segments: foldingDoorSegments,
|
segments,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -501,7 +543,7 @@ export default function DoorPanel() {
|
|||||||
operationState: 0,
|
operationState: 0,
|
||||||
garagePanelCount: 4,
|
garagePanelCount: 4,
|
||||||
contentPadding: [0.04, 0.04],
|
contentPadding: [0.04, 0.04],
|
||||||
segments: foldingDoorSegments,
|
segments,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -518,7 +560,7 @@ export default function DoorPanel() {
|
|||||||
operationState: 0,
|
operationState: 0,
|
||||||
garagePanelCount: 4,
|
garagePanelCount: 4,
|
||||||
contentPadding: [0.04, 0.04],
|
contentPadding: [0.04, 0.04],
|
||||||
segments: foldingDoorSegments,
|
segments,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -527,6 +569,7 @@ export default function DoorPanel() {
|
|||||||
doorType: nextDoorType,
|
doorType: nextDoorType,
|
||||||
leafCount: 1,
|
leafCount: 1,
|
||||||
...dimensionUpdates,
|
...dimensionUpdates,
|
||||||
|
segments,
|
||||||
threshold: true,
|
threshold: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -109,6 +109,25 @@ function addShape(
|
|||||||
parent.add(mesh)
|
parent.add(mesh)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function addShapeAt(
|
||||||
|
parent: THREE.Object3D,
|
||||||
|
material: THREE.Material,
|
||||||
|
shape: THREE.Shape,
|
||||||
|
depth: number,
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
z: number,
|
||||||
|
) {
|
||||||
|
const geometry = new THREE.ExtrudeGeometry(shape, {
|
||||||
|
depth,
|
||||||
|
bevelEnabled: false,
|
||||||
|
curveSegments: 24,
|
||||||
|
})
|
||||||
|
geometry.translate(x, y, z - depth / 2)
|
||||||
|
const mesh = new THREE.Mesh(geometry, material)
|
||||||
|
parent.add(mesh)
|
||||||
|
}
|
||||||
|
|
||||||
function getClampedArchHeight(width: number, height: number, archHeight: number | undefined) {
|
function getClampedArchHeight(width: number, height: number, archHeight: number | undefined) {
|
||||||
return Math.min(Math.max(archHeight ?? width / 2, 0.01), Math.max(height, 0.01))
|
return Math.min(Math.max(archHeight ?? width / 2, 0.01), Math.max(height, 0.01))
|
||||||
}
|
}
|
||||||
@@ -145,6 +164,51 @@ function getArchBoundaryY(x: number, halfWidth: number, springY: number, archHei
|
|||||||
return springY + archHeight * Math.sqrt(Math.max(1 - t * t, 0))
|
return springY + archHeight * Math.sqrt(Math.max(1 - t * t, 0))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getOneSidedArchBoundaryYAtX(
|
||||||
|
x: number,
|
||||||
|
left: number,
|
||||||
|
right: number,
|
||||||
|
top: number,
|
||||||
|
archHeight: number,
|
||||||
|
curvedSide: 'left' | 'right',
|
||||||
|
) {
|
||||||
|
const width = right - left
|
||||||
|
if (width <= 1e-6) return top
|
||||||
|
const clampedArchHeight = getClampedArchHeight(width, Number.MAX_SAFE_INTEGER, archHeight)
|
||||||
|
const springY = top - clampedArchHeight
|
||||||
|
const distanceFromApex =
|
||||||
|
curvedSide === 'left'
|
||||||
|
? Math.max(0, Math.min((right - x) / width, 1))
|
||||||
|
: Math.max(0, Math.min((x - left) / width, 1))
|
||||||
|
return (
|
||||||
|
springY + clampedArchHeight * Math.sqrt(Math.max(1 - distanceFromApex * distanceFromApex, 0))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRoundedBoundaryYAtX(
|
||||||
|
x: number,
|
||||||
|
left: number,
|
||||||
|
right: number,
|
||||||
|
top: number,
|
||||||
|
radii: TopCornerRadii,
|
||||||
|
) {
|
||||||
|
if (radii.topLeft > 1e-6 && x < left + radii.topLeft) {
|
||||||
|
const centerX = left + radii.topLeft
|
||||||
|
const centerY = top - radii.topLeft
|
||||||
|
const dx = x - centerX
|
||||||
|
return centerY + Math.sqrt(Math.max(radii.topLeft * radii.topLeft - dx * dx, 0))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (radii.topRight > 1e-6 && x > right - radii.topRight) {
|
||||||
|
const centerX = right - radii.topRight
|
||||||
|
const centerY = top - radii.topRight
|
||||||
|
const dx = x - centerX
|
||||||
|
return centerY + Math.sqrt(Math.max(radii.topRight * radii.topRight - dx * dx, 0))
|
||||||
|
}
|
||||||
|
|
||||||
|
return top
|
||||||
|
}
|
||||||
|
|
||||||
function createArchBandShape(
|
function createArchBandShape(
|
||||||
width: number,
|
width: number,
|
||||||
outerSpringY: number,
|
outerSpringY: number,
|
||||||
@@ -358,6 +422,174 @@ function createRoundedLeafFrameShape(
|
|||||||
return outer
|
return outer
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createRoundedClippedLeafFrameShape(
|
||||||
|
left: number,
|
||||||
|
right: number,
|
||||||
|
bottom: number,
|
||||||
|
top: number,
|
||||||
|
fullLeft: number,
|
||||||
|
fullRight: number,
|
||||||
|
radii: TopCornerRadii,
|
||||||
|
insetX: number,
|
||||||
|
insetY: number,
|
||||||
|
) {
|
||||||
|
const outerRadii = normalizeTopCornerRadii(radii, fullRight - fullLeft, top - bottom)
|
||||||
|
const outer = createTopClippedRectShape(left, right, bottom, top, (x) =>
|
||||||
|
getRoundedBoundaryYAtX(x, fullLeft, fullRight, top, {
|
||||||
|
topLeft: outerRadii.topLeft,
|
||||||
|
topRight: outerRadii.topRight,
|
||||||
|
bottomLeft: 0,
|
||||||
|
bottomRight: 0,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!outer) return null
|
||||||
|
|
||||||
|
const innerLeft = left + insetX
|
||||||
|
const innerRight = right - insetX
|
||||||
|
const innerBottom = bottom + insetY
|
||||||
|
const innerTop = top - insetY
|
||||||
|
|
||||||
|
if (innerRight <= innerLeft + 0.01 || innerTop <= innerBottom + 0.01) return outer
|
||||||
|
|
||||||
|
const innerFullLeft = fullLeft + insetX
|
||||||
|
const innerFullRight = fullRight - insetX
|
||||||
|
const innerRadii = normalizeTopCornerRadii(
|
||||||
|
{
|
||||||
|
topLeft: Math.max(outerRadii.topLeft - Math.max(insetX, insetY), 0),
|
||||||
|
topRight: Math.max(outerRadii.topRight - Math.max(insetX, insetY), 0),
|
||||||
|
},
|
||||||
|
innerFullRight - innerFullLeft,
|
||||||
|
innerTop - innerBottom,
|
||||||
|
)
|
||||||
|
const holeShape = createTopClippedRectShape(innerLeft, innerRight, innerBottom, innerTop, (x) =>
|
||||||
|
getRoundedBoundaryYAtX(x, innerFullLeft, innerFullRight, innerTop, {
|
||||||
|
topLeft: innerRadii.topLeft,
|
||||||
|
topRight: innerRadii.topRight,
|
||||||
|
bottomLeft: 0,
|
||||||
|
bottomRight: 0,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
if (holeShape) outer.holes.push(shapeToReversedPath(holeShape))
|
||||||
|
|
||||||
|
return outer
|
||||||
|
}
|
||||||
|
|
||||||
|
function createArchedLeafFrameShape(
|
||||||
|
width: number,
|
||||||
|
bottom: number,
|
||||||
|
top: number,
|
||||||
|
archHeight: number,
|
||||||
|
insetX: number,
|
||||||
|
insetY: number,
|
||||||
|
) {
|
||||||
|
const halfWidth = width / 2
|
||||||
|
const outer = createArchShape(-halfWidth, halfWidth, bottom, top, archHeight)
|
||||||
|
const innerLeft = -halfWidth + insetX
|
||||||
|
const innerRight = halfWidth - insetX
|
||||||
|
const innerBottom = bottom + insetY
|
||||||
|
const innerTop = top - insetY
|
||||||
|
|
||||||
|
if (innerRight <= innerLeft + 0.01 || innerTop <= innerBottom + 0.01) return outer
|
||||||
|
|
||||||
|
const innerArchHeight = getClampedArchHeight(
|
||||||
|
innerRight - innerLeft,
|
||||||
|
innerTop - innerBottom,
|
||||||
|
Math.max(archHeight - insetY, 0.01),
|
||||||
|
)
|
||||||
|
outer.holes.push(
|
||||||
|
shapeToReversedPath(
|
||||||
|
createArchShape(innerLeft, innerRight, innerBottom, innerTop, innerArchHeight),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
return outer
|
||||||
|
}
|
||||||
|
|
||||||
|
function createArchedClippedLeafFrameShape(
|
||||||
|
left: number,
|
||||||
|
right: number,
|
||||||
|
bottom: number,
|
||||||
|
top: number,
|
||||||
|
fullLeft: number,
|
||||||
|
fullRight: number,
|
||||||
|
archHeight: number,
|
||||||
|
insetX: number,
|
||||||
|
insetY: number,
|
||||||
|
) {
|
||||||
|
const fullCenterX = (fullLeft + fullRight) / 2
|
||||||
|
const fullHalfWidth = (fullRight - fullLeft) / 2
|
||||||
|
const springY = top - archHeight
|
||||||
|
const outer = createTopClippedRectShape(left, right, bottom, top, (x) =>
|
||||||
|
getArchBoundaryY(x - fullCenterX, fullHalfWidth, springY, archHeight),
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!outer) return null
|
||||||
|
|
||||||
|
const innerLeft = left + insetX
|
||||||
|
const innerRight = right - insetX
|
||||||
|
const innerBottom = bottom + insetY
|
||||||
|
const innerTop = top - insetY
|
||||||
|
|
||||||
|
if (innerRight <= innerLeft + 0.01 || innerTop <= innerBottom + 0.01) return outer
|
||||||
|
|
||||||
|
const innerFullLeft = fullLeft + insetX
|
||||||
|
const innerFullRight = fullRight - insetX
|
||||||
|
const innerArchHeight = getClampedArchHeight(
|
||||||
|
innerFullRight - innerFullLeft,
|
||||||
|
innerTop - innerBottom,
|
||||||
|
Math.max(archHeight - insetY, 0.01),
|
||||||
|
)
|
||||||
|
const innerFullCenterX = (innerFullLeft + innerFullRight) / 2
|
||||||
|
const innerFullHalfWidth = (innerFullRight - innerFullLeft) / 2
|
||||||
|
const innerSpringY = innerTop - innerArchHeight
|
||||||
|
const holeShape = createTopClippedRectShape(innerLeft, innerRight, innerBottom, innerTop, (x) =>
|
||||||
|
getArchBoundaryY(x - innerFullCenterX, innerFullHalfWidth, innerSpringY, innerArchHeight),
|
||||||
|
)
|
||||||
|
|
||||||
|
if (holeShape) outer.holes.push(shapeToReversedPath(holeShape))
|
||||||
|
|
||||||
|
return outer
|
||||||
|
}
|
||||||
|
|
||||||
|
function createOneSidedArchLeafFrameShape(
|
||||||
|
left: number,
|
||||||
|
right: number,
|
||||||
|
bottom: number,
|
||||||
|
top: number,
|
||||||
|
archHeight: number,
|
||||||
|
insetX: number,
|
||||||
|
insetY: number,
|
||||||
|
curvedSide: 'left' | 'right',
|
||||||
|
) {
|
||||||
|
const outer = createTopClippedRectShape(left, right, bottom, top, (x) =>
|
||||||
|
getOneSidedArchBoundaryYAtX(x, left, right, top, archHeight, curvedSide),
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!outer) return null
|
||||||
|
|
||||||
|
const innerLeft = left + insetX
|
||||||
|
const innerRight = right - insetX
|
||||||
|
const innerBottom = bottom + insetY
|
||||||
|
const innerTop = top - insetY
|
||||||
|
|
||||||
|
if (innerRight <= innerLeft + 0.01 || innerTop <= innerBottom + 0.01) return outer
|
||||||
|
|
||||||
|
const innerArchHeight = getClampedArchHeight(
|
||||||
|
innerRight - innerLeft,
|
||||||
|
innerTop - innerBottom,
|
||||||
|
Math.max(archHeight - insetY, 0.01),
|
||||||
|
)
|
||||||
|
const holeShape = createTopClippedRectShape(innerLeft, innerRight, innerBottom, innerTop, (x) =>
|
||||||
|
getOneSidedArchBoundaryYAtX(x, innerLeft, innerRight, innerTop, innerArchHeight, curvedSide),
|
||||||
|
)
|
||||||
|
|
||||||
|
if (holeShape) outer.holes.push(shapeToReversedPath(holeShape))
|
||||||
|
|
||||||
|
return outer
|
||||||
|
}
|
||||||
|
|
||||||
function createTopClippedRectShape(
|
function createTopClippedRectShape(
|
||||||
left: number,
|
left: number,
|
||||||
right: number,
|
right: number,
|
||||||
@@ -395,6 +627,7 @@ function disposeObject(object: THREE.Object3D) {
|
|||||||
|
|
||||||
function addLeafSegmentContent({
|
function addLeafSegmentContent({
|
||||||
addLeafBox,
|
addLeafBox,
|
||||||
|
addLeafShape,
|
||||||
leafWidth,
|
leafWidth,
|
||||||
leafHeight,
|
leafHeight,
|
||||||
leafCenterX,
|
leafCenterX,
|
||||||
@@ -403,6 +636,11 @@ function addLeafSegmentContent({
|
|||||||
segments,
|
segments,
|
||||||
contentPadding,
|
contentPadding,
|
||||||
keepFrameWhenEmpty = false,
|
keepFrameWhenEmpty = false,
|
||||||
|
renderPerimeterFrame = true,
|
||||||
|
openingShape = 'rectangle',
|
||||||
|
openingTopRadii,
|
||||||
|
archHeight,
|
||||||
|
archOuterSide,
|
||||||
}: {
|
}: {
|
||||||
addLeafBox: (
|
addLeafBox: (
|
||||||
material: THREE.Material,
|
material: THREE.Material,
|
||||||
@@ -413,6 +651,7 @@ function addLeafSegmentContent({
|
|||||||
y: number,
|
y: number,
|
||||||
z: number,
|
z: number,
|
||||||
) => void
|
) => void
|
||||||
|
addLeafShape?: (material: THREE.Material, shape: THREE.Shape, depth: number) => void
|
||||||
leafWidth: number
|
leafWidth: number
|
||||||
leafHeight: number
|
leafHeight: number
|
||||||
leafCenterX: number
|
leafCenterX: number
|
||||||
@@ -421,12 +660,17 @@ function addLeafSegmentContent({
|
|||||||
segments: DoorNode['segments']
|
segments: DoorNode['segments']
|
||||||
contentPadding: DoorNode['contentPadding']
|
contentPadding: DoorNode['contentPadding']
|
||||||
keepFrameWhenEmpty?: boolean
|
keepFrameWhenEmpty?: boolean
|
||||||
|
renderPerimeterFrame?: boolean
|
||||||
|
openingShape?: DoorNode['openingShape']
|
||||||
|
openingTopRadii?: TopCornerRadii
|
||||||
|
archHeight?: number
|
||||||
|
archOuterSide?: 'left' | 'right'
|
||||||
}) {
|
}) {
|
||||||
const hasLeafContent = segments.some((seg) => seg.type !== 'empty')
|
const hasLeafContent = segments.some((seg) => seg.type !== 'empty')
|
||||||
const shouldRenderFrame = hasLeafContent || keepFrameWhenEmpty
|
const shouldRenderFrame = hasLeafContent || keepFrameWhenEmpty
|
||||||
const cpX = contentPadding[0]
|
const cpX = contentPadding[0]
|
||||||
const cpY = contentPadding[1]
|
const cpY = contentPadding[1]
|
||||||
if (shouldRenderFrame && cpY > 0) {
|
if (renderPerimeterFrame && shouldRenderFrame && cpY > 0) {
|
||||||
addLeafBox(
|
addLeafBox(
|
||||||
baseMaterial,
|
baseMaterial,
|
||||||
leafWidth,
|
leafWidth,
|
||||||
@@ -446,7 +690,7 @@ function addLeafSegmentContent({
|
|||||||
0,
|
0,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (shouldRenderFrame && cpX > 0) {
|
if (renderPerimeterFrame && shouldRenderFrame && cpX > 0) {
|
||||||
const innerH = leafHeight - 2 * cpY
|
const innerH = leafHeight - 2 * cpY
|
||||||
addLeafBox(
|
addLeafBox(
|
||||||
baseMaterial,
|
baseMaterial,
|
||||||
@@ -474,9 +718,46 @@ function addLeafSegmentContent({
|
|||||||
const contentTop = leafCenterY + contentH / 2
|
const contentTop = leafCenterY + contentH / 2
|
||||||
|
|
||||||
let segY = contentTop
|
let segY = contentTop
|
||||||
|
const leafLeft = leafCenterX - leafWidth / 2
|
||||||
|
const leafRight = leafCenterX + leafWidth / 2
|
||||||
|
const leafTop = leafCenterY + leafHeight / 2
|
||||||
|
const hasShapedTop = openingShape === 'rounded' || openingShape === 'arch'
|
||||||
|
const clampedLeafArchHeight =
|
||||||
|
openingShape === 'arch' ? getClampedArchHeight(leafWidth, leafHeight, archHeight) : 0
|
||||||
|
const leafSpringY = leafTop - clampedLeafArchHeight
|
||||||
|
const topBoundaryAtX = (x: number) => {
|
||||||
|
if (openingShape === 'rounded' && openingTopRadii) {
|
||||||
|
return getRoundedBoundaryYAtX(x, leafLeft, leafRight, leafTop, {
|
||||||
|
topLeft: openingTopRadii.topLeft,
|
||||||
|
topRight: openingTopRadii.topRight,
|
||||||
|
bottomLeft: 0,
|
||||||
|
bottomRight: 0,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (openingShape === 'arch') {
|
||||||
|
if (archOuterSide) {
|
||||||
|
return getOneSidedArchBoundaryYAtX(
|
||||||
|
x,
|
||||||
|
leafLeft,
|
||||||
|
leafRight,
|
||||||
|
leafTop,
|
||||||
|
archHeight,
|
||||||
|
archOuterSide,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return getArchBoundaryY(x - leafCenterX, leafWidth / 2, leafSpringY, clampedLeafArchHeight)
|
||||||
|
}
|
||||||
|
|
||||||
|
return leafTop
|
||||||
|
}
|
||||||
|
|
||||||
for (const seg of segments) {
|
for (const seg of segments) {
|
||||||
const segH = (seg.heightRatio / totalRatio) * contentH
|
const segH = (seg.heightRatio / totalRatio) * contentH
|
||||||
const segCenterY = segY - segH / 2
|
const segCenterY = segY - segH / 2
|
||||||
|
const segTop = segY
|
||||||
|
const segBottom = segY - segH
|
||||||
const numCols = seg.columnRatios.length
|
const numCols = seg.columnRatios.length
|
||||||
const colSum = seg.columnRatios.reduce((a, b) => a + b, 0)
|
const colSum = seg.columnRatios.reduce((a, b) => a + b, 0)
|
||||||
const usableW = contentW - (numCols - 1) * seg.dividerThickness
|
const usableW = contentW - (numCols - 1) * seg.dividerThickness
|
||||||
@@ -494,15 +775,32 @@ function addLeafSegmentContent({
|
|||||||
cx = leafCenterX - contentW / 2
|
cx = leafCenterX - contentW / 2
|
||||||
for (let c = 0; c < numCols - 1; c++) {
|
for (let c = 0; c < numCols - 1; c++) {
|
||||||
cx += colWidths[c]!
|
cx += colWidths[c]!
|
||||||
addLeafBox(
|
const dividerLeft = cx
|
||||||
baseMaterial,
|
const dividerRight = cx + seg.dividerThickness
|
||||||
seg.dividerThickness,
|
const dividerShape =
|
||||||
segH,
|
hasShapedTop && addLeafShape
|
||||||
leafDepth + 0.001,
|
? createTopClippedRectShape(
|
||||||
cx + seg.dividerThickness / 2,
|
dividerLeft,
|
||||||
segCenterY,
|
dividerRight,
|
||||||
0,
|
segBottom,
|
||||||
)
|
segTop,
|
||||||
|
topBoundaryAtX,
|
||||||
|
)
|
||||||
|
: null
|
||||||
|
|
||||||
|
if (dividerShape && addLeafShape) {
|
||||||
|
addLeafShape(baseMaterial, dividerShape, leafDepth + 0.001)
|
||||||
|
} else {
|
||||||
|
addLeafBox(
|
||||||
|
baseMaterial,
|
||||||
|
seg.dividerThickness,
|
||||||
|
segH,
|
||||||
|
leafDepth + 0.001,
|
||||||
|
cx + seg.dividerThickness / 2,
|
||||||
|
segCenterY,
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
}
|
||||||
cx += seg.dividerThickness
|
cx += seg.dividerThickness
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -513,15 +811,68 @@ function addLeafSegmentContent({
|
|||||||
|
|
||||||
if (seg.type === 'glass') {
|
if (seg.type === 'glass') {
|
||||||
const glassDepth = Math.max(0.004, leafDepth * 0.15)
|
const glassDepth = Math.max(0.004, leafDepth * 0.15)
|
||||||
addLeafBox(glassMaterial, colW, segH, glassDepth, colX, segCenterY, 0)
|
const segmentLeft = colX - colW / 2
|
||||||
|
const segmentRight = colX + colW / 2
|
||||||
|
const glassShape =
|
||||||
|
hasShapedTop && addLeafShape
|
||||||
|
? createTopClippedRectShape(
|
||||||
|
segmentLeft,
|
||||||
|
segmentRight,
|
||||||
|
segBottom,
|
||||||
|
segTop,
|
||||||
|
topBoundaryAtX,
|
||||||
|
)
|
||||||
|
: null
|
||||||
|
|
||||||
|
if (glassShape && addLeafShape) {
|
||||||
|
addLeafShape(glassMaterial, glassShape, glassDepth)
|
||||||
|
} else {
|
||||||
|
addLeafBox(glassMaterial, colW, segH, glassDepth, colX, segCenterY, 0)
|
||||||
|
}
|
||||||
} else if (seg.type === 'panel') {
|
} else if (seg.type === 'panel') {
|
||||||
addLeafBox(baseMaterial, colW, segH, leafDepth, colX, segCenterY, 0)
|
const segmentLeft = colX - colW / 2
|
||||||
|
const segmentRight = colX + colW / 2
|
||||||
|
const outerPanelShape =
|
||||||
|
hasShapedTop && addLeafShape
|
||||||
|
? createTopClippedRectShape(
|
||||||
|
segmentLeft,
|
||||||
|
segmentRight,
|
||||||
|
segBottom,
|
||||||
|
segTop,
|
||||||
|
topBoundaryAtX,
|
||||||
|
)
|
||||||
|
: null
|
||||||
|
|
||||||
|
if (outerPanelShape && addLeafShape) {
|
||||||
|
addLeafShape(baseMaterial, outerPanelShape, leafDepth)
|
||||||
|
} else {
|
||||||
|
addLeafBox(baseMaterial, colW, segH, leafDepth, colX, segCenterY, 0)
|
||||||
|
}
|
||||||
const panelW = colW - 2 * seg.panelInset
|
const panelW = colW - 2 * seg.panelInset
|
||||||
const panelH = segH - 2 * seg.panelInset
|
const panelH = segH - 2 * seg.panelInset
|
||||||
if (panelW > 0.01 && panelH > 0.01) {
|
if (panelW > 0.01 && panelH > 0.01) {
|
||||||
const effectiveDepth = Math.abs(seg.panelDepth) < 0.002 ? 0.005 : Math.abs(seg.panelDepth)
|
const effectiveDepth = Math.abs(seg.panelDepth) < 0.002 ? 0.005 : Math.abs(seg.panelDepth)
|
||||||
const panelZ = leafDepth / 2 + effectiveDepth / 2
|
const panelZ = leafDepth / 2 + effectiveDepth / 2
|
||||||
addLeafBox(baseMaterial, panelW, panelH, effectiveDepth, colX, segCenterY, panelZ)
|
const insetLeft = colX - panelW / 2
|
||||||
|
const insetRight = colX + panelW / 2
|
||||||
|
const insetTop = segTop - seg.panelInset
|
||||||
|
const insetBottom = segBottom + seg.panelInset
|
||||||
|
const innerPanelShape =
|
||||||
|
hasShapedTop && addLeafShape
|
||||||
|
? createTopClippedRectShape(
|
||||||
|
insetLeft,
|
||||||
|
insetRight,
|
||||||
|
insetBottom,
|
||||||
|
insetTop,
|
||||||
|
topBoundaryAtX,
|
||||||
|
)
|
||||||
|
: null
|
||||||
|
|
||||||
|
if (innerPanelShape && addLeafShape) {
|
||||||
|
addLeafShape(baseMaterial, innerPanelShape, effectiveDepth)
|
||||||
|
} else {
|
||||||
|
addLeafBox(baseMaterial, panelW, panelH, effectiveDepth, colX, segCenterY, panelZ)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -551,6 +902,12 @@ function addDoorLeaf(
|
|||||||
panicBar,
|
panicBar,
|
||||||
panicBarHeight,
|
panicBarHeight,
|
||||||
doorHeight,
|
doorHeight,
|
||||||
|
openingShape,
|
||||||
|
openingTopRadii,
|
||||||
|
archHeight,
|
||||||
|
roundedBoundary,
|
||||||
|
archedBoundary,
|
||||||
|
archOuterSide,
|
||||||
}: {
|
}: {
|
||||||
leafWidth: number
|
leafWidth: number
|
||||||
leafHeight: number
|
leafHeight: number
|
||||||
@@ -570,6 +927,20 @@ function addDoorLeaf(
|
|||||||
panicBar: boolean
|
panicBar: boolean
|
||||||
panicBarHeight: number
|
panicBarHeight: number
|
||||||
doorHeight: number
|
doorHeight: number
|
||||||
|
openingShape: DoorNode['openingShape']
|
||||||
|
openingTopRadii: TopCornerRadii
|
||||||
|
archHeight: number
|
||||||
|
roundedBoundary?: {
|
||||||
|
fullLeft: number
|
||||||
|
fullRight: number
|
||||||
|
radii: TopCornerRadii
|
||||||
|
}
|
||||||
|
archedBoundary?: {
|
||||||
|
fullLeft: number
|
||||||
|
fullRight: number
|
||||||
|
archHeight: number
|
||||||
|
}
|
||||||
|
archOuterSide?: 'left' | 'right'
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
const hasLeafContent = segments.some((seg) => seg.type !== 'empty')
|
const hasLeafContent = segments.some((seg) => seg.type !== 'empty')
|
||||||
@@ -587,9 +958,90 @@ function addDoorLeaf(
|
|||||||
y: number,
|
y: number,
|
||||||
z: number,
|
z: number,
|
||||||
) => addBox(leafGroup, material, w, h, d, x - hingeX, y, z)
|
) => addBox(leafGroup, material, w, h, d, x - hingeX, y, z)
|
||||||
|
const addLeafShape = (material: THREE.Material, shape: THREE.Shape, depth: number) =>
|
||||||
|
addShapeAt(leafGroup, material, shape, depth, -hingeX, 0, 0)
|
||||||
|
|
||||||
|
const localLeafCenterX = leafCenterX - hingeX
|
||||||
|
const leafBottom = leafCenterY - leafHeight / 2
|
||||||
|
const leafTop = leafCenterY + leafHeight / 2
|
||||||
|
const usesShapedLeafFrame = openingShape === 'rounded' || openingShape === 'arch'
|
||||||
|
|
||||||
|
if (usesShapedLeafFrame && hasLeafContent) {
|
||||||
|
if (openingShape === 'rounded') {
|
||||||
|
const roundedLeafShape = roundedBoundary
|
||||||
|
? createRoundedClippedLeafFrameShape(
|
||||||
|
leafCenterX - leafWidth / 2,
|
||||||
|
leafCenterX + leafWidth / 2,
|
||||||
|
leafBottom,
|
||||||
|
leafTop,
|
||||||
|
roundedBoundary.fullLeft,
|
||||||
|
roundedBoundary.fullRight,
|
||||||
|
roundedBoundary.radii,
|
||||||
|
contentPadding[0],
|
||||||
|
contentPadding[1],
|
||||||
|
)
|
||||||
|
: createRoundedLeafFrameShape(
|
||||||
|
leafWidth,
|
||||||
|
leafBottom,
|
||||||
|
leafTop,
|
||||||
|
openingTopRadii,
|
||||||
|
contentPadding[0],
|
||||||
|
contentPadding[1],
|
||||||
|
)
|
||||||
|
|
||||||
|
if (roundedLeafShape) {
|
||||||
|
if (roundedBoundary) {
|
||||||
|
addLeafShape(baseMaterial, roundedLeafShape, leafDepth)
|
||||||
|
} else {
|
||||||
|
addShapeAt(leafGroup, baseMaterial, roundedLeafShape, leafDepth, localLeafCenterX, 0, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (openingShape === 'arch') {
|
||||||
|
const archedLeafShape = archOuterSide
|
||||||
|
? createOneSidedArchLeafFrameShape(
|
||||||
|
leafCenterX - leafWidth / 2,
|
||||||
|
leafCenterX + leafWidth / 2,
|
||||||
|
leafBottom,
|
||||||
|
leafTop,
|
||||||
|
archHeight,
|
||||||
|
contentPadding[0],
|
||||||
|
contentPadding[1],
|
||||||
|
archOuterSide,
|
||||||
|
)
|
||||||
|
: archedBoundary
|
||||||
|
? createArchedClippedLeafFrameShape(
|
||||||
|
leafCenterX - leafWidth / 2,
|
||||||
|
leafCenterX + leafWidth / 2,
|
||||||
|
leafBottom,
|
||||||
|
leafTop,
|
||||||
|
archedBoundary.fullLeft,
|
||||||
|
archedBoundary.fullRight,
|
||||||
|
archedBoundary.archHeight,
|
||||||
|
contentPadding[0],
|
||||||
|
contentPadding[1],
|
||||||
|
)
|
||||||
|
: createArchedLeafFrameShape(
|
||||||
|
leafWidth,
|
||||||
|
leafBottom,
|
||||||
|
leafTop,
|
||||||
|
getClampedArchHeight(leafWidth, leafHeight, archHeight),
|
||||||
|
contentPadding[0],
|
||||||
|
contentPadding[1],
|
||||||
|
)
|
||||||
|
|
||||||
|
if (archedLeafShape) {
|
||||||
|
if (archOuterSide || archedBoundary) {
|
||||||
|
addLeafShape(baseMaterial, archedLeafShape, leafDepth)
|
||||||
|
} else {
|
||||||
|
addShapeAt(leafGroup, baseMaterial, archedLeafShape, leafDepth, localLeafCenterX, 0, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
addLeafSegmentContent({
|
addLeafSegmentContent({
|
||||||
addLeafBox,
|
addLeafBox,
|
||||||
|
addLeafShape,
|
||||||
leafWidth,
|
leafWidth,
|
||||||
leafHeight,
|
leafHeight,
|
||||||
leafCenterX,
|
leafCenterX,
|
||||||
@@ -597,6 +1049,11 @@ function addDoorLeaf(
|
|||||||
leafDepth,
|
leafDepth,
|
||||||
segments,
|
segments,
|
||||||
contentPadding,
|
contentPadding,
|
||||||
|
renderPerimeterFrame: !usesShapedLeafFrame,
|
||||||
|
openingShape,
|
||||||
|
openingTopRadii,
|
||||||
|
archHeight,
|
||||||
|
archOuterSide,
|
||||||
})
|
})
|
||||||
|
|
||||||
if (hasLeafContent && handle) {
|
if (hasLeafContent && handle) {
|
||||||
@@ -640,8 +1097,6 @@ function addDoorLeaf(
|
|||||||
const hingeH = 0.1
|
const hingeH = 0.1
|
||||||
const hingeW = 0.024
|
const hingeW = 0.024
|
||||||
const hingeD = leafDepth + 0.016
|
const hingeD = leafDepth + 0.016
|
||||||
const leafBottom = leafCenterY - leafHeight / 2
|
|
||||||
const leafTop = leafCenterY + leafHeight / 2
|
|
||||||
addBox(mesh, baseMaterial, hingeW, hingeH, hingeD, hingeMarkerX, leafBottom + 0.25, 0)
|
addBox(mesh, baseMaterial, hingeW, hingeH, hingeD, hingeMarkerX, leafBottom + 0.25, 0)
|
||||||
addBox(mesh, baseMaterial, hingeW, hingeH, hingeD, hingeMarkerX, (leafBottom + leafTop) / 2, 0)
|
addBox(mesh, baseMaterial, hingeW, hingeH, hingeD, hingeMarkerX, (leafBottom + leafTop) / 2, 0)
|
||||||
addBox(mesh, baseMaterial, hingeW, hingeH, hingeD, hingeMarkerX, leafTop - 0.25, 0)
|
addBox(mesh, baseMaterial, hingeW, hingeH, hingeD, hingeMarkerX, leafTop - 0.25, 0)
|
||||||
@@ -1401,6 +1856,15 @@ function addGarageTiltupDoor(
|
|||||||
addBox(mesh, revealMaterial, insideWidth, 0.026, Math.max(frameDepth * 0.4, 0.035), 0, hingeY, 0)
|
addBox(mesh, revealMaterial, insideWidth, 0.026, Math.max(frameDepth * 0.4, 0.035), 0, hingeY, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getEffectiveOpeningShape(node: DoorNode): DoorNode['openingShape'] {
|
||||||
|
return node.doorType === 'folding' ||
|
||||||
|
node.doorType === 'pocket' ||
|
||||||
|
node.doorType === 'barn' ||
|
||||||
|
node.doorType === 'sliding'
|
||||||
|
? 'rectangle'
|
||||||
|
: (node.openingShape ?? 'rectangle')
|
||||||
|
}
|
||||||
|
|
||||||
function updateDoorMesh(node: DoorNode, mesh: THREE.Mesh) {
|
function updateDoorMesh(node: DoorNode, mesh: THREE.Mesh) {
|
||||||
// Root mesh is an invisible hitbox; all visuals live in child meshes
|
// Root mesh is an invisible hitbox; all visuals live in child meshes
|
||||||
mesh.geometry.dispose()
|
mesh.geometry.dispose()
|
||||||
@@ -1422,7 +1886,7 @@ function updateDoorMesh(node: DoorNode, mesh: THREE.Mesh) {
|
|||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
openingKind,
|
openingKind,
|
||||||
openingShape,
|
openingShape: rawOpeningShape,
|
||||||
frameThickness,
|
frameThickness,
|
||||||
frameDepth,
|
frameDepth,
|
||||||
threshold,
|
threshold,
|
||||||
@@ -1444,6 +1908,7 @@ function updateDoorMesh(node: DoorNode, mesh: THREE.Mesh) {
|
|||||||
slideDirection = 'left',
|
slideDirection = 'left',
|
||||||
garagePanelCount = 4,
|
garagePanelCount = 4,
|
||||||
} = node
|
} = node
|
||||||
|
const openingShape = getEffectiveOpeningShape(node) ?? rawOpeningShape
|
||||||
const runtimeDoorState = useInteractive.getState().doors[node.id]
|
const runtimeDoorState = useInteractive.getState().doors[node.id]
|
||||||
const swingAngle = runtimeDoorState?.swingAngle ?? nodeSwingAngle
|
const swingAngle = runtimeDoorState?.swingAngle ?? nodeSwingAngle
|
||||||
const operationState = runtimeDoorState?.operationState ?? nodeOperationState
|
const operationState = runtimeDoorState?.operationState ?? nodeOperationState
|
||||||
@@ -1662,6 +2127,15 @@ function updateDoorMesh(node: DoorNode, mesh: THREE.Mesh) {
|
|||||||
})
|
})
|
||||||
} else if (doorType === 'double' || doorType === 'french') {
|
} else if (doorType === 'double' || doorType === 'french') {
|
||||||
const doubleLeafW = insideWidth / 2
|
const doubleLeafW = insideWidth / 2
|
||||||
|
const fullLeafTopRadii = getDoorTopRadii(node, insideWidth, leafH)
|
||||||
|
const roundedBoundary =
|
||||||
|
openingShape === 'rounded'
|
||||||
|
? {
|
||||||
|
fullLeft: -insideWidth / 2,
|
||||||
|
fullRight: insideWidth / 2,
|
||||||
|
radii: fullLeafTopRadii,
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
addDoorLeaf(mesh, {
|
addDoorLeaf(mesh, {
|
||||||
leafWidth: doubleLeafW,
|
leafWidth: doubleLeafW,
|
||||||
leafHeight: leafH,
|
leafHeight: leafH,
|
||||||
@@ -1681,6 +2155,14 @@ function updateDoorMesh(node: DoorNode, mesh: THREE.Mesh) {
|
|||||||
panicBar,
|
panicBar,
|
||||||
panicBarHeight,
|
panicBarHeight,
|
||||||
doorHeight: height,
|
doorHeight: height,
|
||||||
|
openingShape,
|
||||||
|
openingTopRadii:
|
||||||
|
openingShape === 'rounded'
|
||||||
|
? { topLeft: fullLeafTopRadii.topLeft, topRight: 0 }
|
||||||
|
: fullLeafTopRadii,
|
||||||
|
archHeight: node.archHeight ?? 0.45,
|
||||||
|
roundedBoundary,
|
||||||
|
archOuterSide: openingShape === 'arch' ? 'left' : undefined,
|
||||||
})
|
})
|
||||||
addDoorLeaf(mesh, {
|
addDoorLeaf(mesh, {
|
||||||
leafWidth: doubleLeafW,
|
leafWidth: doubleLeafW,
|
||||||
@@ -1701,6 +2183,14 @@ function updateDoorMesh(node: DoorNode, mesh: THREE.Mesh) {
|
|||||||
panicBar,
|
panicBar,
|
||||||
panicBarHeight,
|
panicBarHeight,
|
||||||
doorHeight: height,
|
doorHeight: height,
|
||||||
|
openingShape,
|
||||||
|
openingTopRadii:
|
||||||
|
openingShape === 'rounded'
|
||||||
|
? { topLeft: 0, topRight: fullLeafTopRadii.topRight }
|
||||||
|
: fullLeafTopRadii,
|
||||||
|
archHeight: node.archHeight ?? 0.45,
|
||||||
|
roundedBoundary,
|
||||||
|
archOuterSide: openingShape === 'arch' ? 'right' : undefined,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
const hingeX = hingesSide === 'right' ? insideWidth / 2 : -insideWidth / 2
|
const hingeX = hingesSide === 'right' ? insideWidth / 2 : -insideWidth / 2
|
||||||
@@ -1724,6 +2214,9 @@ function updateDoorMesh(node: DoorNode, mesh: THREE.Mesh) {
|
|||||||
panicBar,
|
panicBar,
|
||||||
panicBarHeight,
|
panicBarHeight,
|
||||||
doorHeight: height,
|
doorHeight: height,
|
||||||
|
openingShape,
|
||||||
|
openingTopRadii: getDoorTopRadii(node, insideWidth, leafH),
|
||||||
|
archHeight: node.archHeight ?? 0.45,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1739,7 +2232,8 @@ function syncDoorCutout(node: DoorNode, mesh: THREE.Mesh) {
|
|||||||
mesh.add(cutout)
|
mesh.add(cutout)
|
||||||
}
|
}
|
||||||
cutout.geometry.dispose()
|
cutout.geometry.dispose()
|
||||||
if (node.openingShape === 'arch') {
|
const openingShape = getEffectiveOpeningShape(node)
|
||||||
|
if (openingShape === 'arch') {
|
||||||
cutout.geometry = new THREE.ExtrudeGeometry(
|
cutout.geometry = new THREE.ExtrudeGeometry(
|
||||||
createArchShape(
|
createArchShape(
|
||||||
-node.width / 2,
|
-node.width / 2,
|
||||||
@@ -1755,7 +2249,7 @@ function syncDoorCutout(node: DoorNode, mesh: THREE.Mesh) {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
cutout.geometry.translate(0, 0, -0.5)
|
cutout.geometry.translate(0, 0, -0.5)
|
||||||
} else if (node.openingShape === 'rounded') {
|
} else if (openingShape === 'rounded') {
|
||||||
cutout.geometry = new THREE.ExtrudeGeometry(
|
cutout.geometry = new THREE.ExtrudeGeometry(
|
||||||
createRoundedTopShape(
|
createRoundedTopShape(
|
||||||
-node.width / 2,
|
-node.width / 2,
|
||||||
|
|||||||
Reference in New Issue
Block a user