Enable pillar duplication in floating action menu
This commit is contained in:
@@ -1,18 +1,198 @@
|
||||
import { type ColumnNode, useLiveTransforms, useRegistry } from '@pascal-app/core'
|
||||
import { createContext, useContext, useMemo, useRef } from 'react'
|
||||
import type { Group, Material } from 'three'
|
||||
import {
|
||||
BoxGeometry,
|
||||
type BufferGeometry,
|
||||
CylinderGeometry,
|
||||
Float32BufferAttribute,
|
||||
type Group,
|
||||
type Material,
|
||||
SphereGeometry,
|
||||
TorusGeometry,
|
||||
} from 'three'
|
||||
import { RoundedBoxGeometry } from 'three/examples/jsm/geometries/RoundedBoxGeometry.js'
|
||||
import { useNodeEvents } from '../../../hooks/use-node-events'
|
||||
import { baseMaterial, createMaterial, createMaterialFromPresetRef } from '../../../lib/materials'
|
||||
|
||||
const ColumnMaterialContext = createContext<Material>(baseMaterial as Material)
|
||||
const ColumnEdgeSoftnessContext = createContext(0.025)
|
||||
const COLUMN_UV_SCALE = 1
|
||||
|
||||
function ColumnMaterial() {
|
||||
const material = useContext(ColumnMaterialContext)
|
||||
return <primitive attach="material" object={material} />
|
||||
}
|
||||
|
||||
function setUvAttributes(geometry: BufferGeometry, uvs: number[]) {
|
||||
geometry.setAttribute('uv', new Float32BufferAttribute(uvs, 2))
|
||||
geometry.setAttribute('uv2', new Float32BufferAttribute(uvs.slice(), 2))
|
||||
return geometry
|
||||
}
|
||||
|
||||
function toUvReadyGeometry(geometry: BufferGeometry) {
|
||||
return geometry.index ? geometry.toNonIndexed() : geometry
|
||||
}
|
||||
|
||||
function applyPlanarColumnUvs(geometry: BufferGeometry) {
|
||||
const mappedGeometry = toUvReadyGeometry(geometry)
|
||||
const positions = mappedGeometry.getAttribute('position')
|
||||
const normals = mappedGeometry.getAttribute('normal')
|
||||
const uvs: number[] = []
|
||||
|
||||
for (let index = 0; index < positions.count; index += 1) {
|
||||
const x = positions.getX(index)
|
||||
const y = positions.getY(index)
|
||||
const z = positions.getZ(index)
|
||||
const normalX = normals ? Math.abs(normals.getX(index)) : 0
|
||||
const normalY = normals ? Math.abs(normals.getY(index)) : 1
|
||||
const normalZ = normals ? Math.abs(normals.getZ(index)) : 0
|
||||
|
||||
if (normalY >= normalX && normalY >= normalZ) {
|
||||
uvs.push(x * COLUMN_UV_SCALE, z * COLUMN_UV_SCALE)
|
||||
} else if (normalX >= normalZ) {
|
||||
uvs.push(z * COLUMN_UV_SCALE, y * COLUMN_UV_SCALE)
|
||||
} else {
|
||||
uvs.push(x * COLUMN_UV_SCALE, y * COLUMN_UV_SCALE)
|
||||
}
|
||||
}
|
||||
|
||||
return setUvAttributes(mappedGeometry, uvs)
|
||||
}
|
||||
|
||||
function ellipseCircumference(radiusX: number, radiusZ: number) {
|
||||
const a = Math.max(0.001, Math.abs(radiusX))
|
||||
const b = Math.max(0.001, Math.abs(radiusZ))
|
||||
return Math.PI * (3 * (a + b) - Math.sqrt((3 * a + b) * (a + 3 * b)))
|
||||
}
|
||||
|
||||
function applyCylindricalColumnUvs(
|
||||
geometry: BufferGeometry,
|
||||
sideCircumference: number,
|
||||
height: number,
|
||||
) {
|
||||
const mappedGeometry = toUvReadyGeometry(geometry)
|
||||
const positions = mappedGeometry.getAttribute('position')
|
||||
const normals = mappedGeometry.getAttribute('normal')
|
||||
const defaultUvs = mappedGeometry.getAttribute('uv')
|
||||
const halfHeight = height / 2
|
||||
const uvs: number[] = []
|
||||
|
||||
for (let index = 0; index < positions.count; index += 1) {
|
||||
const x = positions.getX(index)
|
||||
const y = positions.getY(index)
|
||||
const z = positions.getZ(index)
|
||||
const normalY = normals ? Math.abs(normals.getY(index)) : 0
|
||||
|
||||
if (normalY > 0.65) {
|
||||
uvs.push(x * COLUMN_UV_SCALE, z * COLUMN_UV_SCALE)
|
||||
} else {
|
||||
const defaultU = defaultUvs ? defaultUvs.getX(index) : 0
|
||||
uvs.push(defaultU * sideCircumference * COLUMN_UV_SCALE, (y + halfHeight) * COLUMN_UV_SCALE)
|
||||
}
|
||||
}
|
||||
|
||||
return setUvAttributes(mappedGeometry, uvs)
|
||||
}
|
||||
|
||||
function applySphericalColumnUvs(geometry: BufferGeometry, radius: number) {
|
||||
const mappedGeometry = toUvReadyGeometry(geometry)
|
||||
const defaultUvs = mappedGeometry.getAttribute('uv')
|
||||
if (!defaultUvs) return mappedGeometry
|
||||
|
||||
const uvs: number[] = []
|
||||
const circumference = Math.PI * 2 * radius
|
||||
const arcHeight = Math.PI * radius
|
||||
|
||||
for (let index = 0; index < defaultUvs.count; index += 1) {
|
||||
uvs.push(
|
||||
defaultUvs.getX(index) * circumference * COLUMN_UV_SCALE,
|
||||
defaultUvs.getY(index) * arcHeight * COLUMN_UV_SCALE,
|
||||
)
|
||||
}
|
||||
|
||||
return setUvAttributes(mappedGeometry, uvs)
|
||||
}
|
||||
|
||||
function applyTorusColumnUvs(geometry: BufferGeometry, ringRadius: number, tubeRadius: number) {
|
||||
const mappedGeometry = toUvReadyGeometry(geometry)
|
||||
const defaultUvs = mappedGeometry.getAttribute('uv')
|
||||
if (!defaultUvs) return mappedGeometry
|
||||
|
||||
const uvs: number[] = []
|
||||
const ringLength = Math.PI * 2 * Math.max(0.001, ringRadius)
|
||||
const tubeLength = Math.PI * 2 * Math.max(0.001, tubeRadius)
|
||||
|
||||
for (let index = 0; index < defaultUvs.count; index += 1) {
|
||||
uvs.push(
|
||||
defaultUvs.getX(index) * ringLength * COLUMN_UV_SCALE,
|
||||
defaultUvs.getY(index) * tubeLength * COLUMN_UV_SCALE,
|
||||
)
|
||||
}
|
||||
|
||||
return setUvAttributes(mappedGeometry, uvs)
|
||||
}
|
||||
|
||||
function createColumnBoxGeometry(width: number, height: number, depth: number, bevelRadius = 0) {
|
||||
const geometry =
|
||||
bevelRadius > 0.001
|
||||
? new RoundedBoxGeometry(width, height, depth, 3, bevelRadius)
|
||||
: new BoxGeometry(width, height, depth)
|
||||
return applyPlanarColumnUvs(geometry)
|
||||
}
|
||||
|
||||
function createColumnCylinderGeometry({
|
||||
height,
|
||||
radiusBottom,
|
||||
radiusTop = radiusBottom,
|
||||
radiusX = 1,
|
||||
radiusZ = 1,
|
||||
segments = 32,
|
||||
}: {
|
||||
height: number
|
||||
radiusBottom: number
|
||||
radiusTop?: number
|
||||
radiusX?: number
|
||||
radiusZ?: number
|
||||
segments?: number
|
||||
}) {
|
||||
const geometry = new CylinderGeometry(radiusTop, radiusBottom, height, segments)
|
||||
geometry.scale(radiusX, 1, radiusZ)
|
||||
const sideRadius = Math.max(radiusTop, radiusBottom)
|
||||
return applyCylindricalColumnUvs(
|
||||
geometry,
|
||||
ellipseCircumference(sideRadius * radiusX, sideRadius * radiusZ),
|
||||
height,
|
||||
)
|
||||
}
|
||||
|
||||
function createColumnSphereGeometry(radius: number, widthSegments = 10, heightSegments = 8) {
|
||||
return applySphericalColumnUvs(new SphereGeometry(radius, widthSegments, heightSegments), radius)
|
||||
}
|
||||
|
||||
function createColumnTorusGeometry({
|
||||
arc = Math.PI * 2,
|
||||
radialSegments = 10,
|
||||
ringRadius,
|
||||
scaleX = ringRadius,
|
||||
scaleY = ringRadius,
|
||||
scaleZ = 1,
|
||||
tubeRadius,
|
||||
tubularSegments = 24,
|
||||
}: {
|
||||
arc?: number
|
||||
radialSegments?: number
|
||||
ringRadius: number
|
||||
scaleX?: number
|
||||
scaleY?: number
|
||||
scaleZ?: number
|
||||
tubeRadius: number
|
||||
tubularSegments?: number
|
||||
}) {
|
||||
const geometry = new TorusGeometry(1, 0.18, radialSegments, tubularSegments, arc)
|
||||
geometry.scale(scaleX, scaleY, scaleZ)
|
||||
return applyTorusColumnUvs(geometry, ringRadius, tubeRadius)
|
||||
}
|
||||
|
||||
function createColumnMaterial({
|
||||
material,
|
||||
materialPreset,
|
||||
@@ -36,12 +216,19 @@ function getShaftProfile(node: ColumnNode) {
|
||||
function getShaftSegmentCount(node: ColumnNode) {
|
||||
const shaftProfile = getShaftProfile(node)
|
||||
const shaftTaper = node.shaftTaper ?? 0
|
||||
const hasTwist = Math.abs(node.shaftTwistStep ?? 0) > 0.001
|
||||
return Math.max(
|
||||
1,
|
||||
shaftProfile === 'straight' && shaftTaper <= 0 ? 1 : (node.shaftSegmentCount ?? 24),
|
||||
hasTwist ? 4 : 1,
|
||||
shaftProfile === 'straight' && shaftTaper <= 0 && !hasTwist
|
||||
? 1
|
||||
: (node.shaftSegmentCount ?? (hasTwist ? 12 : 24)),
|
||||
)
|
||||
}
|
||||
|
||||
function getShaftTwistRadians(node: ColumnNode, index: number) {
|
||||
return ((node.shaftTwistStep ?? 0) * Math.PI * index) / 180
|
||||
}
|
||||
|
||||
function getShaftScaleAt(node: ColumnNode, t: number) {
|
||||
const shaftProfile = getShaftProfile(node)
|
||||
const shaftTaper = Math.min(node.shaftTaper ?? 0, 0.85)
|
||||
@@ -70,6 +257,181 @@ function getShaftScaleAt(node: ColumnNode, t: number) {
|
||||
return Math.max(0.1, profileScale)
|
||||
}
|
||||
|
||||
type VectorTuple = [number, number, number]
|
||||
|
||||
function MappedBox({
|
||||
depth,
|
||||
height,
|
||||
position,
|
||||
rotation,
|
||||
softenEdges = true,
|
||||
width,
|
||||
}: {
|
||||
depth: number
|
||||
height: number
|
||||
position: VectorTuple
|
||||
rotation?: VectorTuple
|
||||
softenEdges?: boolean
|
||||
width: number
|
||||
}) {
|
||||
const edgeSoftness = useContext(ColumnEdgeSoftnessContext)
|
||||
const minDimension = Math.max(0, Math.min(width, height, depth))
|
||||
const bevelRadius = softenEdges ? Math.min(Math.max(0, edgeSoftness), minDimension * 0.35) : 0
|
||||
const geometry = useMemo(() => {
|
||||
if (height <= 0 || width <= 0 || depth <= 0) return null
|
||||
return createColumnBoxGeometry(width, height, depth, bevelRadius)
|
||||
}, [bevelRadius, depth, height, width])
|
||||
|
||||
if (!geometry) return null
|
||||
|
||||
return (
|
||||
<mesh dispose={null} position={position} rotation={rotation}>
|
||||
<primitive attach="geometry" dispose={null} object={geometry} />
|
||||
<ColumnMaterial />
|
||||
</mesh>
|
||||
)
|
||||
}
|
||||
|
||||
function MappedCylinder({
|
||||
height,
|
||||
position,
|
||||
radius,
|
||||
radiusBottom = radius,
|
||||
radiusTop = radius,
|
||||
radiusX = 1,
|
||||
radiusZ = 1,
|
||||
rotation,
|
||||
segments = 32,
|
||||
}: {
|
||||
height: number
|
||||
position: VectorTuple
|
||||
radius: number
|
||||
radiusBottom?: number
|
||||
radiusTop?: number
|
||||
radiusX?: number
|
||||
radiusZ?: number
|
||||
rotation?: VectorTuple
|
||||
segments?: number
|
||||
}) {
|
||||
const geometry = useMemo(() => {
|
||||
if (height <= 0 || radius <= 0 || radiusBottom < 0 || radiusTop < 0) return null
|
||||
return createColumnCylinderGeometry({
|
||||
height,
|
||||
radiusBottom,
|
||||
radiusTop,
|
||||
radiusX,
|
||||
radiusZ,
|
||||
segments,
|
||||
})
|
||||
}, [height, radius, radiusBottom, radiusTop, radiusX, radiusZ, segments])
|
||||
|
||||
if (!geometry) return null
|
||||
|
||||
return (
|
||||
<mesh dispose={null} position={position} rotation={rotation}>
|
||||
<primitive attach="geometry" dispose={null} object={geometry} />
|
||||
<ColumnMaterial />
|
||||
</mesh>
|
||||
)
|
||||
}
|
||||
|
||||
function MappedCone({
|
||||
height,
|
||||
position,
|
||||
radiusX,
|
||||
radiusZ = radiusX,
|
||||
rotation,
|
||||
segments = 6,
|
||||
}: {
|
||||
height: number
|
||||
position: VectorTuple
|
||||
radiusX: number
|
||||
radiusZ?: number
|
||||
rotation?: VectorTuple
|
||||
segments?: number
|
||||
}) {
|
||||
const geometry = useMemo(() => {
|
||||
if (height <= 0 || radiusX <= 0 || radiusZ <= 0) return null
|
||||
return createColumnCylinderGeometry({
|
||||
height,
|
||||
radiusBottom: 1,
|
||||
radiusTop: 0,
|
||||
radiusX,
|
||||
radiusZ,
|
||||
segments,
|
||||
})
|
||||
}, [height, radiusX, radiusZ, segments])
|
||||
|
||||
if (!geometry) return null
|
||||
|
||||
return (
|
||||
<mesh dispose={null} position={position} rotation={rotation}>
|
||||
<primitive attach="geometry" dispose={null} object={geometry} />
|
||||
<ColumnMaterial />
|
||||
</mesh>
|
||||
)
|
||||
}
|
||||
|
||||
function MappedSphere({
|
||||
position,
|
||||
radius,
|
||||
segments = 10,
|
||||
verticalSegments = 8,
|
||||
}: {
|
||||
position: VectorTuple
|
||||
radius: number
|
||||
segments?: number
|
||||
verticalSegments?: number
|
||||
}) {
|
||||
const geometry = useMemo(() => {
|
||||
if (radius <= 0) return null
|
||||
return createColumnSphereGeometry(radius, segments, verticalSegments)
|
||||
}, [radius, segments, verticalSegments])
|
||||
|
||||
if (!geometry) return null
|
||||
|
||||
return (
|
||||
<mesh dispose={null} position={position}>
|
||||
<primitive attach="geometry" dispose={null} object={geometry} />
|
||||
<ColumnMaterial />
|
||||
</mesh>
|
||||
)
|
||||
}
|
||||
|
||||
function MappedTorus({
|
||||
arc,
|
||||
position,
|
||||
ringRadius,
|
||||
rotation,
|
||||
scaleX,
|
||||
scaleY,
|
||||
scaleZ,
|
||||
tubeRadius,
|
||||
}: {
|
||||
arc?: number
|
||||
position: VectorTuple
|
||||
ringRadius: number
|
||||
rotation?: VectorTuple
|
||||
scaleX?: number
|
||||
scaleY?: number
|
||||
scaleZ?: number
|
||||
tubeRadius: number
|
||||
}) {
|
||||
const geometry = useMemo(() => {
|
||||
if (ringRadius <= 0 || tubeRadius <= 0) return null
|
||||
return createColumnTorusGeometry({ arc, ringRadius, scaleX, scaleY, scaleZ, tubeRadius })
|
||||
}, [arc, ringRadius, scaleX, scaleY, scaleZ, tubeRadius])
|
||||
|
||||
if (!geometry) return null
|
||||
|
||||
return (
|
||||
<mesh dispose={null} position={position} rotation={rotation}>
|
||||
<primitive attach="geometry" dispose={null} object={geometry} />
|
||||
<ColumnMaterial />
|
||||
</mesh>
|
||||
)
|
||||
}
|
||||
|
||||
function SquareBlock({
|
||||
y,
|
||||
height,
|
||||
@@ -83,31 +445,14 @@ function SquareBlock({
|
||||
depth: number
|
||||
softenEdges?: boolean
|
||||
}) {
|
||||
const edgeSoftness = useContext(ColumnEdgeSoftnessContext)
|
||||
const minDimension = Math.max(0, Math.min(width, height, depth))
|
||||
const bevelRadius = softenEdges ? Math.min(Math.max(0, edgeSoftness), minDimension * 0.35) : 0
|
||||
const roundedGeometry = useMemo(() => {
|
||||
if (bevelRadius <= 0.001) return null
|
||||
return new RoundedBoxGeometry(width, height, depth, 3, bevelRadius)
|
||||
}, [bevelRadius, depth, height, width])
|
||||
|
||||
if (height <= 0) return null
|
||||
|
||||
const position = [0, y + height / 2, 0] as const
|
||||
|
||||
return (
|
||||
<mesh
|
||||
dispose={null}
|
||||
position={position}
|
||||
scale={roundedGeometry ? undefined : [width, height, depth]}
|
||||
>
|
||||
{roundedGeometry ? (
|
||||
<primitive attach="geometry" dispose={null} object={roundedGeometry} />
|
||||
) : (
|
||||
<boxGeometry args={[1, 1, 1]} dispose={null} />
|
||||
)}
|
||||
<ColumnMaterial />
|
||||
</mesh>
|
||||
<MappedBox
|
||||
depth={depth}
|
||||
height={height}
|
||||
position={[0, y + height / 2, 0]}
|
||||
softenEdges={softenEdges}
|
||||
width={width}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -126,13 +471,13 @@ function RoundBlock({
|
||||
radius: number
|
||||
segments?: number
|
||||
}) {
|
||||
if (height <= 0) return null
|
||||
|
||||
return (
|
||||
<mesh position={[x, y + height / 2, z]} scale={[radius, height, radius]}>
|
||||
<cylinderGeometry args={[1, 1, 1, segments]} />
|
||||
<ColumnMaterial />
|
||||
</mesh>
|
||||
<MappedCylinder
|
||||
height={height}
|
||||
position={[x, y + height / 2, z]}
|
||||
radius={radius}
|
||||
segments={segments}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -196,13 +541,15 @@ function OvalBlock({
|
||||
depth: number
|
||||
segments?: number
|
||||
}) {
|
||||
if (height <= 0) return null
|
||||
|
||||
return (
|
||||
<mesh position={[0, y + height / 2, 0]} scale={[width / 2, height, depth / 2]}>
|
||||
<cylinderGeometry args={[1, 1, 1, segments]} />
|
||||
<ColumnMaterial />
|
||||
</mesh>
|
||||
<MappedCylinder
|
||||
height={height}
|
||||
position={[0, y + height / 2, 0]}
|
||||
radius={1}
|
||||
radiusX={width / 2}
|
||||
radiusZ={depth / 2}
|
||||
segments={segments}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -240,13 +587,14 @@ function TaperedRoundShaft({ node, y, height }: { node: ColumnNode; y: number; h
|
||||
const t = (index + 0.5) / segmentCount
|
||||
const profileScale = getShaftScaleAt(node, t)
|
||||
return (
|
||||
<RoundBlock
|
||||
height={segmentHeight * 1.015}
|
||||
key={index}
|
||||
radius={node.radius * profileScale}
|
||||
segments={getSegments(node)}
|
||||
y={y + index * segmentHeight}
|
||||
/>
|
||||
<group key={index} rotation={[0, getShaftTwistRadians(node, index), 0]}>
|
||||
<RoundBlock
|
||||
height={segmentHeight * 1.015}
|
||||
radius={node.radius * profileScale}
|
||||
segments={getSegments(node)}
|
||||
y={y + index * segmentHeight}
|
||||
/>
|
||||
</group>
|
||||
)
|
||||
})}
|
||||
</group>
|
||||
@@ -263,14 +611,15 @@ function TaperedSquareShaft({ node, y, height }: { node: ColumnNode; y: number;
|
||||
const t = (index + 0.5) / segmentCount
|
||||
const profileScale = getShaftScaleAt(node, t)
|
||||
return (
|
||||
<RoundedRectangleShaftSegment
|
||||
cornerRadius={(node.shaftCornerRadius ?? 0.035) * profileScale}
|
||||
depth={node.depth * profileScale}
|
||||
height={segmentHeight * 1.015}
|
||||
key={index}
|
||||
width={node.width * profileScale}
|
||||
y={y + index * segmentHeight}
|
||||
/>
|
||||
<group key={index} rotation={[0, getShaftTwistRadians(node, index), 0]}>
|
||||
<RoundedRectangleShaftSegment
|
||||
cornerRadius={(node.shaftCornerRadius ?? 0.035) * profileScale}
|
||||
depth={node.depth * profileScale}
|
||||
height={segmentHeight * 1.015}
|
||||
width={node.width * profileScale}
|
||||
y={y + index * segmentHeight}
|
||||
/>
|
||||
</group>
|
||||
)
|
||||
})}
|
||||
</group>
|
||||
@@ -285,13 +634,7 @@ function Shaft({ node, y, height }: { node: ColumnNode; y: number; height: numbe
|
||||
const offset = Math.max(node.radius * 0.78, node.width * 0.22)
|
||||
return (
|
||||
<group>
|
||||
<mesh
|
||||
position={[0, y + height / 2, 0]}
|
||||
scale={[node.radius * 0.62, height, node.radius * 0.62]}
|
||||
>
|
||||
<cylinderGeometry args={[1, 1, 1, 24]} />
|
||||
<ColumnMaterial />
|
||||
</mesh>
|
||||
<RoundBlock height={height} radius={node.radius * 0.62} segments={24} y={y} />
|
||||
{(
|
||||
[
|
||||
[offset, 0],
|
||||
@@ -300,14 +643,15 @@ function Shaft({ node, y, height }: { node: ColumnNode; y: number; height: numbe
|
||||
[0, -offset],
|
||||
] satisfies [number, number][]
|
||||
).map(([x, z], index) => (
|
||||
<mesh
|
||||
<RoundBlock
|
||||
height={height}
|
||||
key={`${x}-${z}-${index}`}
|
||||
position={[x, y + height / 2, z]}
|
||||
scale={[sideRadius, height, sideRadius]}
|
||||
>
|
||||
<cylinderGeometry args={[1, 1, 1, 16]} />
|
||||
<ColumnMaterial />
|
||||
</mesh>
|
||||
radius={sideRadius}
|
||||
segments={16}
|
||||
x={x}
|
||||
y={y}
|
||||
z={z}
|
||||
/>
|
||||
))}
|
||||
</group>
|
||||
)
|
||||
@@ -439,19 +783,18 @@ function Base({ node, height }: { node: ColumnNode; height: number }) {
|
||||
{Array.from({ length: ribCount }, (_, index) => {
|
||||
const angle = (index / ribCount) * Math.PI * 2
|
||||
return (
|
||||
<mesh
|
||||
<MappedCylinder
|
||||
height={height * 0.38}
|
||||
key={index}
|
||||
position={[
|
||||
Math.cos(angle) * baseRadius * 0.86,
|
||||
height * 0.58,
|
||||
Math.sin(angle) * baseRadius * 0.86,
|
||||
]}
|
||||
radius={ribRadius}
|
||||
rotation={[0, -angle, 0]}
|
||||
scale={[ribRadius, height * 0.38, ribRadius]}
|
||||
>
|
||||
<cylinderGeometry args={[1, 1, 1, 6]} />
|
||||
<ColumnMaterial />
|
||||
</mesh>
|
||||
segments={6}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
<RoundBlock
|
||||
@@ -482,15 +825,15 @@ function Base({ node, height }: { node: ColumnNode; height: number }) {
|
||||
[-node.width * widthScale * 0.51, 0, Math.PI / 2],
|
||||
] satisfies [number, number, number][]
|
||||
).map(([x, z, rotation], index) => (
|
||||
<mesh
|
||||
<MappedBox
|
||||
depth={inset}
|
||||
height={height * 0.42}
|
||||
key={index}
|
||||
position={[x, height * 0.5, z]}
|
||||
rotation={[0, rotation, 0]}
|
||||
scale={[node.width * 0.36, height * 0.42, inset]}
|
||||
>
|
||||
<boxGeometry args={[1, 1, 1]} />
|
||||
<ColumnMaterial />
|
||||
</mesh>
|
||||
softenEdges={false}
|
||||
width={node.width * 0.36}
|
||||
/>
|
||||
))}
|
||||
</group>
|
||||
)
|
||||
@@ -515,15 +858,15 @@ function BaseCarvings({ node, height }: { node: ColumnNode; height: number }) {
|
||||
{Array.from({ length: count }, (_, index) => {
|
||||
const angle = (index / count) * Math.PI * 2
|
||||
return (
|
||||
<mesh
|
||||
<MappedCone
|
||||
height={height * 0.28}
|
||||
key={index}
|
||||
position={[Math.cos(angle) * radius, y, Math.sin(angle) * radius]}
|
||||
radiusX={0.014}
|
||||
radiusZ={0.01}
|
||||
rotation={[0.36, -angle, 0]}
|
||||
scale={[0.014, height * 0.28, 0.01]}
|
||||
>
|
||||
<coneGeometry args={[1, 1, 5]} />
|
||||
<ColumnMaterial />
|
||||
</mesh>
|
||||
segments={5}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</group>
|
||||
@@ -653,14 +996,13 @@ function Flutes({
|
||||
const x = Math.cos(angle) * shaftRadius
|
||||
const z = Math.sin(angle) * shaftRadius
|
||||
return (
|
||||
<mesh
|
||||
<MappedCylinder
|
||||
height={shaftHeight * 0.92}
|
||||
key={index}
|
||||
position={[x, shaftY + shaftHeight / 2, z]}
|
||||
scale={[fluteRadius, shaftHeight * 0.92, fluteRadius]}
|
||||
>
|
||||
<cylinderGeometry args={[1, 1, 1, 8]} />
|
||||
<ColumnMaterial />
|
||||
</mesh>
|
||||
radius={fluteRadius}
|
||||
segments={8}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</group>
|
||||
@@ -699,33 +1041,54 @@ function DravidianShaftPanels({
|
||||
rotation?: number
|
||||
}) => (
|
||||
<group position={position} rotation={[0, rotation, 0]}>
|
||||
<mesh position={[0, panelHeight / 2, 0]} scale={[panelWidth, rail, reliefDepth]}>
|
||||
<boxGeometry args={[1, 1, 1]} />
|
||||
<ColumnMaterial />
|
||||
</mesh>
|
||||
<mesh position={[0, -panelHeight / 2, 0]} scale={[panelWidth, rail, reliefDepth]}>
|
||||
<boxGeometry args={[1, 1, 1]} />
|
||||
<ColumnMaterial />
|
||||
</mesh>
|
||||
<mesh position={[panelWidth / 2, 0, 0]} scale={[rail, panelHeight, reliefDepth]}>
|
||||
<boxGeometry args={[1, 1, 1]} />
|
||||
<ColumnMaterial />
|
||||
</mesh>
|
||||
<mesh position={[-panelWidth / 2, 0, 0]} scale={[rail, panelHeight, reliefDepth]}>
|
||||
<boxGeometry args={[1, 1, 1]} />
|
||||
<ColumnMaterial />
|
||||
</mesh>
|
||||
<MappedBox
|
||||
depth={reliefDepth}
|
||||
height={rail}
|
||||
position={[0, panelHeight / 2, 0]}
|
||||
softenEdges={false}
|
||||
width={panelWidth}
|
||||
/>
|
||||
<MappedBox
|
||||
depth={reliefDepth}
|
||||
height={rail}
|
||||
position={[0, -panelHeight / 2, 0]}
|
||||
softenEdges={false}
|
||||
width={panelWidth}
|
||||
/>
|
||||
<MappedBox
|
||||
depth={reliefDepth}
|
||||
height={panelHeight}
|
||||
position={[panelWidth / 2, 0, 0]}
|
||||
softenEdges={false}
|
||||
width={rail}
|
||||
/>
|
||||
<MappedBox
|
||||
depth={reliefDepth}
|
||||
height={panelHeight}
|
||||
position={[-panelWidth / 2, 0, 0]}
|
||||
softenEdges={false}
|
||||
width={rail}
|
||||
/>
|
||||
{panelShape === 'diamond' && (
|
||||
<mesh rotation={[0, 0, Math.PI / 4]} scale={[rail * 1.2, panelHeight * 0.42, reliefDepth]}>
|
||||
<boxGeometry args={[1, 1, 1]} />
|
||||
<ColumnMaterial />
|
||||
</mesh>
|
||||
<MappedBox
|
||||
depth={reliefDepth}
|
||||
height={panelHeight * 0.42}
|
||||
position={[0, 0, 0]}
|
||||
rotation={[0, 0, Math.PI / 4]}
|
||||
softenEdges={false}
|
||||
width={rail * 1.2}
|
||||
/>
|
||||
)}
|
||||
{panelShape === 'arched' && (
|
||||
<mesh position={[0, panelHeight * 0.28, 0]} scale={[panelWidth * 0.42, rail, reliefDepth]}>
|
||||
<torusGeometry args={[1, 0.18, 8, 16, Math.PI]} />
|
||||
<ColumnMaterial />
|
||||
</mesh>
|
||||
<MappedTorus
|
||||
arc={Math.PI}
|
||||
position={[0, panelHeight * 0.28, 0]}
|
||||
ringRadius={panelWidth * 0.42}
|
||||
scaleX={panelWidth * 0.42}
|
||||
scaleY={rail}
|
||||
scaleZ={reliefDepth}
|
||||
tubeRadius={Math.max(rail, reliefDepth) * 0.18}
|
||||
/>
|
||||
)}
|
||||
</group>
|
||||
)
|
||||
@@ -781,19 +1144,18 @@ function SpiralRibs({
|
||||
const angle = (ribIndex / ribCount) * Math.PI * 2 + t * spiralTwist * Math.PI * 2
|
||||
const taperScale = 1 - Math.min(shaftTaper, 0.85) * t
|
||||
return (
|
||||
<mesh
|
||||
<MappedCylinder
|
||||
height={segmentHeight}
|
||||
key={index}
|
||||
position={[
|
||||
Math.cos(angle) * ribDistance * taperScale,
|
||||
shaftY + shaftHeight * t,
|
||||
Math.sin(angle) * ribDistance * taperScale,
|
||||
]}
|
||||
radius={ribWidth}
|
||||
rotation={[0, -angle, lean]}
|
||||
scale={[ribWidth, segmentHeight, ribWidth]}
|
||||
>
|
||||
<cylinderGeometry args={[1, 1, 1, 8]} />
|
||||
<ColumnMaterial />
|
||||
</mesh>
|
||||
segments={8}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</group>
|
||||
@@ -830,19 +1192,18 @@ function LowerCarvedBand({
|
||||
{Array.from({ length: count }, (_, index) => {
|
||||
const angle = (index / count) * Math.PI * 2
|
||||
return (
|
||||
<mesh
|
||||
<MappedCylinder
|
||||
height={bandHeight * 0.62}
|
||||
key={index}
|
||||
position={[
|
||||
Math.cos(angle) * distance,
|
||||
y + bandHeight * 0.5,
|
||||
Math.sin(angle) * distance,
|
||||
]}
|
||||
radius={0.012}
|
||||
rotation={[0, -angle, 0]}
|
||||
scale={[0.012, bandHeight * 0.62, 0.012]}
|
||||
>
|
||||
<cylinderGeometry args={[1, 1, 1, 5]} />
|
||||
<ColumnMaterial />
|
||||
</mesh>
|
||||
segments={5}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</group>
|
||||
@@ -907,26 +1268,38 @@ function CapitalCarvings({
|
||||
))}
|
||||
{xPositions.map((x, index) => (
|
||||
<group key={`front-back-dentil-${index}`}>
|
||||
<mesh position={[x, y, halfDepth]} scale={[dentilWidth, dentilHeight, dentilDepth]}>
|
||||
<boxGeometry args={[1, 1, 1]} />
|
||||
<ColumnMaterial />
|
||||
</mesh>
|
||||
<mesh position={[x, y, -halfDepth]} scale={[dentilWidth, dentilHeight, dentilDepth]}>
|
||||
<boxGeometry args={[1, 1, 1]} />
|
||||
<ColumnMaterial />
|
||||
</mesh>
|
||||
<MappedBox
|
||||
depth={dentilDepth}
|
||||
height={dentilHeight}
|
||||
position={[x, y, halfDepth]}
|
||||
softenEdges={false}
|
||||
width={dentilWidth}
|
||||
/>
|
||||
<MappedBox
|
||||
depth={dentilDepth}
|
||||
height={dentilHeight}
|
||||
position={[x, y, -halfDepth]}
|
||||
softenEdges={false}
|
||||
width={dentilWidth}
|
||||
/>
|
||||
</group>
|
||||
))}
|
||||
{zPositions.map((z, index) => (
|
||||
<group key={`side-dentil-${index}`}>
|
||||
<mesh position={[halfWidth, y, z]} scale={[dentilDepth, dentilHeight, dentilWidth]}>
|
||||
<boxGeometry args={[1, 1, 1]} />
|
||||
<ColumnMaterial />
|
||||
</mesh>
|
||||
<mesh position={[-halfWidth, y, z]} scale={[dentilDepth, dentilHeight, dentilWidth]}>
|
||||
<boxGeometry args={[1, 1, 1]} />
|
||||
<ColumnMaterial />
|
||||
</mesh>
|
||||
<MappedBox
|
||||
depth={dentilWidth}
|
||||
height={dentilHeight}
|
||||
position={[halfWidth, y, z]}
|
||||
softenEdges={false}
|
||||
width={dentilDepth}
|
||||
/>
|
||||
<MappedBox
|
||||
depth={dentilWidth}
|
||||
height={dentilHeight}
|
||||
position={[-halfWidth, y, z]}
|
||||
softenEdges={false}
|
||||
width={dentilDepth}
|
||||
/>
|
||||
</group>
|
||||
))}
|
||||
</group>
|
||||
@@ -946,14 +1319,11 @@ function CapitalCarvings({
|
||||
{Array.from({ length: beadCount }, (_, index) => {
|
||||
const angle = (index / beadCount) * Math.PI * 2
|
||||
return (
|
||||
<mesh
|
||||
<MappedSphere
|
||||
key={`bead-${index}`}
|
||||
position={[Math.cos(angle) * beadDistance, beadY, Math.sin(angle) * beadDistance]}
|
||||
scale={[beadRadius, beadRadius, beadRadius]}
|
||||
>
|
||||
<sphereGeometry args={[1, 10, 8]} />
|
||||
<ColumnMaterial />
|
||||
</mesh>
|
||||
radius={beadRadius}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</group>
|
||||
@@ -1015,15 +1385,14 @@ function Volutes({
|
||||
return (
|
||||
<group>
|
||||
{volutes.map((volute, index) => (
|
||||
<mesh
|
||||
<MappedTorus
|
||||
key={index}
|
||||
position={volute.position}
|
||||
ringRadius={radius}
|
||||
rotation={volute.rotation}
|
||||
scale={[radius, radius, radius * 0.28]}
|
||||
>
|
||||
<torusGeometry args={[1, 0.18, 10, 24]} />
|
||||
<ColumnMaterial />
|
||||
</mesh>
|
||||
scaleZ={radius * 0.28}
|
||||
tubeRadius={radius * 0.18}
|
||||
/>
|
||||
))}
|
||||
</group>
|
||||
)
|
||||
@@ -1060,15 +1429,15 @@ function LeafCarvings({
|
||||
Array.from({ length: leafCount }, (_, index) => {
|
||||
const angle = (index / leafCount) * Math.PI * 2 + row.offset
|
||||
return (
|
||||
<mesh
|
||||
<MappedCone
|
||||
height={capitalHeight * row.scale}
|
||||
key={`${rowIndex}-${index}`}
|
||||
position={[Math.cos(angle) * distance, row.y, Math.sin(angle) * distance]}
|
||||
radiusX={0.018}
|
||||
radiusZ={0.01}
|
||||
rotation={[0.48, -angle, 0]}
|
||||
scale={[0.018, capitalHeight * row.scale, 0.01]}
|
||||
>
|
||||
<coneGeometry args={[1, 1, 6]} />
|
||||
<ColumnMaterial />
|
||||
</mesh>
|
||||
segments={6}
|
||||
/>
|
||||
)
|
||||
}),
|
||||
)}
|
||||
@@ -1106,14 +1475,14 @@ function Capital({ node, y, height }: { node: ColumnNode; y: number; height: num
|
||||
const angle = (index / count) * Math.PI * 2
|
||||
const distance = Math.max(node.width, node.depth) * 0.56
|
||||
return (
|
||||
<mesh
|
||||
<MappedCone
|
||||
height={height * 0.28}
|
||||
key={index}
|
||||
position={[Math.cos(angle) * distance, y - height * 0.1, Math.sin(angle) * distance]}
|
||||
scale={[0.035, height * 0.28, 0.035]}
|
||||
>
|
||||
<coneGeometry args={[1, 1, 6]} />
|
||||
<ColumnMaterial />
|
||||
</mesh>
|
||||
radiusX={0.035}
|
||||
rotation={[0, 0, 0]}
|
||||
segments={6}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</group>
|
||||
|
||||
Reference in New Issue
Block a user