Fix curved wall and fence angle measurements
This commit is contained in:
@@ -1,10 +1,5 @@
|
||||
import { type AnyNodeId, sceneRegistry, useScene, type WindowNode } from '@pascal-app/core'
|
||||
import { useFrame } from '@react-three/fiber'
|
||||
import {
|
||||
type AnyNodeId,
|
||||
sceneRegistry,
|
||||
useScene,
|
||||
type WindowNode,
|
||||
} from '@pascal-app/core'
|
||||
import * as THREE from 'three'
|
||||
import { baseMaterial, glassMaterial } from '../../lib/materials'
|
||||
|
||||
@@ -55,6 +50,521 @@ function addBox(
|
||||
parent.add(m)
|
||||
}
|
||||
|
||||
function addShape(
|
||||
parent: THREE.Object3D,
|
||||
material: THREE.Material,
|
||||
shape: THREE.Shape,
|
||||
depth: number,
|
||||
z = 0,
|
||||
) {
|
||||
const geometry = new THREE.ExtrudeGeometry(shape, {
|
||||
depth,
|
||||
bevelEnabled: false,
|
||||
curveSegments: 24,
|
||||
})
|
||||
geometry.translate(0, 0, -depth / 2 + z)
|
||||
const mesh = new THREE.Mesh(geometry, material)
|
||||
parent.add(mesh)
|
||||
}
|
||||
|
||||
function createRectShape(left: number, right: number, bottom: number, top: number) {
|
||||
const shape = new THREE.Shape()
|
||||
shape.moveTo(left, bottom)
|
||||
shape.lineTo(right, bottom)
|
||||
shape.lineTo(right, top)
|
||||
shape.lineTo(left, top)
|
||||
shape.closePath()
|
||||
return shape
|
||||
}
|
||||
|
||||
type CornerRadii = {
|
||||
topLeft: number
|
||||
topRight: number
|
||||
bottomRight: number
|
||||
bottomLeft: number
|
||||
}
|
||||
|
||||
function normalizeCornerRadii(radii: CornerRadii, width: number, height: number): CornerRadii {
|
||||
const next = { ...radii }
|
||||
const scale = Math.min(
|
||||
1,
|
||||
width / Math.max(next.topLeft + next.topRight, 1e-6),
|
||||
width / Math.max(next.bottomLeft + next.bottomRight, 1e-6),
|
||||
height / Math.max(next.topLeft + next.bottomLeft, 1e-6),
|
||||
height / Math.max(next.topRight + next.bottomRight, 1e-6),
|
||||
)
|
||||
|
||||
if (scale < 1) {
|
||||
next.topLeft *= scale
|
||||
next.topRight *= scale
|
||||
next.bottomRight *= scale
|
||||
next.bottomLeft *= scale
|
||||
}
|
||||
|
||||
return next
|
||||
}
|
||||
|
||||
function getWindowRoundedRadii(node: WindowNode, width: number, height: number): CornerRadii {
|
||||
if (node.openingRadiusMode === 'individual') {
|
||||
const [topLeft = 0, topRight = 0, bottomRight = 0, bottomLeft = 0] =
|
||||
node.openingCornerRadii ?? [0.15, 0.15, 0.15, 0.15]
|
||||
return normalizeCornerRadii(
|
||||
{
|
||||
topLeft: Math.max(topLeft, 0),
|
||||
topRight: Math.max(topRight, 0),
|
||||
bottomRight: Math.max(bottomRight, 0),
|
||||
bottomLeft: Math.max(bottomLeft, 0),
|
||||
},
|
||||
width,
|
||||
height,
|
||||
)
|
||||
}
|
||||
|
||||
const maxRadius = Math.min(width / 2, height / 2)
|
||||
const radius = Math.min(Math.max(node.cornerRadius ?? 0.15, 0), maxRadius)
|
||||
return { topLeft: radius, topRight: radius, bottomRight: radius, bottomLeft: radius }
|
||||
}
|
||||
|
||||
function insetCornerRadii(radii: CornerRadii, inset: number, width: number, height: number) {
|
||||
return normalizeCornerRadii(
|
||||
{
|
||||
topLeft: Math.max(radii.topLeft - inset, 0),
|
||||
topRight: Math.max(radii.topRight - inset, 0),
|
||||
bottomRight: Math.max(radii.bottomRight - inset, 0),
|
||||
bottomLeft: Math.max(radii.bottomLeft - inset, 0),
|
||||
},
|
||||
width,
|
||||
height,
|
||||
)
|
||||
}
|
||||
|
||||
function createRoundedShape(
|
||||
left: number,
|
||||
right: number,
|
||||
bottom: number,
|
||||
top: number,
|
||||
radii: CornerRadii,
|
||||
) {
|
||||
const shape = new THREE.Shape()
|
||||
const { topLeft, topRight, bottomRight, bottomLeft } = radii
|
||||
|
||||
shape.moveTo(left + bottomLeft, bottom)
|
||||
shape.lineTo(right - bottomRight, bottom)
|
||||
if (bottomRight > 1e-6) {
|
||||
shape.absarc(right - bottomRight, bottom + bottomRight, bottomRight, -Math.PI / 2, 0, false)
|
||||
} else {
|
||||
shape.lineTo(right, bottom)
|
||||
}
|
||||
|
||||
shape.lineTo(right, top - topRight)
|
||||
if (topRight > 1e-6) {
|
||||
shape.absarc(right - topRight, top - topRight, topRight, 0, Math.PI / 2, false)
|
||||
} else {
|
||||
shape.lineTo(right, top)
|
||||
}
|
||||
|
||||
shape.lineTo(left + topLeft, top)
|
||||
if (topLeft > 1e-6) {
|
||||
shape.absarc(left + topLeft, top - topLeft, topLeft, Math.PI / 2, Math.PI, false)
|
||||
} else {
|
||||
shape.lineTo(left, top)
|
||||
}
|
||||
|
||||
shape.lineTo(left, bottom + bottomLeft)
|
||||
if (bottomLeft > 1e-6) {
|
||||
shape.absarc(left + bottomLeft, bottom + bottomLeft, bottomLeft, Math.PI, Math.PI * 1.5, false)
|
||||
} else {
|
||||
shape.lineTo(left, bottom)
|
||||
}
|
||||
|
||||
shape.closePath()
|
||||
return shape
|
||||
}
|
||||
|
||||
function createRoundedFrameShape(
|
||||
width: number,
|
||||
height: number,
|
||||
frameThickness: number,
|
||||
outerRadii: CornerRadii,
|
||||
) {
|
||||
const halfWidth = width / 2
|
||||
const bottom = -height / 2
|
||||
const top = height / 2
|
||||
const outer = createRoundedShape(-halfWidth, halfWidth, bottom, top, outerRadii)
|
||||
const inset = Math.min(frameThickness, width / 2 - 0.005, height / 2 - 0.005)
|
||||
|
||||
if (inset <= 0.001) return outer
|
||||
|
||||
const innerLeft = -halfWidth + inset
|
||||
const innerRight = halfWidth - inset
|
||||
const innerBottom = bottom + inset
|
||||
const innerTop = top - inset
|
||||
const innerRadii = insetCornerRadii(
|
||||
outerRadii,
|
||||
inset,
|
||||
innerRight - innerLeft,
|
||||
innerTop - innerBottom,
|
||||
)
|
||||
const holeShape = createRoundedShape(innerLeft, innerRight, innerBottom, innerTop, innerRadii)
|
||||
const hole = new THREE.Path(holeShape.getPoints(32).reverse())
|
||||
outer.holes.push(hole)
|
||||
|
||||
return outer
|
||||
}
|
||||
|
||||
function getClampedArchHeight(width: number, height: number, archHeight: number | undefined) {
|
||||
return Math.min(Math.max(archHeight ?? width / 2, 0.01), Math.max(height, 0.01))
|
||||
}
|
||||
|
||||
function createArchShape(
|
||||
left: number,
|
||||
right: number,
|
||||
bottom: number,
|
||||
top: number,
|
||||
archHeight: number,
|
||||
) {
|
||||
const centerX = (left + right) / 2
|
||||
const halfWidth = (right - left) / 2
|
||||
const clampedArchHeight = getClampedArchHeight(right - left, top - bottom, archHeight)
|
||||
const springY = top - clampedArchHeight
|
||||
const shape = new THREE.Shape()
|
||||
const segments = 32
|
||||
|
||||
shape.moveTo(left, bottom)
|
||||
shape.lineTo(right, bottom)
|
||||
shape.lineTo(right, springY)
|
||||
for (let index = 1; index <= segments; index += 1) {
|
||||
const x = right + (left - right) * (index / segments)
|
||||
shape.lineTo(x, getArchBoundaryY(x - centerX, halfWidth, springY, clampedArchHeight))
|
||||
}
|
||||
shape.lineTo(left, bottom)
|
||||
shape.closePath()
|
||||
return shape
|
||||
}
|
||||
|
||||
function createArchedFrameShape(
|
||||
width: number,
|
||||
height: number,
|
||||
archHeight: number,
|
||||
frameThickness: number,
|
||||
) {
|
||||
const halfWidth = width / 2
|
||||
const bottom = -height / 2
|
||||
const top = height / 2
|
||||
const outer = createArchShape(-halfWidth, halfWidth, bottom, top, archHeight)
|
||||
const inset = Math.min(frameThickness, width / 2 - 0.005, height / 2 - 0.005)
|
||||
|
||||
if (inset <= 0.001) return outer
|
||||
|
||||
const innerLeft = -halfWidth + inset
|
||||
const innerRight = halfWidth - inset
|
||||
const innerBottom = bottom + inset
|
||||
const innerTop = top - inset
|
||||
const innerArchHeight = getClampedArchHeight(
|
||||
innerRight - innerLeft,
|
||||
innerTop - innerBottom,
|
||||
archHeight - inset,
|
||||
)
|
||||
const hole = new THREE.Path(
|
||||
createArchShape(innerLeft, innerRight, innerBottom, innerTop, innerArchHeight)
|
||||
.getPoints(32)
|
||||
.reverse(),
|
||||
)
|
||||
outer.holes.push(hole)
|
||||
|
||||
return outer
|
||||
}
|
||||
|
||||
function getArchBoundaryY(x: number, halfWidth: number, springY: number, archHeight: number) {
|
||||
if (halfWidth <= 1e-6) return springY
|
||||
const t = Math.min(Math.abs(x) / halfWidth, 1)
|
||||
return springY + archHeight * Math.sqrt(Math.max(1 - t * t, 0))
|
||||
}
|
||||
|
||||
function getArchedOpeningHalfWidthAtY(
|
||||
y: number,
|
||||
halfWidth: number,
|
||||
springY: number,
|
||||
archHeight: number,
|
||||
) {
|
||||
if (y <= springY || archHeight <= 1e-6) return halfWidth
|
||||
const normalizedY = Math.min(Math.max((y - springY) / archHeight, 0), 1)
|
||||
return halfWidth * Math.sqrt(Math.max(1 - normalizedY * normalizedY, 0))
|
||||
}
|
||||
|
||||
function getRoundedBoundaryYAtX(
|
||||
x: number,
|
||||
left: number,
|
||||
right: number,
|
||||
top: number,
|
||||
radii: CornerRadii,
|
||||
) {
|
||||
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 getRoundedHorizontalBoundsAtY(
|
||||
y: number,
|
||||
left: number,
|
||||
right: number,
|
||||
top: number,
|
||||
radii: CornerRadii,
|
||||
) {
|
||||
let minX = left
|
||||
let maxX = right
|
||||
|
||||
if (radii.topLeft > 1e-6 && y > top - radii.topLeft) {
|
||||
const centerX = left + radii.topLeft
|
||||
const centerY = top - radii.topLeft
|
||||
const dy = y - centerY
|
||||
minX = centerX - Math.sqrt(Math.max(radii.topLeft * radii.topLeft - dy * dy, 0))
|
||||
}
|
||||
|
||||
if (radii.topRight > 1e-6 && y > top - radii.topRight) {
|
||||
const centerX = right - radii.topRight
|
||||
const centerY = top - radii.topRight
|
||||
const dy = y - centerY
|
||||
maxX = centerX + Math.sqrt(Math.max(radii.topRight * radii.topRight - dy * dy, 0))
|
||||
}
|
||||
|
||||
return { minX, maxX }
|
||||
}
|
||||
|
||||
function addRoundedWindowVisuals(node: WindowNode, mesh: THREE.Mesh) {
|
||||
const {
|
||||
width,
|
||||
height,
|
||||
frameDepth,
|
||||
frameThickness,
|
||||
columnRatios,
|
||||
rowRatios,
|
||||
columnDividerThickness,
|
||||
rowDividerThickness,
|
||||
sill,
|
||||
sillDepth,
|
||||
sillThickness,
|
||||
} = node
|
||||
const halfWidth = width / 2
|
||||
const bottom = -height / 2
|
||||
const top = height / 2
|
||||
const outerRadii = getWindowRoundedRadii(node, width, height)
|
||||
const inset = Math.max(0, Math.min(frameThickness, width / 2 - 0.005, height / 2 - 0.005))
|
||||
const innerLeft = -halfWidth + inset
|
||||
const innerRight = halfWidth - inset
|
||||
const innerBottom = bottom + inset
|
||||
const innerTop = top - inset
|
||||
const innerW = innerRight - innerLeft
|
||||
const innerH = innerTop - innerBottom
|
||||
const innerRadii = insetCornerRadii(outerRadii, inset, innerW, innerH)
|
||||
|
||||
addShape(
|
||||
mesh,
|
||||
baseMaterial,
|
||||
createRoundedFrameShape(width, height, inset, outerRadii),
|
||||
frameDepth,
|
||||
)
|
||||
|
||||
if (innerW > 0.01 && innerH > 0.01) {
|
||||
const glassDepth = Math.max(0.004, frameDepth * 0.08)
|
||||
addShape(
|
||||
mesh,
|
||||
glassMaterial,
|
||||
createRoundedShape(innerLeft, innerRight, innerBottom, innerTop, innerRadii),
|
||||
glassDepth,
|
||||
)
|
||||
|
||||
const numCols = columnRatios.length
|
||||
const numRows = rowRatios.length
|
||||
const usableW = innerW - (numCols - 1) * columnDividerThickness
|
||||
const usableH = innerH - (numRows - 1) * rowDividerThickness
|
||||
const colSum = columnRatios.reduce((a, b) => a + b, 0)
|
||||
const rowSum = rowRatios.reduce((a, b) => a + b, 0)
|
||||
const colWidths = columnRatios.map((r) => (r / colSum) * usableW)
|
||||
const rowHeights = rowRatios.map((r) => (r / rowSum) * usableH)
|
||||
|
||||
let x = innerLeft
|
||||
for (let c = 0; c < numCols - 1; c++) {
|
||||
x += colWidths[c]!
|
||||
const x1 = x
|
||||
const x2 = x + columnDividerThickness
|
||||
const dividerTop = Math.min(
|
||||
getRoundedBoundaryYAtX(x1, innerLeft, innerRight, innerTop, innerRadii),
|
||||
getRoundedBoundaryYAtX(x2, innerLeft, innerRight, innerTop, innerRadii),
|
||||
)
|
||||
if (dividerTop > innerBottom + 0.01) {
|
||||
addShape(
|
||||
mesh,
|
||||
baseMaterial,
|
||||
createRectShape(x1, x2, innerBottom, dividerTop),
|
||||
frameDepth + 0.001,
|
||||
)
|
||||
}
|
||||
x += columnDividerThickness
|
||||
}
|
||||
|
||||
let y = innerTop
|
||||
for (let r = 0; r < numRows - 1; r++) {
|
||||
y -= rowHeights[r]!
|
||||
const yTop = y
|
||||
const yBottom = y - rowDividerThickness
|
||||
const { minX, maxX } = getRoundedHorizontalBoundsAtY(
|
||||
yTop,
|
||||
innerLeft,
|
||||
innerRight,
|
||||
innerTop,
|
||||
innerRadii,
|
||||
)
|
||||
if (maxX - minX > 0.01 && yTop > innerBottom) {
|
||||
addShape(
|
||||
mesh,
|
||||
baseMaterial,
|
||||
createRectShape(minX, maxX, Math.max(yBottom, innerBottom), yTop),
|
||||
frameDepth + 0.001,
|
||||
)
|
||||
}
|
||||
y -= rowDividerThickness
|
||||
}
|
||||
}
|
||||
|
||||
if (sill) {
|
||||
const sillW = width + sillDepth * 0.4
|
||||
const sillZ = frameDepth / 2 + sillDepth / 2
|
||||
addBox(
|
||||
mesh,
|
||||
baseMaterial,
|
||||
sillW,
|
||||
sillThickness,
|
||||
sillDepth,
|
||||
0,
|
||||
-height / 2 - sillThickness / 2,
|
||||
sillZ,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function addArchedWindowVisuals(node: WindowNode, mesh: THREE.Mesh) {
|
||||
const {
|
||||
width,
|
||||
height,
|
||||
frameDepth,
|
||||
frameThickness,
|
||||
columnRatios,
|
||||
rowRatios,
|
||||
columnDividerThickness,
|
||||
rowDividerThickness,
|
||||
sill,
|
||||
sillDepth,
|
||||
sillThickness,
|
||||
} = node
|
||||
const halfWidth = width / 2
|
||||
const bottom = -height / 2
|
||||
const top = height / 2
|
||||
const archHeight = getClampedArchHeight(width, height, node.archHeight)
|
||||
const inset = Math.max(0, Math.min(frameThickness, width / 2 - 0.005, height / 2 - 0.005))
|
||||
const innerLeft = -halfWidth + inset
|
||||
const innerRight = halfWidth - inset
|
||||
const innerBottom = bottom + inset
|
||||
const innerTop = top - inset
|
||||
const innerW = innerRight - innerLeft
|
||||
const innerH = innerTop - innerBottom
|
||||
const innerArchHeight = getClampedArchHeight(innerW, innerH, archHeight - inset)
|
||||
const innerSpringY = innerTop - innerArchHeight
|
||||
|
||||
addShape(mesh, baseMaterial, createArchedFrameShape(width, height, archHeight, inset), frameDepth)
|
||||
|
||||
if (innerW > 0.01 && innerH > 0.01) {
|
||||
const glassDepth = Math.max(0.004, frameDepth * 0.08)
|
||||
addShape(
|
||||
mesh,
|
||||
glassMaterial,
|
||||
createArchShape(innerLeft, innerRight, innerBottom, innerTop, innerArchHeight),
|
||||
glassDepth,
|
||||
)
|
||||
|
||||
const numCols = columnRatios.length
|
||||
const numRows = rowRatios.length
|
||||
const usableW = innerW - (numCols - 1) * columnDividerThickness
|
||||
const usableH = innerH - (numRows - 1) * rowDividerThickness
|
||||
const colSum = columnRatios.reduce((a, b) => a + b, 0)
|
||||
const rowSum = rowRatios.reduce((a, b) => a + b, 0)
|
||||
const colWidths = columnRatios.map((r) => (r / colSum) * usableW)
|
||||
const rowHeights = rowRatios.map((r) => (r / rowSum) * usableH)
|
||||
const innerHalfWidth = innerW / 2
|
||||
|
||||
let x = innerLeft
|
||||
for (let c = 0; c < numCols - 1; c++) {
|
||||
x += colWidths[c]!
|
||||
const x1 = x
|
||||
const x2 = x + columnDividerThickness
|
||||
const dividerTop = Math.min(
|
||||
getArchBoundaryY(x1, innerHalfWidth, innerSpringY, innerArchHeight),
|
||||
getArchBoundaryY(x2, innerHalfWidth, innerSpringY, innerArchHeight),
|
||||
)
|
||||
if (dividerTop > innerBottom + 0.01) {
|
||||
addShape(
|
||||
mesh,
|
||||
baseMaterial,
|
||||
createRectShape(x1, x2, innerBottom, dividerTop),
|
||||
frameDepth + 0.001,
|
||||
)
|
||||
}
|
||||
x += columnDividerThickness
|
||||
}
|
||||
|
||||
let y = innerTop
|
||||
for (let r = 0; r < numRows - 1; r++) {
|
||||
y -= rowHeights[r]!
|
||||
const yTop = y
|
||||
const yBottom = y - rowDividerThickness
|
||||
const halfAtTop = getArchedOpeningHalfWidthAtY(
|
||||
yTop,
|
||||
innerHalfWidth,
|
||||
innerSpringY,
|
||||
innerArchHeight,
|
||||
)
|
||||
const x1 = -halfAtTop
|
||||
const x2 = halfAtTop
|
||||
if (x2 - x1 > 0.01 && yTop > innerBottom) {
|
||||
addShape(
|
||||
mesh,
|
||||
baseMaterial,
|
||||
createRectShape(x1, x2, Math.max(yBottom, innerBottom), yTop),
|
||||
frameDepth + 0.001,
|
||||
)
|
||||
}
|
||||
y -= rowDividerThickness
|
||||
}
|
||||
}
|
||||
|
||||
if (sill) {
|
||||
const sillW = width + sillDepth * 0.4
|
||||
const sillZ = frameDepth / 2 + sillDepth / 2
|
||||
addBox(
|
||||
mesh,
|
||||
baseMaterial,
|
||||
sillW,
|
||||
sillThickness,
|
||||
sillDepth,
|
||||
0,
|
||||
-height / 2 - sillThickness / 2,
|
||||
sillZ,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function updateWindowMesh(node: WindowNode, mesh: THREE.Mesh) {
|
||||
// Root mesh is an invisible hitbox; all visuals live in child meshes
|
||||
mesh.geometry.dispose()
|
||||
@@ -85,6 +595,7 @@ function updateWindowMesh(node: WindowNode, mesh: THREE.Mesh) {
|
||||
sillDepth,
|
||||
sillThickness,
|
||||
openingKind,
|
||||
openingShape,
|
||||
} = node
|
||||
|
||||
if (openingKind === 'opening') {
|
||||
@@ -92,6 +603,18 @@ function updateWindowMesh(node: WindowNode, mesh: THREE.Mesh) {
|
||||
return
|
||||
}
|
||||
|
||||
if (openingShape === 'arch') {
|
||||
addArchedWindowVisuals(node, mesh)
|
||||
syncWindowCutout(node, mesh)
|
||||
return
|
||||
}
|
||||
|
||||
if (openingShape === 'rounded') {
|
||||
addRoundedWindowVisuals(node, mesh)
|
||||
syncWindowCutout(node, mesh)
|
||||
return
|
||||
}
|
||||
|
||||
const innerW = width - 2 * frameThickness
|
||||
const innerH = height - 2 * frameThickness
|
||||
|
||||
@@ -252,6 +775,40 @@ function syncWindowCutout(node: WindowNode, mesh: THREE.Mesh) {
|
||||
mesh.add(cutout)
|
||||
}
|
||||
cutout.geometry.dispose()
|
||||
cutout.geometry = new THREE.BoxGeometry(node.width, node.height, 1.0)
|
||||
if (node.openingShape === 'arch') {
|
||||
cutout.geometry = new THREE.ExtrudeGeometry(
|
||||
createArchShape(
|
||||
-node.width / 2,
|
||||
node.width / 2,
|
||||
-node.height / 2,
|
||||
node.height / 2,
|
||||
getClampedArchHeight(node.width, node.height, node.archHeight),
|
||||
),
|
||||
{
|
||||
depth: 1,
|
||||
bevelEnabled: false,
|
||||
curveSegments: 24,
|
||||
},
|
||||
)
|
||||
cutout.geometry.translate(0, 0, -0.5)
|
||||
} else if (node.openingShape === 'rounded') {
|
||||
cutout.geometry = new THREE.ExtrudeGeometry(
|
||||
createRoundedShape(
|
||||
-node.width / 2,
|
||||
node.width / 2,
|
||||
-node.height / 2,
|
||||
node.height / 2,
|
||||
getWindowRoundedRadii(node, node.width, node.height),
|
||||
),
|
||||
{
|
||||
depth: 1,
|
||||
bevelEnabled: false,
|
||||
curveSegments: 24,
|
||||
},
|
||||
)
|
||||
cutout.geometry.translate(0, 0, -0.5)
|
||||
} else {
|
||||
cutout.geometry = new THREE.BoxGeometry(node.width, node.height, 1.0)
|
||||
}
|
||||
cutout.visible = false
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user