Spaces:
Sleeping
Sleeping
| import { Path, PathComponent, Point, Vector, packPathComponent, queryPath } from './types'; | |
| export function distance(p0: Point, p1: Point): number { | |
| const dx = p0.x - p1.x; | |
| const dy = p0.y - p1.y; | |
| return Math.sqrt(dx * dx + dy * dy); | |
| } | |
| export function pointsEqual(p0: Point, p1: Point): boolean { | |
| return p0.x == p1.x && p0.y == p1.y; | |
| } | |
| export function manhattanDistance(p0: Point, p1: Point) { | |
| return Math.abs(p0.x - p1.x) + Math.abs(p0.y - p1.y); | |
| } | |
| export function pathOverlaps(path: Path, time: number): boolean { | |
| if (path.length < 2) { | |
| throw new Error(`Invalid path: ${JSON.stringify(path)}`); | |
| } | |
| const start = queryPath(path, 0); | |
| const end = queryPath(path, path.length - 1); | |
| return start.t <= time && time <= end.t; | |
| } | |
| export function pathPosition( | |
| path: Path, | |
| time: number, | |
| ): { position: Point; facing: Vector; velocity: number } { | |
| if (path.length < 2) { | |
| throw new Error(`Invalid path: ${JSON.stringify(path)}`); | |
| } | |
| const first = queryPath(path, 0); | |
| if (time < first.t) { | |
| return { position: first.position, facing: first.facing, velocity: 0 }; | |
| } | |
| const last = queryPath(path, path.length - 1); | |
| if (last.t < time) { | |
| return { position: last.position, facing: last.facing, velocity: 0 }; | |
| } | |
| for (let i = 0; i < path.length - 1; i++) { | |
| const segmentStart = queryPath(path, i); | |
| const segmentEnd = queryPath(path, i + 1); | |
| if (segmentStart.t <= time && time <= segmentEnd.t) { | |
| const interp = (time - segmentStart.t) / (segmentEnd.t - segmentStart.t); | |
| return { | |
| position: { | |
| x: segmentStart.position.x + interp * (segmentEnd.position.x - segmentStart.position.x), | |
| y: segmentStart.position.y + interp * (segmentEnd.position.y - segmentStart.position.y), | |
| }, | |
| facing: segmentStart.facing, | |
| velocity: | |
| distance(segmentStart.position, segmentEnd.position) / (segmentEnd.t - segmentStart.t), | |
| }; | |
| } | |
| } | |
| throw new Error(`Timestamp checks not exhaustive?`); | |
| } | |
| export const EPSILON = 0.0001; | |
| export function vector(p0: Point, p1: Point): Vector { | |
| const dx = p1.x - p0.x; | |
| const dy = p1.y - p0.y; | |
| return { dx, dy }; | |
| } | |
| export function vectorLength(vector: Vector): number { | |
| return Math.sqrt(vector.dx * vector.dx + vector.dy * vector.dy); | |
| } | |
| export function normalize(vector: Vector): Vector | null { | |
| const len = vectorLength(vector); | |
| if (len < EPSILON) { | |
| return null; | |
| } | |
| const { dx, dy } = vector; | |
| return { | |
| dx: dx / len, | |
| dy: dy / len, | |
| }; | |
| } | |
| export function orientationDegrees(vector: Vector): number { | |
| if (Math.sqrt(vector.dx * vector.dx + vector.dy * vector.dy) < EPSILON) { | |
| throw new Error(`Can't compute the orientation of too small vector ${JSON.stringify(vector)}`); | |
| } | |
| const twoPi = 2 * Math.PI; | |
| const radians = (Math.atan2(vector.dy, vector.dx) + twoPi) % twoPi; | |
| return (radians / twoPi) * 360; | |
| } | |
| export function compressPath(densePath: PathComponent[]): Path { | |
| const packed = densePath.map(packPathComponent); | |
| if (densePath.length <= 2) { | |
| return densePath.map(packPathComponent); | |
| } | |
| const out = [packPathComponent(densePath[0])]; | |
| let last = densePath[0]; | |
| let candidate; | |
| for (const point of densePath.slice(1)) { | |
| if (!candidate) { | |
| candidate = point; | |
| continue; | |
| } | |
| // We can skip `candidate` if it interpolates cleanly between | |
| // `last` and `point`. | |
| const { position, facing } = pathPosition( | |
| [packPathComponent(last), packPathComponent(point)], | |
| candidate.t, | |
| ); | |
| const positionCloseEnough = distance(position, candidate.position) < EPSILON; | |
| const facingDifference = { | |
| dx: facing.dx - candidate.facing.dx, | |
| dy: facing.dy - candidate.facing.dy, | |
| }; | |
| const facingCloseEnough = vectorLength(facingDifference) < EPSILON; | |
| if (positionCloseEnough && facingCloseEnough) { | |
| candidate = point; | |
| continue; | |
| } | |
| out.push(packPathComponent(candidate)); | |
| last = candidate; | |
| candidate = point; | |
| } | |
| if (candidate) { | |
| out.push(packPathComponent(candidate)); | |
| } | |
| return out; | |
| } | |