Refactor guide events and column placement handling
This commit is contained in:
@@ -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(() => {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user