Add frameless door openings and first-person collider support
This commit is contained in:
@@ -32,6 +32,13 @@ export const DoorNode = BaseNode.extend({
|
||||
width: z.number().default(0.9),
|
||||
height: z.number().default(2.1),
|
||||
|
||||
// Opening mode
|
||||
openingKind: z.enum(['door', 'opening']).default('door'),
|
||||
openingShape: z.enum(['rectangle', 'rounded', 'arch']).default('rectangle'),
|
||||
cornerRadius: z.number().min(0).default(0.15),
|
||||
archHeight: z.number().min(0).default(0.45),
|
||||
openingRevealRadius: z.number().min(0).default(0.025),
|
||||
|
||||
// Frame
|
||||
frameThickness: z.number().default(0.05),
|
||||
frameDepth: z.number().default(0.07),
|
||||
@@ -81,6 +88,7 @@ export const DoorNode = BaseNode.extend({
|
||||
panicBarHeight: z.number().default(1.0),
|
||||
}).describe(dedent`Door node - a parametric door placed on a wall
|
||||
- position: center of the door in wall-local coordinate system (Y = height/2, always at floor)
|
||||
- openingKind/openingShape: hinged door or frameless wall opening shape
|
||||
- segments: rows stacked top to bottom, each defining its own columnRatios
|
||||
- type 'empty' = no leaf fill for that segment, 'panel' = raised/recessed panel, 'glass' = glazed
|
||||
- hingesSide/swingDirection/swingAngle: which way the door opens and how far it is currently open
|
||||
|
||||
@@ -78,6 +78,7 @@ function updateDoorMesh(node: DoorNode, mesh: THREE.Mesh) {
|
||||
const {
|
||||
width,
|
||||
height,
|
||||
openingKind,
|
||||
frameThickness,
|
||||
frameDepth,
|
||||
threshold,
|
||||
@@ -97,6 +98,11 @@ function updateDoorMesh(node: DoorNode, mesh: THREE.Mesh) {
|
||||
const hasLeafContent = segments.some((seg) => seg.type !== 'empty')
|
||||
const clampedSwingAngle = Math.max(0, Math.min(Math.PI / 2, swingAngle))
|
||||
|
||||
if (openingKind === 'opening') {
|
||||
syncDoorCutout(node, mesh)
|
||||
return
|
||||
}
|
||||
|
||||
// Leaf occupies the full opening (no bottom frame bar — door opens to floor)
|
||||
const leafW = width - 2 * frameThickness
|
||||
const leafH = height - frameThickness // only top frame
|
||||
@@ -306,6 +312,10 @@ function updateDoorMesh(node: DoorNode, mesh: THREE.Mesh) {
|
||||
addBox(mesh, baseMaterial, hingeW, hingeH, hingeD, hingeX, leafTop - 0.25, hingeZ)
|
||||
}
|
||||
|
||||
syncDoorCutout(node, mesh)
|
||||
}
|
||||
|
||||
function syncDoorCutout(node: DoorNode, mesh: THREE.Mesh) {
|
||||
// ── Cutout (for wall CSG) — always full door dimensions, 1m deep ──
|
||||
let cutout = mesh.getObjectByName('cutout') as THREE.Mesh | undefined
|
||||
if (!cutout) {
|
||||
|
||||
@@ -5,7 +5,7 @@ import { computeBoundsTree } from 'three-mesh-bvh'
|
||||
import { sceneRegistry } from '../../hooks/scene-registry/scene-registry'
|
||||
import { spatialGridManager } from '../../hooks/spatial-grid/spatial-grid-manager'
|
||||
import { resolveLevelId } from '../../hooks/spatial-grid/spatial-grid-sync'
|
||||
import type { AnyNode, AnyNodeId, WallNode } from '../../schema'
|
||||
import type { AnyNode, AnyNodeId, DoorNode, WallNode } from '../../schema'
|
||||
import useScene from '../../store/use-scene'
|
||||
import { getWallCurveFrameAt, getWallSurfacePolygon, isCurvedWall } from './wall-curve'
|
||||
import { DEFAULT_WALL_HEIGHT, getWallPlanFootprint, getWallThickness } from './wall-footprint'
|
||||
@@ -546,6 +546,11 @@ function collectCutoutBrushes(
|
||||
for (const child of childrenNodes) {
|
||||
if (child.type !== 'item' && child.type !== 'window' && child.type !== 'door') continue
|
||||
|
||||
if (child.type === 'door' && child.openingKind === 'opening') {
|
||||
brushes.push(createDoorOpeningCutoutBrush(child, wallThickness))
|
||||
continue
|
||||
}
|
||||
|
||||
const childMesh = sceneRegistry.nodes.get(child.id)
|
||||
if (!childMesh) continue
|
||||
|
||||
@@ -600,3 +605,76 @@ function collectCutoutBrushes(
|
||||
|
||||
return brushes
|
||||
}
|
||||
|
||||
function createDoorOpeningCutoutBrush(door: DoorNode, wallThickness: number): Brush {
|
||||
const shape = createDoorOpeningCutoutShape(door)
|
||||
const depth = wallThickness * 2
|
||||
const bevelSize =
|
||||
door.openingShape === 'rounded'
|
||||
? Math.min(
|
||||
Math.max(door.openingRevealRadius ?? 0.025, 0),
|
||||
Math.max(wallThickness * 0.45, 0.001),
|
||||
Math.max((door.cornerRadius ?? 0.15) * 0.45, 0.001),
|
||||
)
|
||||
: 0
|
||||
const geometry = new THREE.ExtrudeGeometry(shape, {
|
||||
depth,
|
||||
bevelEnabled: bevelSize > 0,
|
||||
bevelSegments: bevelSize > 0 ? 8 : 0,
|
||||
bevelSize,
|
||||
bevelThickness: bevelSize,
|
||||
curveSegments: 24,
|
||||
})
|
||||
|
||||
geometry.translate(0, 0, -depth / 2)
|
||||
geometry.computeBoundsTree = computeBoundsTree
|
||||
geometry.computeBoundsTree({ maxLeafSize: 10 })
|
||||
|
||||
return new Brush(geometry)
|
||||
}
|
||||
|
||||
function createDoorOpeningCutoutShape(door: DoorNode): THREE.Shape {
|
||||
const halfWidth = door.width / 2
|
||||
const bottom = door.position[1] - door.height / 2
|
||||
const top = door.position[1] + door.height / 2
|
||||
const centerX = door.position[0]
|
||||
const left = centerX - halfWidth
|
||||
const right = centerX + halfWidth
|
||||
const width = Math.max(door.width, 1e-6)
|
||||
const height = Math.max(door.height, 1e-6)
|
||||
const shape = new THREE.Shape()
|
||||
|
||||
if (door.openingShape === 'arch') {
|
||||
const archHeight = Math.min(Math.max(door.archHeight ?? width / 2, 0.01), height)
|
||||
const springY = top - archHeight
|
||||
|
||||
shape.moveTo(left, bottom)
|
||||
shape.lineTo(right, bottom)
|
||||
shape.lineTo(right, springY)
|
||||
shape.quadraticCurveTo(centerX, top, left, springY)
|
||||
shape.lineTo(left, bottom)
|
||||
shape.closePath()
|
||||
return shape
|
||||
}
|
||||
|
||||
if (door.openingShape === 'rounded') {
|
||||
const radius = Math.min(Math.max(door.cornerRadius ?? 0.15, 0), width / 2, height)
|
||||
|
||||
shape.moveTo(left, bottom)
|
||||
shape.lineTo(right, bottom)
|
||||
shape.lineTo(right, top - radius)
|
||||
shape.absarc(right - radius, top - radius, radius, 0, Math.PI / 2, false)
|
||||
shape.lineTo(left + radius, top)
|
||||
shape.absarc(left + radius, top - radius, radius, Math.PI / 2, Math.PI, false)
|
||||
shape.lineTo(left, bottom)
|
||||
shape.closePath()
|
||||
return shape
|
||||
}
|
||||
|
||||
shape.moveTo(left, bottom)
|
||||
shape.lineTo(right, bottom)
|
||||
shape.lineTo(right, top)
|
||||
shape.lineTo(left, top)
|
||||
shape.closePath()
|
||||
return shape
|
||||
}
|
||||
|
||||
@@ -45,6 +45,12 @@ function isMesh(object: THREE.Object3D): object is THREE.Mesh {
|
||||
return 'isMesh' in object && (object as THREE.Mesh).isMesh
|
||||
}
|
||||
|
||||
function isColliderMaterialVisible(material: THREE.Material | THREE.Material[]) {
|
||||
return Array.isArray(material)
|
||||
? material.some((entry) => entry.visible)
|
||||
: material.visible
|
||||
}
|
||||
|
||||
function cloneWorldGeometry(mesh: THREE.Mesh) {
|
||||
const sourceGeometry = mesh.geometry
|
||||
const position = sourceGeometry.getAttribute('position')
|
||||
@@ -79,6 +85,8 @@ function shouldSkipColliderNode(nodeId: string, type: (typeof COLLIDER_NODE_TYPE
|
||||
const node = useScene.getState().nodes[nodeId]
|
||||
if (!node || node.type !== 'door') return false
|
||||
|
||||
if (node.openingKind === 'opening') return true
|
||||
|
||||
if (!node.segments.length) return true
|
||||
|
||||
return node.segments.every((segment) => segment.type === 'empty')
|
||||
@@ -109,7 +117,12 @@ function collectColliderGeometriesFromNode(
|
||||
if (visitedMeshes.has(object)) return
|
||||
visitedMeshes.add(object)
|
||||
|
||||
if (isMesh(object) && object.visible && !SKIPPED_MESH_NAMES.has(object.name)) {
|
||||
if (
|
||||
isMesh(object) &&
|
||||
object.visible &&
|
||||
isColliderMaterialVisible(object.material) &&
|
||||
!SKIPPED_MESH_NAMES.has(object.name)
|
||||
) {
|
||||
const geometry = cloneWorldGeometry(object)
|
||||
if (geometry) {
|
||||
geometries.push(geometry)
|
||||
|
||||
@@ -4346,9 +4346,15 @@ const FloorplanGeometryLayer = memo(function FloorplanGeometryLayer({
|
||||
const doorCubeSize = Math.min(Math.max(width * 0.08, 0.06), 0.12)
|
||||
const doorCubeInset = doorCubeSize * 0.5
|
||||
const doorCubeStroke = palette.openingStroke
|
||||
const hingeCubeCenter = { x: hx + nx * doorCubeInset, y: hy + ny * doorCubeInset }
|
||||
const strikeCubeCenter = { x: ox2 - nx * doorCubeInset, y: oy2 - ny * doorCubeInset }
|
||||
const hingeTangentSign = hingesSide === 'left' ? 1 : -1
|
||||
const hingeCubeCenter = {
|
||||
x: hx + nx * hingeTangentSign * doorCubeInset,
|
||||
y: hy + ny * hingeTangentSign * doorCubeInset,
|
||||
}
|
||||
const strikeCubeCenter = {
|
||||
x: ox2 - nx * hingeTangentSign * doorCubeInset,
|
||||
y: oy2 - ny * hingeTangentSign * doorCubeInset,
|
||||
}
|
||||
const leafHalfThickness = doorCubeSize * 0.18
|
||||
const leafSideOffset = hingeTangentSign * (doorCubeSize / 2 + leafHalfThickness)
|
||||
const leafStart = {
|
||||
@@ -4377,7 +4383,7 @@ const FloorplanGeometryLayer = memo(function FloorplanGeometryLayer({
|
||||
x: leafStart.x + closedLeafVector.x * openCos - closedLeafVector.y * openSin,
|
||||
y: leafStart.y + closedLeafVector.x * openSin + closedLeafVector.y * openCos,
|
||||
}
|
||||
const doorBackgroundPoints = [
|
||||
const doorBackgroundPointList = [
|
||||
{
|
||||
x: svgP1.x - px * depthDirectionSign * depthExtraOffset,
|
||||
y: svgP1.y - py * depthDirectionSign * depthExtraOffset,
|
||||
@@ -4395,8 +4401,76 @@ const FloorplanGeometryLayer = memo(function FloorplanGeometryLayer({
|
||||
y: svgP4.y + py * depthDirectionSign * depthExtraOffset,
|
||||
},
|
||||
]
|
||||
const doorBackgroundPoints = doorBackgroundPointList
|
||||
.map((point) => `${point.x},${point.y}`)
|
||||
.join(' ')
|
||||
const openingPlanPath =
|
||||
opening.openingKind === 'opening' && opening.openingShape === 'rounded'
|
||||
? (() => {
|
||||
const [a, b, c, d] = doorBackgroundPointList
|
||||
if (!(a && b && c && d)) return null
|
||||
|
||||
const tangentRadius = Math.min(width * 0.14, doorCubeSize * 1.6)
|
||||
const depthRadius = Math.min(
|
||||
Math.hypot(svgP4.x - svgP1.x, svgP4.y - svgP1.y) * 0.42,
|
||||
doorCubeSize,
|
||||
)
|
||||
const radius = Math.min(tangentRadius, depthRadius)
|
||||
const offset = (from: Point2D, to: Point2D, distance: number) => {
|
||||
const dx = to.x - from.x
|
||||
const dy = to.y - from.y
|
||||
const length = Math.hypot(dx, dy)
|
||||
if (length < 1e-6) return from
|
||||
return {
|
||||
x: from.x + (dx / length) * Math.min(distance, length / 2),
|
||||
y: from.y + (dy / length) * Math.min(distance, length / 2),
|
||||
}
|
||||
}
|
||||
|
||||
const aToB = offset(a, b, radius)
|
||||
const bToA = offset(b, a, radius)
|
||||
const bToC = offset(b, c, radius)
|
||||
const cToB = offset(c, b, radius)
|
||||
const cToD = offset(c, d, radius)
|
||||
const dToC = offset(d, c, radius)
|
||||
const dToA = offset(d, a, radius)
|
||||
const aToD = offset(a, d, radius)
|
||||
|
||||
return [
|
||||
`M ${aToB.x} ${aToB.y}`,
|
||||
`L ${bToA.x} ${bToA.y}`,
|
||||
`Q ${b.x} ${b.y} ${bToC.x} ${bToC.y}`,
|
||||
`L ${cToB.x} ${cToB.y}`,
|
||||
`Q ${c.x} ${c.y} ${cToD.x} ${cToD.y}`,
|
||||
`L ${dToC.x} ${dToC.y}`,
|
||||
`Q ${d.x} ${d.y} ${dToA.x} ${dToA.y}`,
|
||||
`L ${aToD.x} ${aToD.y}`,
|
||||
`Q ${a.x} ${a.y} ${aToB.x} ${aToB.y}`,
|
||||
'Z',
|
||||
].join(' ')
|
||||
})()
|
||||
: null
|
||||
const archPlanPath =
|
||||
opening.openingKind === 'opening' && opening.openingShape === 'arch'
|
||||
? (() => {
|
||||
const centerStart = {
|
||||
x: (svgP1.x + svgP4.x) / 2,
|
||||
y: (svgP1.y + svgP4.y) / 2,
|
||||
}
|
||||
const centerEnd = {
|
||||
x: (svgP2.x + svgP3.x) / 2,
|
||||
y: (svgP2.y + svgP3.y) / 2,
|
||||
}
|
||||
const midpoint = {
|
||||
x: (centerStart.x + centerEnd.x) / 2,
|
||||
y: (centerStart.y + centerEnd.y) / 2,
|
||||
}
|
||||
const bow = Math.min(width * 0.18, doorCubeSize * 1.8)
|
||||
return `M ${centerStart.x} ${centerStart.y} Q ${midpoint.x + px * bow} ${
|
||||
midpoint.y + py * bow
|
||||
} ${centerEnd.x} ${centerEnd.y}`
|
||||
})()
|
||||
: null
|
||||
const leafPolygonPoints = [
|
||||
{
|
||||
x: leafStart.x - nx * leafHalfThickness,
|
||||
@@ -4467,6 +4541,37 @@ const FloorplanGeometryLayer = memo(function FloorplanGeometryLayer({
|
||||
vectorEffect="non-scaling-stroke"
|
||||
/>
|
||||
)}
|
||||
{opening.openingKind === 'opening' ? (
|
||||
<>
|
||||
{openingPlanPath ? (
|
||||
<path
|
||||
d={openingPlanPath}
|
||||
fill="#ffffff"
|
||||
stroke={isDeleteHovered ? palette.deleteStroke : doorCubeStroke}
|
||||
strokeWidth="1.25"
|
||||
vectorEffect="non-scaling-stroke"
|
||||
/>
|
||||
) : (
|
||||
<polygon
|
||||
fill="#ffffff"
|
||||
points={doorBackgroundPoints}
|
||||
stroke={isDeleteHovered ? palette.deleteStroke : doorCubeStroke}
|
||||
strokeWidth="1.25"
|
||||
vectorEffect="non-scaling-stroke"
|
||||
/>
|
||||
)}
|
||||
{archPlanPath && (
|
||||
<path
|
||||
d={archPlanPath}
|
||||
fill="none"
|
||||
stroke={isDeleteHovered ? palette.deleteStroke : doorCubeStroke}
|
||||
strokeWidth={arcStrokeWidth}
|
||||
vectorEffect="non-scaling-stroke"
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<polygon fill="#ffffff" points={doorBackgroundPoints} stroke="none" />
|
||||
{[hingeCubeCenter, strikeCubeCenter].map((point, index) => (
|
||||
<rect
|
||||
@@ -4495,6 +4600,8 @@ const FloorplanGeometryLayer = memo(function FloorplanGeometryLayer({
|
||||
strokeWidth={arcStrokeWidth}
|
||||
vectorEffect="non-scaling-stroke"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</g>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -85,9 +85,10 @@ export function DoorPanel() {
|
||||
}, [node, setMovingNode, setSelection])
|
||||
|
||||
const setSegmentHeightRatio = (segIdx: number, newVal: number) => {
|
||||
const numSegs = node?.segments.length
|
||||
const totalH = node?.segments.reduce((sum, s) => sum + s.heightRatio, 0)
|
||||
const normH = node?.segments.map((s) => s.heightRatio / totalH)
|
||||
if (!node) return
|
||||
const numSegs = node.segments.length
|
||||
const totalH = node.segments.reduce((sum, s) => sum + s.heightRatio, 0)
|
||||
const normH = node.segments.map((s) => s.heightRatio / totalH)
|
||||
const clamped = Math.max(0.05, Math.min(0.95, newVal))
|
||||
const neighborIdx = segIdx < numSegs - 1 ? segIdx + 1 : segIdx - 1
|
||||
const delta = clamped - normH[segIdx]!
|
||||
@@ -97,7 +98,7 @@ export function DoorPanel() {
|
||||
if (i === neighborIdx) return neighborVal
|
||||
return v
|
||||
})
|
||||
const updated = node?.segments.map((s, idx) => ({ ...s, heightRatio: newRatios[idx]! }))
|
||||
const updated = node.segments.map((s, idx) => ({ ...s, heightRatio: newRatios[idx]! }))
|
||||
handleUpdate({ segments: updated })
|
||||
}
|
||||
|
||||
@@ -131,6 +132,11 @@ export function DoorPanel() {
|
||||
height: node.height,
|
||||
frameThickness: node.frameThickness,
|
||||
frameDepth: node.frameDepth,
|
||||
openingKind: node.openingKind,
|
||||
openingShape: node.openingShape,
|
||||
cornerRadius: node.cornerRadius,
|
||||
archHeight: node.archHeight,
|
||||
openingRevealRadius: node.openingRevealRadius,
|
||||
contentPadding: node.contentPadding,
|
||||
hingesSide: node.hingesSide,
|
||||
swingDirection: node.swingDirection,
|
||||
@@ -177,6 +183,11 @@ export function DoorPanel() {
|
||||
|
||||
const hSum = node.segments.reduce((s, seg) => s + seg.heightRatio, 0)
|
||||
const normHeights = node.segments.map((seg) => seg.heightRatio / hSum)
|
||||
const isOpening = node.openingKind === 'opening'
|
||||
const openingShape = node.openingShape ?? 'rectangle'
|
||||
const cornerRadius = node.cornerRadius ?? 0.15
|
||||
const archHeight = node.archHeight ?? 0.45
|
||||
const openingRevealRadius = node.openingRevealRadius ?? 0.025
|
||||
|
||||
return (
|
||||
<PanelWrapper
|
||||
@@ -206,6 +217,31 @@ export function DoorPanel() {
|
||||
</PresetsPopover>
|
||||
</div>
|
||||
|
||||
<PanelSection title="Type">
|
||||
<div className="flex flex-col gap-2 px-1 pb-1">
|
||||
<SegmentedControl
|
||||
onChange={(v) =>
|
||||
handleUpdate(
|
||||
v === 'opening'
|
||||
? {
|
||||
openingKind: v,
|
||||
openingShape,
|
||||
cornerRadius,
|
||||
archHeight,
|
||||
openingRevealRadius,
|
||||
}
|
||||
: { openingKind: v },
|
||||
)
|
||||
}
|
||||
options={[
|
||||
{ label: 'Door', value: 'door' },
|
||||
{ label: 'Opening', value: 'opening' },
|
||||
]}
|
||||
value={node.openingKind}
|
||||
/>
|
||||
</div>
|
||||
</PanelSection>
|
||||
|
||||
<PanelSection title="Position">
|
||||
<SliderControl
|
||||
label={
|
||||
@@ -221,6 +257,7 @@ export function DoorPanel() {
|
||||
unit="m"
|
||||
value={Math.round(node.position[0] * 100) / 100}
|
||||
/>
|
||||
{!isOpening && (
|
||||
<div className="px-1 pt-2 pb-1">
|
||||
<ActionButton
|
||||
className="w-full"
|
||||
@@ -229,6 +266,7 @@ export function DoorPanel() {
|
||||
onClick={handleFlip}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</PanelSection>
|
||||
|
||||
<PanelSection title="Dimensions">
|
||||
@@ -256,6 +294,66 @@ export function DoorPanel() {
|
||||
/>
|
||||
</PanelSection>
|
||||
|
||||
{isOpening && (
|
||||
<PanelSection title="Opening Shape">
|
||||
<div className="flex flex-col gap-2 px-1 pb-1">
|
||||
<SegmentedControl
|
||||
onChange={(v) =>
|
||||
handleUpdate({
|
||||
openingShape: v,
|
||||
...(v === 'rounded' ? { cornerRadius, openingRevealRadius } : {}),
|
||||
...(v === 'arch' ? { archHeight } : {}),
|
||||
})
|
||||
}
|
||||
options={[
|
||||
{ label: 'Rect', value: 'rectangle' },
|
||||
{ label: 'Rounded', value: 'rounded' },
|
||||
{ label: 'Arch', value: 'arch' },
|
||||
]}
|
||||
value={openingShape}
|
||||
/>
|
||||
</div>
|
||||
{openingShape === 'rounded' && (
|
||||
<>
|
||||
<SliderControl
|
||||
label="Corner Radius"
|
||||
max={Math.min(node.width / 2, node.height)}
|
||||
min={0}
|
||||
onChange={(v) => handleUpdate({ cornerRadius: v })}
|
||||
precision={2}
|
||||
step={0.05}
|
||||
unit="m"
|
||||
value={Math.round(cornerRadius * 100) / 100}
|
||||
/>
|
||||
<SliderControl
|
||||
label="Reveal Radius"
|
||||
max={0.08}
|
||||
min={0}
|
||||
onChange={(v) => handleUpdate({ openingRevealRadius: v })}
|
||||
precision={3}
|
||||
step={0.005}
|
||||
unit="m"
|
||||
value={Math.round(openingRevealRadius * 1000) / 1000}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{openingShape === 'arch' && (
|
||||
<SliderControl
|
||||
label="Arch Height"
|
||||
max={node.height}
|
||||
min={0.05}
|
||||
onChange={(v) => handleUpdate({ archHeight: v })}
|
||||
precision={2}
|
||||
step={0.05}
|
||||
unit="m"
|
||||
value={Math.round(archHeight * 100) / 100}
|
||||
/>
|
||||
)}
|
||||
</PanelSection>
|
||||
)}
|
||||
|
||||
{!isOpening && (
|
||||
<>
|
||||
<PanelSection title="Frame">
|
||||
<SliderControl
|
||||
label="Thickness"
|
||||
@@ -567,6 +665,9 @@ export function DoorPanel() {
|
||||
</div>
|
||||
</PanelSection>
|
||||
|
||||
</>
|
||||
)}
|
||||
|
||||
<PanelSection title="Actions">
|
||||
<ActionGroup>
|
||||
<ActionButton icon={<Move className="h-3.5 w-3.5" />} label="Move" onClick={handleMove} />
|
||||
|
||||
@@ -153,12 +153,14 @@ export const useKeyboard = ({
|
||||
const node = useScene.getState().nodes[selectedNodeIds[0]!]
|
||||
if (node?.type === 'door') {
|
||||
e.preventDefault()
|
||||
if (node.openingKind !== 'opening') {
|
||||
const currentSwingAngle = node.swingAngle ?? 0
|
||||
useScene.getState().updateNode(node.id, {
|
||||
swingAngle:
|
||||
currentSwingAngle >= DOOR_SWING_OPEN_ANGLE / 2 ? 0 : DOOR_SWING_OPEN_ANGLE,
|
||||
})
|
||||
sfxEmitter.emit('sfx:item-rotate')
|
||||
}
|
||||
} else if (node && 'rotation' in node) {
|
||||
e.preventDefault()
|
||||
const ROTATION_STEP = Math.PI / 4
|
||||
@@ -181,8 +183,10 @@ export const useKeyboard = ({
|
||||
const node = useScene.getState().nodes[selectedNodeIds[0]!]
|
||||
if (node?.type === 'door') {
|
||||
e.preventDefault()
|
||||
if (node.openingKind !== 'opening') {
|
||||
useScene.getState().updateNode(node.id, { swingAngle: 0 })
|
||||
sfxEmitter.emit('sfx:item-rotate')
|
||||
}
|
||||
} else if (node && 'rotation' in node) {
|
||||
e.preventDefault()
|
||||
const ROTATION_STEP = Math.PI / 4
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { type DoorNode, useRegistry, useScene } from '@pascal-app/core'
|
||||
import { useLayoutEffect, useMemo, useRef } from 'react'
|
||||
import type { Mesh } from 'three'
|
||||
import { useLayoutEffect, useRef } from 'react'
|
||||
import { MeshBasicMaterial, type Mesh } from 'three'
|
||||
import { useNodeEvents } from '../../../hooks/use-node-events'
|
||||
import { createMaterial, DEFAULT_DOOR_MATERIAL } from '../../../lib/materials'
|
||||
|
||||
const doorHitboxMaterial = new MeshBasicMaterial({ visible: false })
|
||||
|
||||
export const DoorRenderer = ({ node }: { node: DoorNode }) => {
|
||||
const ref = useRef<Mesh>(null!)
|
||||
@@ -14,16 +15,10 @@ export const DoorRenderer = ({ node }: { node: DoorNode }) => {
|
||||
const handlers = useNodeEvents(node, 'door')
|
||||
const isTransient = !!(node.metadata as Record<string, unknown> | null)?.isTransient
|
||||
|
||||
const material = useMemo(() => {
|
||||
const mat = node.material
|
||||
if (!mat) return DEFAULT_DOOR_MATERIAL
|
||||
return createMaterial(mat)
|
||||
}, [node.material, node.material?.preset, node.material?.properties, node.material?.texture])
|
||||
|
||||
return (
|
||||
<mesh
|
||||
castShadow
|
||||
material={material}
|
||||
material={doorHitboxMaterial}
|
||||
position={node.position}
|
||||
receiveShadow
|
||||
ref={ref}
|
||||
|
||||
Reference in New Issue
Block a user