Refactor guide events and column placement handling

This commit is contained in:
sudhir
2026-05-04 23:06:23 +05:30
parent db765f1eb3
commit 762c9fc763
8 changed files with 224 additions and 210 deletions
-8
View File
@@ -7,7 +7,6 @@ import type {
ColumnNode, ColumnNode,
DoorNode, DoorNode,
FenceNode, FenceNode,
GuideNode,
ItemNode, ItemNode,
LevelNode, LevelNode,
RoofNode, RoofNode,
@@ -133,12 +132,6 @@ type ToolEvents = {
'tool:cancel': undefined 'tool:cancel': undefined
} }
type GuideEvents = {
'guide:set-reference-scale': { guideId: GuideNode['id'] }
'guide:cancel-reference-scale': undefined
'guide:deleted': { guideId: GuideNode['id'] }
}
type PresetEvents = { type PresetEvents = {
'preset:generate-thumbnail': { presetId: string; nodeId: string } 'preset:generate-thumbnail': { presetId: string; nodeId: string }
'preset:thumbnail-updated': { presetId: string; thumbnailUrl: string } 'preset:thumbnail-updated': { presetId: string; thumbnailUrl: string }
@@ -180,7 +173,6 @@ type EditorEvents = GridEvents &
NodeEvents<'door', DoorEvent> & NodeEvents<'door', DoorEvent> &
CameraControlEvents & CameraControlEvents &
ToolEvents & ToolEvents &
GuideEvents &
PresetEvents & PresetEvents &
ThumbnailEvents & ThumbnailEvents &
SnapshotEvents & SnapshotEvents &
@@ -64,6 +64,7 @@ import {
rotatePlanVector as rotateSharedPlanVector, rotatePlanVector as rotateSharedPlanVector,
type FloorplanNodeTransform as SharedFloorplanNodeTransform, type FloorplanNodeTransform as SharedFloorplanNodeTransform,
} from '../../lib/floorplan' } from '../../lib/floorplan'
import { guideEmitter } from '../../lib/guide-events'
import { duplicateRoofSubtree } from '../../lib/roof-duplication' import { duplicateRoofSubtree } from '../../lib/roof-duplication'
import { sfxEmitter } from '../../lib/sfx-bus' import { sfxEmitter } from '../../lib/sfx-bus'
import { duplicateStairSubtree } from '../../lib/stair-duplication' import { duplicateStairSubtree } from '../../lib/stair-duplication'
@@ -9131,9 +9132,9 @@ export function FloorplanPanel() {
} }
} }
emitter.on('guide:set-reference-scale', handleSetReferenceScale) guideEmitter.on('guide:set-reference-scale', handleSetReferenceScale)
return () => { return () => {
emitter.off('guide:set-reference-scale', handleSetReferenceScale) guideEmitter.off('guide:set-reference-scale', handleSetReferenceScale)
} }
}, [startReferenceScaleForGuide]) }, [startReferenceScaleForGuide])
@@ -9143,9 +9144,9 @@ export function FloorplanPanel() {
setPendingReferenceScale(null) setPendingReferenceScale(null)
} }
emitter.on('guide:cancel-reference-scale', handleCancel) guideEmitter.on('guide:cancel-reference-scale', handleCancel)
return () => { return () => {
emitter.off('guide:cancel-reference-scale', handleCancel) guideEmitter.off('guide:cancel-reference-scale', handleCancel)
} }
}, []) }, [])
@@ -9160,9 +9161,9 @@ export function FloorplanPanel() {
clearGuideUi(payload.guideId) clearGuideUi(payload.guideId)
} }
emitter.on('guide:deleted', handleDeleted) guideEmitter.on('guide:deleted', handleDeleted)
return () => { return () => {
emitter.off('guide:deleted', handleDeleted) guideEmitter.off('guide:deleted', handleDeleted)
} }
}, [clearGuideUi]) }, [clearGuideUi])
@@ -1,16 +1,15 @@
import '../../../three-types' import '../../../three-types'
import { import {
type AnyNode,
COLUMN_PRESETS, COLUMN_PRESETS,
ColumnNode, ColumnNode,
type ColumnNode as ColumnNodeType,
type ColumnPresetId, type ColumnPresetId,
emitter, emitter,
type GridEvent, type GridEvent,
type LevelNode, type LevelNode,
useScene, useScene,
} from '@pascal-app/core' } from '@pascal-app/core'
import { useViewer } from '@pascal-app/viewer'
import { useEffect, useRef, useState } from 'react' import { useEffect, useRef, useState } from 'react'
import type { Group } from 'three' import type { Group } from 'three'
import { sfxEmitter } from '../../../lib/sfx-bus' import { sfxEmitter } from '../../../lib/sfx-bus'
@@ -41,9 +40,10 @@ function createColumnFromPreset(presetId: ColumnPresetId, position: [number, num
type ColumnToolProps = { type ColumnToolProps = {
currentLevelId: LevelNode['id'] | null currentLevelId: LevelNode['id'] | null
onPlaced?: (nodeId: ColumnNodeType['id']) => void
} }
export const ColumnTool: React.FC<ColumnToolProps> = ({ currentLevelId }) => { export const ColumnTool: React.FC<ColumnToolProps> = ({ currentLevelId, onPlaced }) => {
const [, setCursorPosition] = useState<[number, number, number] | null>(null) const [, setCursorPosition] = useState<[number, number, number] | null>(null)
const cursorRef = useRef<Group>(null) const cursorRef = useRef<Group>(null)
@@ -68,7 +68,7 @@ export const ColumnTool: React.FC<ColumnToolProps> = ({ currentLevelId }) => {
] ]
const column = createColumnFromPreset(DEFAULT_COLUMN_PRESET_ID, position) const column = createColumnFromPreset(DEFAULT_COLUMN_PRESET_ID, position)
useScene.getState().createNode(column, currentLevelId) useScene.getState().createNode(column, currentLevelId)
useViewer.getState().setSelection({ selectedIds: [column.id as AnyNode['id']] }) onPlaced?.(column.id)
sfxEmitter.emit('sfx:structure-build') sfxEmitter.emit('sfx:structure-build')
useEditor.getState().setTool(null) useEditor.getState().setTool(null)
useEditor.getState().setMode('select') useEditor.getState().setMode('select')
@@ -81,7 +81,7 @@ export const ColumnTool: React.FC<ColumnToolProps> = ({ currentLevelId }) => {
emitter.off('grid:move', onGridMove) emitter.off('grid:move', onGridMove)
emitter.off('grid:click', onGridClick) emitter.off('grid:click', onGridClick)
} }
}, [currentLevelId]) }, [currentLevelId, onPlaced])
if (!currentLevelId) return null if (!currentLevelId) return null
@@ -127,7 +127,7 @@ export const ToolManager: React.FC = () => {
const showBuildTool = mode === 'build' && tool !== null const showBuildTool = mode === 'build' && tool !== null
const BuildToolComponent = showBuildTool ? tools[phase]?.[tool] : null const BuildToolComponent = showBuildTool ? tools[phase]?.[tool] : null
const handleSpawnSelected = (nodeId: `spawn_${string}`) => { const handlePlacedNodeSelected = (nodeId: AnyNodeId) => {
setSelection({ selectedIds: [nodeId] }) setSelection({ selectedIds: [nodeId] })
} }
@@ -135,7 +135,7 @@ export const ToolManager: React.FC = () => {
<> <>
{showSiteBoundaryEditor && <SiteBoundaryEditor />} {showSiteBoundaryEditor && <SiteBoundaryEditor />}
{/* World-space tools: site boundary and building movement operate in world coordinates */} {/* World-space tools: site boundary and building movement operate in world coordinates */}
{movingNode?.type === 'building' && <MoveTool onSpawnMoved={handleSpawnSelected} />} {movingNode?.type === 'building' && <MoveTool onSpawnMoved={handlePlacedNodeSelected} />}
{/* Building-local group: all other tools are relative to the selected building. {/* Building-local group: all other tools are relative to the selected building.
Cursor visuals set positions in building-local space; this group applies the Cursor visuals set positions in building-local space; this group applies the
@@ -160,13 +160,13 @@ export const ToolManager: React.FC = () => {
{curvingWall && <CurveWallTool node={curvingWall} />} {curvingWall && <CurveWallTool node={curvingWall} />}
{curvingFence && <CurveFenceTool node={curvingFence} />} {curvingFence && <CurveFenceTool node={curvingFence} />}
{movingNode && movingNode.type !== 'building' && ( {movingNode && movingNode.type !== 'building' && (
<MoveTool onSpawnMoved={handleSpawnSelected} /> <MoveTool onSpawnMoved={handlePlacedNodeSelected} />
)} )}
{!movingNode && showBuildTool && tool === 'spawn' && ( {!movingNode && showBuildTool && tool === 'spawn' && (
<SpawnTool currentLevelId={selectedLevelId} onPlaced={handleSpawnSelected} /> <SpawnTool currentLevelId={selectedLevelId} onPlaced={handlePlacedNodeSelected} />
)} )}
{!movingNode && showBuildTool && tool === 'column' && ( {!movingNode && showBuildTool && tool === 'column' && (
<ColumnTool currentLevelId={selectedLevelId} /> <ColumnTool currentLevelId={selectedLevelId} onPlaced={handlePlacedNodeSelected} />
)} )}
{!movingNode && BuildToolComponent && tool !== 'column' && <BuildToolComponent />} {!movingNode && BuildToolComponent && tool !== 'column' && <BuildToolComponent />}
</group> </group>
@@ -2,7 +2,6 @@
import { import {
type AnyNode, type AnyNode,
emitter,
type GuideNode, type GuideNode,
loadAssetUrl, loadAssetUrl,
saveAsset, saveAsset,
@@ -11,6 +10,7 @@ import {
} from '@pascal-app/core' } from '@pascal-app/core'
import { Eye, EyeOff, LocateFixed, Lock, RotateCcw, Ruler, Trash2, Unlock, Upload } from 'lucide-react' import { Eye, EyeOff, LocateFixed, Lock, RotateCcw, Ruler, Trash2, Unlock, Upload } from 'lucide-react'
import { useCallback, useEffect, useRef, useState } from 'react' import { useCallback, useEffect, useRef, useState } from 'react'
import { guideEmitter } from '../../../lib/guide-events'
import { getGuideImageName } from '../../../lib/local-guide-image' import { getGuideImageName } from '../../../lib/local-guide-image'
import useEditor from '../../../store/use-editor' import useEditor from '../../../store/use-editor'
import { ActionButton, ActionGroup } from '../controls/action-button' import { ActionButton, ActionGroup } from '../controls/action-button'
@@ -98,7 +98,7 @@ export function ReferencePanel() {
} }
deleteNode(selectedReferenceId as AnyNode['id']) deleteNode(selectedReferenceId as AnyNode['id'])
emitter.emit('guide:deleted', { guideId: selectedReferenceId as GuideNode['id'] }) guideEmitter.emit('guide:deleted', { guideId: selectedReferenceId as GuideNode['id'] })
clearGuideUi(selectedReferenceId) clearGuideUi(selectedReferenceId)
setSelectedReferenceId(null) setSelectedReferenceId(null)
}, [clearGuideUi, deleteNode, node?.type, selectedReferenceId, setSelectedReferenceId]) }, [clearGuideUi, deleteNode, node?.type, selectedReferenceId, setSelectedReferenceId])
@@ -108,11 +108,11 @@ export function ReferencePanel() {
return return
} }
emitter.emit('guide:set-reference-scale', { guideId: node.id }) guideEmitter.emit('guide:set-reference-scale', { guideId: node.id })
}, [node]) }, [node])
const handleCancelScale = useCallback(() => { const handleCancelScale = useCallback(() => {
emitter.emit('guide:cancel-reference-scale') guideEmitter.emit('guide:cancel-reference-scale')
}, []) }, [])
useEffect(() => { useEffect(() => {
+10
View File
@@ -0,0 +1,10 @@
import type { GuideNode } from '@pascal-app/core'
import mitt from 'mitt'
type GuideEditorEvents = {
'guide:set-reference-scale': { guideId: GuideNode['id'] }
'guide:cancel-reference-scale': undefined
'guide:deleted': { guideId: GuideNode['id'] }
}
export const guideEmitter = mitt<GuideEditorEvents>()
@@ -1,198 +1,23 @@
import { type ColumnNode, useLiveTransforms, useRegistry } from '@pascal-app/core' import { type ColumnNode, useLiveTransforms, useRegistry } from '@pascal-app/core'
import { createContext, useContext, useMemo, useRef } from 'react' import { createContext, useContext, useMemo, useRef } from 'react'
import { import type { Group, Material } from 'three'
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 { useNodeEvents } from '../../../hooks/use-node-events'
import { baseMaterial, createMaterial, createMaterialFromPresetRef } from '../../../lib/materials' import { baseMaterial, createMaterial, createMaterialFromPresetRef } from '../../../lib/materials'
import {
createColumnBoxGeometry,
createColumnCylinderGeometry,
createColumnSphereGeometry,
createColumnTorusGeometry,
} from '../../../systems/column/column-geometry'
const ColumnMaterialContext = createContext<Material>(baseMaterial as Material) const ColumnMaterialContext = createContext<Material>(baseMaterial as Material)
const ColumnEdgeSoftnessContext = createContext(0.025) const ColumnEdgeSoftnessContext = createContext(0.025)
const COLUMN_UV_SCALE = 1
function ColumnMaterial() { function ColumnMaterial() {
const material = useContext(ColumnMaterialContext) const material = useContext(ColumnMaterialContext)
return <primitive attach="material" object={material} /> 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({ function createColumnMaterial({
material, material,
materialPreset, materialPreset,
@@ -0,0 +1,186 @@
import {
BoxGeometry,
type BufferGeometry,
CylinderGeometry,
Float32BufferAttribute,
SphereGeometry,
TorusGeometry,
} from 'three'
import { RoundedBoxGeometry } from 'three/examples/jsm/geometries/RoundedBoxGeometry.js'
const COLUMN_UV_SCALE = 1
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)
}
export 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)
}
export 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,
)
}
export function createColumnSphereGeometry(radius: number, widthSegments = 10, heightSegments = 8) {
return applySphericalColumnUvs(new SphereGeometry(radius, widthSegments, heightSegments), radius)
}
export 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)
}