Matou-Garou / convex /util /geometry.ts
Jofthomas's picture
Jofthomas HF staff
bulk
ce8b18b
raw
history blame
4.07 kB
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;
}