threepipe
Version:
A modern 3D viewer framework built on top of three.js, written in TypeScript, designed to make creating high-quality, modular, and extensible 3D experiences on the web simple and enjoyable.
331 lines (303 loc) • 13.2 kB
text/typescript
import {
ArcCurve,
CatmullRomCurve3,
CubicBezierCurve,
CubicBezierCurve3,
Curve,
CurvePath,
EllipseCurve,
LineCurve,
LineCurve3,
QuadraticBezierCurve,
QuadraticBezierCurve3,
SplineCurve,
Vector2,
Vector3,
} from 'three'
import {generateValueConfig, IUiConfigContainer, UiObjectConfig} from 'uiconfig.js'
import {Class} from 'ts-browser-helpers'
import {EllipseCurve3D} from '../../../core/geometry/EllipseCurve3D'
import {CurvePath3} from '../../../three/utils/curve'
export type SupportedCurveTypes =
| LineCurve
| LineCurve3
| CubicBezierCurve
| CubicBezierCurve3
| EllipseCurve
| ArcCurve
| QuadraticBezierCurve
| QuadraticBezierCurve3
| CatmullRomCurve3
| SplineCurve
| CurvePath<Vector2>
| CurvePath3
export interface CurveUiEntry {
ctor?: Class<Curve<any>>,
getUiConfig: (curve: any) => UiObjectConfig['children']
/** Maps semantic property names to actual curve property names, used to copy values when switching curve types */
vals: Record<string, string>
}
/**
* Registry of curve types with their UI config generators.
* Used by geometry generators that have curve parameters (TubeGeometryGenerator, LineGeometryGenerator, etc.)
*/
export const curveUiConfigs: Record<string, CurveUiEntry> = {}
// ── 2D Curves ──
curveUiConfigs.LineCurve = {
ctor: LineCurve,
vals: {start: 'v1', end: 'v2'},
getUiConfig: (curve: LineCurve | LineCurve3) => [
generateValueConfig(curve, 'v1', 'Start Point'),
generateValueConfig(curve, 'v2', 'End Point'),
] as UiObjectConfig['children'],
}
curveUiConfigs.CubicBezierCurve = {
ctor: CubicBezierCurve,
vals: {start: 'v0', end: 'v3', control1: 'v1', control2: 'v2'},
getUiConfig: (curve: CubicBezierCurve | CubicBezierCurve3) => [
generateValueConfig(curve, 'v0', 'Start Point'),
generateValueConfig(curve, 'v1', 'Control Point 1'),
generateValueConfig(curve, 'v2', 'Control Point 2'),
generateValueConfig(curve, 'v3', 'End Point'),
] as UiObjectConfig['children'],
}
curveUiConfigs.QuadraticBezierCurve = {
ctor: QuadraticBezierCurve,
vals: {start: 'v0', end: 'v2', control1: 'v1'},
getUiConfig: (curve: QuadraticBezierCurve | QuadraticBezierCurve3) => [
generateValueConfig(curve, 'v0', 'Start Point'),
generateValueConfig(curve, 'v1', 'Control Point'),
generateValueConfig(curve, 'v2', 'End Point'),
] as UiObjectConfig['children'],
}
curveUiConfigs.EllipseCurve = {
ctor: EllipseCurve,
vals: {centerX: 'aX', centerY: 'aY', radiusX: 'xRadius', radiusY: 'yRadius', startAngle: 'aStartAngle', endAngle: 'aEndAngle', clockwise: 'aClockwise', rotation: 'aRotation'},
getUiConfig: (curve: EllipseCurve) => [
generateValueConfig(curve, 'aX', 'X Center'),
generateValueConfig(curve, 'aY', 'Y Center'),
generateValueConfig(curve, 'xRadius', 'X Radius'),
generateValueConfig(curve, 'yRadius', 'Y Radius'),
generateValueConfig(curve, 'aStartAngle', 'Start Angle'),
generateValueConfig(curve, 'aEndAngle', 'End Angle'),
generateValueConfig(curve, 'aClockwise', 'Clockwise'),
generateValueConfig(curve, 'aRotation', 'Rotation'),
] as UiObjectConfig['children'],
}
curveUiConfigs.ArcCurve = {
ctor: ArcCurve,
vals: {centerX: 'aX', centerY: 'aY', radiusX: 'xRadius', radiusY: 'yRadius', startAngle: 'aStartAngle', endAngle: 'aEndAngle', clockwise: 'aClockwise'},
getUiConfig: (curve: ArcCurve) => {
const radiusConfig = generateValueConfig(curve, 'xRadius', 'Radius')
if (radiusConfig) {
const origOnChange = radiusConfig.onChange as ((...args: any[]) => void) | undefined
radiusConfig.onChange = (...args: any[]) => {
curve.yRadius = curve.xRadius // keep circle (ArcCurve = equal radii)
origOnChange?.(...args)
}
}
return [
generateValueConfig(curve, 'aX', 'X Center'),
generateValueConfig(curve, 'aY', 'Y Center'),
radiusConfig,
generateValueConfig(curve, 'aStartAngle', 'Start Angle'),
generateValueConfig(curve, 'aEndAngle', 'End Angle'),
generateValueConfig(curve, 'aClockwise', 'Clockwise'),
] as UiObjectConfig['children']
},
}
curveUiConfigs.SplineCurve = {
ctor: SplineCurve,
vals: {points: 'points'},
getUiConfig: (curve: SplineCurve) => [
() => curve.points.map((_, i) => generateValueConfig(curve.points, i.toString(), `Point ${i}`)),
{
type: 'button', label: 'Add Point',
value: () => {
const points = curve.points
const prev = points[points.length - 1] || new Vector2(0, 0)
const prev2 = points[points.length - 2] || new Vector2(1, 1)
points.push(prev.clone().add(prev.clone().sub(prev2)))
curve.points = points
curve.updateArcLengths()
;(curve as IUiConfigContainer).uiConfig?.uiRefresh?.(true, 'postFrame')
},
},
] as UiObjectConfig['children'],
}
// ── 3D Curves ──
curveUiConfigs.LineCurve3 = {
...curveUiConfigs.LineCurve,
ctor: LineCurve3,
}
curveUiConfigs.CubicBezierCurve3 = {
...curveUiConfigs.CubicBezierCurve,
ctor: CubicBezierCurve3,
}
curveUiConfigs.QuadraticBezierCurve3 = {
...curveUiConfigs.QuadraticBezierCurve,
ctor: QuadraticBezierCurve3,
}
curveUiConfigs.CatmullRomCurve3 = {
ctor: CatmullRomCurve3,
vals: {points: 'points', closed: 'closed', curveType: 'curveType', tension: 'tension'},
getUiConfig: (curve: CatmullRomCurve3) => [
() => curve.points.map((_, i) => generateValueConfig(curve.points, i.toString(), `Point ${i}`)),
{
type: 'button', label: 'Add Point',
value: () => {
const points = curve.points
const prev = points[points.length - 1] || new Vector3(0, 0, 0)
const prev2 = points[points.length - 2] || new Vector3(1, 1, 0)
points.push(prev.clone().add(prev.clone().sub(prev2)))
curve.points = points
curve.updateArcLengths()
;(curve as IUiConfigContainer).uiConfig?.uiRefresh?.(true, 'postFrame')
},
},
generateValueConfig(curve, 'closed', 'Closed Loop'),
{
type: 'dropdown', property: [curve, 'curveType'], label: 'Curve Type',
children: ['centripetal', 'chordal', 'catmullrom'].map(value => ({label: value, value})),
},
generateValueConfig(curve, 'tension', 'Tension'),
] as UiObjectConfig['children'],
}
curveUiConfigs.EllipseCurve3D = {
ctor: EllipseCurve3D,
vals: {centerX: 'aX', centerY: 'aY', radiusX: 'xRadius', radiusY: 'yRadius', startAngle: 'aStartAngle', endAngle: 'aEndAngle', clockwise: 'aClockwise', rotation: 'aRotation'},
getUiConfig: curveUiConfigs.EllipseCurve.getUiConfig, // same props as EllipseCurve
}
// ── CurvePath (composite) ──
curveUiConfigs.CurvePath = {
ctor: CurvePath as any,
vals: {},
getUiConfig: (curve: CurvePath<Vector2> | CurvePath3) => [
() => curve.curves.map((c, i) => {
if ((c as IUiConfigContainer).uiConfig) return (c as IUiConfigContainer).uiConfig
const type = c.type
const uic = curveUiConfigs[type]
if (uic) {
const children = uic.getUiConfig(c as any) || []
const config = {
type: 'folder',
label: type.replace(/([A-Z])/g, ' $1').trim(),
children: [
createCurveTypeDropdown(c as any, (curve1) => {
curve.curves[i] = curve1 as any
curve.updateArcLengths()
;(curve as IUiConfigContainer).uiConfig?.uiRefresh?.(true, 'postFrame')
}),
...children,
{
type: 'button', label: 'Remove Curve',
value: () => {
curve.curves.splice(i, 1)
curve.updateArcLengths()
;(curve as IUiConfigContainer).uiConfig?.uiRefresh?.(true, 'postFrame')
return true
},
},
],
} as UiObjectConfig
;(c as IUiConfigContainer).uiConfig = config
return config
}
return undefined
}),
{
type: 'button', label: 'Add Curve',
value: () => {
const newCurve = curve.type.endsWith('3')
? new LineCurve3(new Vector3(0, 0, 0), new Vector3(1, 1, 0))
: new LineCurve(new Vector2(0, 0), new Vector2(1, 1))
curve.add(newCurve as any)
curve.updateArcLengths()
;(curve as IUiConfigContainer).uiConfig?.uiRefresh?.(true, 'postFrame')
return true
},
},
] as UiObjectConfig['children'],
}
curveUiConfigs.CurvePath3 = {
...curveUiConfigs.CurvePath,
ctor: CurvePath3 as any,
}
// ── Public API ──
/**
* Create a curve type dropdown UI config for selecting between curve types.
* @param curve Current curve instance
* @param configs Curve UI config registry to use (defaults to full registry)
* @param onTypeChange Callback when curve type is changed
*/
export function createCurveTypeDropdown(
curve: Curve<Vector2 | Vector3>,
onTypeChange: (newCurve: Curve<any>) => void,
configs: Record<string, CurveUiEntry> = curveUiConfigs,
): UiObjectConfig {
return {
type: 'dropdown',
label: 'Curve Type',
children: Object.keys(configs).map(key => ({
label: key.replace(/([A-Z])/g, ' $1').trim(),
value: key,
})),
getValue: () => curve ? (curve as any).type || 'LineCurve3' : 'LineCurve3',
setValue: (v1: string) => {
const oldSettings = (curve as any).type ? configs[(curve as any).type] : null
const settings = configs[v1]
if (!(curve && settings && (curve as any).type !== v1)) return true
const ctor = settings.ctor
if (!ctor) return false
const newCurve = new ctor()
// Initialize empty point arrays for spline/catmull-rom types
if (Array.isArray((newCurve as SplineCurve).points) && (newCurve as SplineCurve).points.length === 0) {
const p = newCurve.type.endsWith('3') ? new Vector3() : new Vector2()
const p2 = newCurve.type.endsWith('3') ? new Vector3(1, 1, 1) : new Vector2(1, 1)
;(newCurve as any).points.push(p, p2)
}
// Initialize empty curves array for CurvePath types
if (Array.isArray((newCurve as CurvePath<any>).curves) && (newCurve as CurvePath<any>).curves.length === 0) {
const c = newCurve.type.endsWith('3')
? new LineCurve3(new Vector3(0, 0, 0), new Vector3(1, 1, 0))
: new LineCurve(new Vector2(0, 0), new Vector2(1, 1))
;(newCurve as any).curves.push(c)
}
// Copy compatible properties from old curve to new one
if (oldSettings) {
const propsC1 = Object.entries(oldSettings.vals)
.map(([k, v]) => [k, (curve as any)[v], settings.vals[k]])
.filter(([_, v, k2]) => v !== undefined && v !== null && !!k2)
for (const [, v, k2] of propsC1) {
const prev = (newCurve as any)[k2]
if (prev === undefined) continue
if (typeof prev === 'object' && typeof prev.copy === 'function')
(newCurve as any)[k2].copy(v)
else if (Array.isArray(prev) && Array.isArray(v))
(newCurve as any)[k2] = [...v]
else if (typeof prev === 'object' && typeof v === 'object' && v !== null)
(newCurve as any)[k2] = {...v}
else
(newCurve as any)[k2] = v
}
}
onTypeChange(newCurve)
return true
},
}
}
/**
* Create UI config entries for editing the properties of a curve.
*/
export function createCurvePropertyUi(curve: Curve<Vector2 | Vector3>, configs: Record<string, CurveUiEntry> = curveUiConfigs): UiObjectConfig[] {
const type = (curve as any).type as string
const entry = configs[type]
if (!entry) return []
const folder: UiObjectConfig = {
type: 'folder',
label: type.replace(/([A-Z])/g, ' $1').trim(),
children: entry.getUiConfig(curve as any) || [],
}
;(curve as IUiConfigContainer).uiConfig = folder
return [folder]
}