137 lines
4.9 KiB
TypeScript
137 lines
4.9 KiB
TypeScript
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 }
|
||
}
|