Enable pillar duplication in floating action menu

This commit is contained in:
sudhir
2026-05-04 22:54:52 +05:30
parent 848529acb0
commit db765f1eb3
4 changed files with 577 additions and 169 deletions
+7
View File
@@ -86,6 +86,7 @@ export const ColumnNode = BaseNode.extend({
shaftStartScale: z.number().min(0.2).max(2).default(0.72),
shaftEndScale: z.number().min(0.2).max(2).default(0.72),
shaftSegmentCount: z.number().int().min(1).max(64).default(24),
shaftTwistStep: z.number().min(-90).max(90).default(0),
shaftCornerRadius: z.number().min(0).max(0.3).default(0.035),
shaftDetail: ColumnShaftDetail.default('none'),
baseStyle: ColumnBaseStyle.default('round-rings'),
@@ -144,6 +145,7 @@ export const ColumnNode = BaseNode.extend({
- height/radius/width/depth: primary dimensions in meters
- edgeSoftness: bevel radius for square/plinth/block edges
- shaftProfile/shaftDetail: profile and surface treatment of the shaft
- shaftTwistStep: per-segment shaft rotation in degrees
- shaftCornerRadius: rounded-corner radius for square/rectangular shaft cross-sections
- baseStyle/capitalStyle: procedural base and top treatment with tier/detail controls
- baseHeight/capitalHeight: bottom and top block proportions
@@ -168,6 +170,7 @@ export const COLUMN_PRESETS = {
shaftStartScale: 0.72,
shaftEndScale: 0.72,
shaftSegmentCount: 1,
shaftTwistStep: 0,
shaftCornerRadius: 0.035,
shaftDetail: 'none',
baseStyle: 'square-plinth',
@@ -217,6 +220,7 @@ export const COLUMN_PRESETS = {
shaftStartScale: 0.72,
shaftEndScale: 0.72,
shaftSegmentCount: 1,
shaftTwistStep: 0,
shaftCornerRadius: 0.045,
shaftDetail: 'none',
baseStyle: 'simple-square',
@@ -266,6 +270,7 @@ export const COLUMN_PRESETS = {
shaftStartScale: 0.82,
shaftEndScale: 0.72,
shaftSegmentCount: 32,
shaftTwistStep: 0,
shaftCornerRadius: 0.035,
shaftDetail: 'none',
baseStyle: 'square-plinth',
@@ -315,6 +320,7 @@ export const COLUMN_PRESETS = {
shaftStartScale: 0.68,
shaftEndScale: 0.68,
shaftSegmentCount: 32,
shaftTwistStep: 0,
shaftCornerRadius: 0.035,
shaftDetail: 'none',
baseStyle: 'square-plinth',
@@ -364,6 +370,7 @@ export const COLUMN_PRESETS = {
shaftStartScale: 0.84,
shaftEndScale: 0.84,
shaftSegmentCount: 32,
shaftTwistStep: 0,
shaftCornerRadius: 0.035,
shaftDetail: 'none',
baseStyle: 'square-plinth',
@@ -4,6 +4,7 @@ import {
type AnyNode,
type AnyNodeId,
type CeilingNode,
ColumnNode,
DoorNode,
FenceNode,
generateId,
@@ -262,6 +263,8 @@ export function FloatingActionMenu() {
duplicate = WindowNode.parse(duplicateInfo)
} else if (node.type === 'item') {
duplicate = ItemNode.parse(duplicateInfo)
} else if (node.type === 'column') {
duplicate = ColumnNode.parse(duplicateInfo)
} else if (node.type === 'wall') {
duplicate = WallNode.parse(duplicateInfo)
} else if (node.type === 'fence') {
@@ -322,6 +325,7 @@ export function FloatingActionMenu() {
}
if (
duplicate.type === 'item' ||
duplicate.type === 'column' ||
duplicate.type === 'wall' ||
duplicate.type === 'fence' ||
duplicate.type === 'window' ||
@@ -425,7 +429,6 @@ export function FloatingActionMenu() {
onDuplicate={
node &&
node.type !== 'spawn' &&
node.type !== 'column' &&
!DELETE_ONLY_TYPES.includes(node.type) &&
!HOLE_TYPES.includes(node.type)
? handleDuplicate
@@ -155,6 +155,7 @@ function shaftProfileUpdates(shaftProfile: ColumnNode['shaftProfile']): Partial<
shaftStartScale: 0.72,
shaftEndScale: 0.72,
shaftSegmentCount: 1,
shaftTwistStep: 0,
}
}
@@ -406,6 +407,34 @@ export function ColumnPanel() {
/>
</>
)}
<SliderControl
label="Segment Twist"
max={90}
min={-90}
onChange={(value) =>
handleUpdate({
shaftTwistStep: value,
...(Math.abs(value) > 0.001 && (node.shaftSegmentCount ?? 1) < 8
? { shaftSegmentCount: 12 }
: {}),
})
}
precision={0}
step={5}
unit="°"
value={node.shaftTwistStep ?? 0}
/>
{Math.abs(node.shaftTwistStep ?? 0) > 0.001 && (
<SliderControl
label="Twist Segments"
max={48}
min={4}
onChange={(value) => handleUpdate({ shaftSegmentCount: Math.round(value) })}
precision={0}
step={1}
value={node.shaftSegmentCount ?? 12}
/>
)}
<SliderControl
label="Ring Pairs"
max={4}
@@ -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 (
<group key={index} rotation={[0, getShaftTwistRadians(node, index), 0]}>
<RoundBlock
height={segmentHeight * 1.015}
key={index}
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 (
<group key={index} rotation={[0, getShaftTwistRadians(node, index), 0]}>
<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>
)
})}
</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>