rcnn/web/src/coaster/bezierUtils.ts
2026-04-21 16:33:15 +02:00

137 lines
4.9 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import * as Cesium from 'cesium'
export interface AnchorPoint {
id: string
position: Cesium.Cartesian3 // base position on terrain
heightOffset: number // meters above terrain surface
}
// ── helpers ──────────────────────────────────────────────────────────────────
function v3add(a: Cesium.Cartesian3, b: Cesium.Cartesian3): Cesium.Cartesian3 {
return Cesium.Cartesian3.add(a, b, new Cesium.Cartesian3())
}
function v3sub(a: Cesium.Cartesian3, b: Cesium.Cartesian3): Cesium.Cartesian3 {
return Cesium.Cartesian3.subtract(a, b, new Cesium.Cartesian3())
}
function v3scale(v: Cesium.Cartesian3, s: number): Cesium.Cartesian3 {
return Cesium.Cartesian3.multiplyByScalar(v, s, new Cesium.Cartesian3())
}
function v3norm(v: Cesium.Cartesian3): Cesium.Cartesian3 {
return Cesium.Cartesian3.normalize(v, new Cesium.Cartesian3())
}
function v3cross(a: Cesium.Cartesian3, b: Cesium.Cartesian3): Cesium.Cartesian3 {
return Cesium.Cartesian3.cross(a, b, new Cesium.Cartesian3())
}
/** Return the effective 3-D position of an anchor including its height offset. */
export function effectivePosition(anchor: AnchorPoint): Cesium.Cartesian3 {
if (anchor.heightOffset === 0) return anchor.position.clone()
const up = v3norm(anchor.position)
return v3add(anchor.position, v3scale(up, anchor.heightOffset))
}
// ── Catmull-Rom → cubic Bézier ───────────────────────────────────────────────
interface Segment {
p0: Cesium.Cartesian3
c1: Cesium.Cartesian3
c2: Cesium.Cartesian3
p1: Cesium.Cartesian3
}
/**
* Convert anchor points to cubic Bézier segments via Catmull-Rom.
* The curve passes through every anchor point with C1 continuity.
*/
export function computeSegments(anchors: AnchorPoint[]): Segment[] {
if (anchors.length < 2) return []
const pts = anchors.map(effectivePosition)
const n = pts.length
// phantom end-points so the curve starts/ends at the first/last anchor
const ext = [pts[0], ...pts, pts[n - 1]]
const segments: Segment[] = []
for (let i = 0; i < n - 1; i++) {
const pm1 = ext[i]
const p0 = ext[i + 1]
const p1 = ext[i + 2]
const p2 = ext[i + 3]
// Catmull-Rom tangent handles converted to cubic Bézier control points
const c1 = v3add(p0, v3scale(v3sub(p1, pm1), 1 / 6))
const c2 = v3add(p1, v3scale(v3sub(p0, p2), 1 / 6))
segments.push({ p0, c1, c2, p1 })
}
return segments
}
function evalBezier(seg: Segment, t: number): Cesium.Cartesian3 {
const { p0, c1, c2, p1 } = seg
const mt = 1 - t
return new Cesium.Cartesian3(
mt ** 3 * p0.x + 3 * mt ** 2 * t * c1.x + 3 * mt * t ** 2 * c2.x + t ** 3 * p1.x,
mt ** 3 * p0.y + 3 * mt ** 2 * t * c1.y + 3 * mt * t ** 2 * c2.y + t ** 3 * p1.y,
mt ** 3 * p0.z + 3 * mt ** 2 * t * c1.z + 3 * mt * t ** 2 * c2.z + t ** 3 * p1.z,
)
}
/** Sample the full spline as a polyline (all segments concatenated). */
export function samplePath(
anchors: AnchorPoint[],
samplesPerSegment = 40,
): Cesium.Cartesian3[] {
const segs = computeSegments(anchors)
if (segs.length === 0) return anchors.map(effectivePosition)
const pts: Cesium.Cartesian3[] = []
segs.forEach((seg, i) => {
const from = i === 0 ? 0 : 1
for (let s = from; s <= samplesPerSegment; s++) {
pts.push(evalBezier(seg, s / samplesPerSegment))
}
})
return pts
}
// ── Rail geometry ─────────────────────────────────────────────────────────────
export interface RailPositions {
left: Cesium.Cartesian3[]
right: Cesium.Cartesian3[]
}
/**
* Given a centre-line path compute parallel left/right rail positions.
* @param gauge distance between rails in metres (default 2.5 for visual clarity)
*/
export function computeRails(
path: Cesium.Cartesian3[],
gauge = 2.5,
): RailPositions {
const left: Cesium.Cartesian3[] = []
const right: Cesium.Cartesian3[] = []
const half = gauge / 2
const n = path.length
for (let i = 0; i < n; i++) {
const pt = path[i]
// Finite-difference tangent along the curve
let tangent: Cesium.Cartesian3
if (i === 0) tangent = v3sub(path[1], path[0])
else if (i === n - 1) tangent = v3sub(path[n - 1], path[n - 2])
else tangent = v3sub(path[i + 1], path[i - 1])
tangent = v3norm(tangent)
// Local up = radially outward from Earth centre
const up = v3norm(pt)
// Track-right = tangent × up (right-hand rule → points right when facing forward)
const rightDir = v3norm(v3cross(tangent, up))
left.push(v3add(pt, v3scale(rightDir, -half)))
right.push(v3add(pt, v3scale(rightDir, half)))
}
return { left, right }
}