frame.akima
Version:
A package for Akima interpolation
332 lines (331 loc) • 13.9 kB
JavaScript
import { Akima } from './Akima';
import { CatmullRom } from './CR';
import { AkimaPoint } from './AkimaPoint';
export function assertEquidistance(points, numDigits = 3) {
// prüfe, ob die aus dem Spline erzeugten Punkte gleichmäßig verteilt sind
let maxDelta = 0;
for (let i = 0; i < points.length; i += 1) {
const currentAngle = (Math.PI * 2 * i) / points.length;
const xPoint = points[i];
if (i === 0 && xPoint.Y !== 0) {
throw new Error('First point Y should be 0');
}
// console.log(
// `xPoint: [${xPoint.X}, ${xPoint.Y}] -< currentAngle: ${currentAngle}`
// );
const angle = xPoint.relAtan(new AkimaPoint(0, 0));
const factor = 1 / Math.pow(10, numDigits);
const delta = Math.abs(angle - currentAngle);
maxDelta = Math.max(maxDelta, delta);
if (delta > factor) {
throw new Error(`Point [${i}]: [${round(xPoint.X, numDigits)}, ${round(xPoint.Y, numDigits)}] -> angle: ${round(angle, numDigits)} (expected: ${round(currentAngle, numDigits)})`);
}
// expect(angle, `angle[${i}]`).toBeCloseTo(currentAngle, factor);
}
return maxDelta;
}
export function printCrucialPoints(getValFromAngle, value) {
if (getValFromAngle) {
const radius = getValFromAngle(value);
if (radius !== null) {
const cos = Math.cos(value);
const sin = Math.sin(value);
const x = Math.round(radius * cos * 1000) / 1000;
const y = Math.round(radius * sin * 1000) / 1000;
console.log(`[${value / Math.PI} * π] x: ${x}, y: ${y}`);
}
}
}
/**
* Calculates the maximum and average distances between a set of points and their closest points
* on a curve defined by the provided Akima interpolation function, with an optional offset.
* Optionally prints detailed information about each point's closest match.
*
* @param pts - Array of points to measure distances from.
* @param akimaFunc - Akima interpolation function that returns the Y value for a given X.
* @param offset - Offset to apply to each point before distance calculation.
* @param print - If true, logs detailed information about each point's closest match.
* @returns An object containing the maximum and average distances found.
*/
export function printDistances(pts, akima, offset, print = false) {
let maxDistance = 0;
let averageDistance = 0;
for (let i = 0; i < pts.length; i += 1) {
const { index, distance } = findClosestPoint(pts[i], akima, offset);
maxDistance = Math.max(maxDistance, distance);
averageDistance += distance;
const pt = getPointFromAkima(index, akima);
if (print) {
console.log(`printDistances: found index: ${Math.round(index * 1000) / 1000} for point ${i} = [${Math.round(pt.X * 1000) / 1000}, ${Math.round(pt.Y * 1000) / 1000}]: distance: ${Math.round(distance * 1000) / 1000}`);
}
}
averageDistance /= pts.length;
return { maxDistance, averageDistance };
}
/**
* Verifies that the radii computed by the provided Akima interpolation function
* closely match the actual radii of the given points from the origin.
*
* Iterates through each point in the `expectedPoints` array, calculates its angle
* relative to the `universeCenter` using the `relAtan` method, and computes its
* Euclidean distance from the origin. It then evaluates the Akima function at the
* calculated angle and asserts that the returned radius is close to the actual radius,
* within two decimal places.
*
* @param expectedPoints - An array of `Point` objects representing the expected points on the circle.
* @param akimaFunc - A function that takes an angle (in radians) and returns the interpolated radius.
*/
export function checkPoints(expectedPoints, akima, addedCenter, print = false, threshold = 0.1) {
if (print) {
console.log(`addedCenter: [${addedCenter.X}, ${addedCenter.Y}]`);
}
let maxDelta = 0;
for (let iloop = 0; iloop < expectedPoints.length; iloop += 1) {
// get the current point and ...
const currentPoint = expectedPoints[iloop];
// ... calculate its relative position to the center
const relativePoint = new AkimaPoint(currentPoint.X - addedCenter.X, currentPoint.Y - addedCenter.Y);
// get the angle and the radian of the relative point
const relativeAngle = relativePoint.relAtan(new AkimaPoint(0, 0));
const relativeRadian = relativePoint.Radius;
// console.log(
// `checkPoints[${iloop}]: [${round(relativePoint.X)}, ${round(
// relativePoint.Y
// )}] -> angle: ${round(relativeAngle)}`
// );
// console.log(`relativeRadius: ${relativeRadius}`);
const relativeRadianFromAkima = Number(akima.Spline(relativeAngle));
const relativeXFromAkima = Math.round(Math.cos(relativeAngle) *
relativeRadianFromAkima /*+ addedCenter.X*/ *
1000) / 1000;
const relativeYFromAkima = Math.round(Math.sin(relativeAngle) *
relativeRadianFromAkima /*+ addedCenter.Y*/ *
1000) / 1000;
const delta = Math.abs(relativeRadianFromAkima - relativeRadian);
const RED = '\x1b[31m';
const ORANGE = '\x1b[38;5;208m';
const GREEN = '\x1b[32m';
const RESET = '\x1b[0m';
if (print) {
const deltaString = `${delta > 0.1 ? RED : GREEN}${round(delta)
.toString()
.padStart(5, ' ')}${RESET}`;
console.log(`checkPoints.point[${iloop
.toString()
.padStart(2, ' ')}] = [${currentPoint.X.toString().padStart(4, ' ')}, ${currentPoint.Y.toString().padStart(4, ' ')}] -> relative: [${round(relativePoint.X)
.toString()
.padStart(8, ' ')}, ${round(relativePoint.Y)
.toString()
.padStart(8, ' ')}] <=== ${deltaString} ===> [${relativeXFromAkima
.toString()
.padStart(8, ' ')}, ${relativeYFromAkima
.toString()
.padStart(8, ' ')}] from Akima`);
}
maxDelta = Math.max(maxDelta, delta);
if (delta > threshold) {
throw new Error(`Difference at index ${iloop}: [${relativePoint.X}, ${relativePoint.Y}] -> ${relativeRadian}` +
`\n` +
`better use [${relativeXFromAkima}, ${relativeYFromAkima}] instead of [${currentPoint.X}, ${currentPoint.Y}]` +
`\n`);
}
}
return maxDelta;
}
export function getCenter(points, print = false) {
let xMin = Number.POSITIVE_INFINITY;
let xMax = Number.NEGATIVE_INFINITY;
let yMin = Number.POSITIVE_INFINITY;
let yMax = Number.NEGATIVE_INFINITY;
for (let i = 0; i < points.length; i += 1) {
const p = points[i];
if (p.X < xMin)
xMin = p.X;
if (p.X > xMax)
xMax = p.X;
if (p.Y < yMin)
yMin = p.Y;
if (p.Y > yMax)
yMax = p.Y;
}
if (print) {
console.log(`[xMin, xMax]: [${Math.round(xMin * 1000) / 1000}, ${Math.round(xMax * 1000) / 1000}] --> extension X: ${Math.round((xMax - xMin) * 1000) / 1000}`);
console.log(`[yMin, yMax]: [${Math.round(yMin * 1000) / 1000}, ${Math.round(yMax * 1000) / 1000}] --> extension Y: ${Math.round((yMax - yMin) * 1000) / 1000}`);
}
return new AkimaPoint((xMin + xMax) / 2, (yMin + yMax) / 2);
}
export function getXminIndex(points) {
let xMin = Number.POSITIVE_INFINITY;
let xMinIndex = -1;
for (let i = 0; i < points.length; i += 1) {
const p = points[i];
if (p.X < xMin) {
xMin = p.X;
xMinIndex = i;
}
}
return xMinIndex;
}
export function getYMinIndex(points) {
let yMin = Number.POSITIVE_INFINITY;
let yMinIndex = -1;
for (let i = 0; i < points.length; i += 1) {
const p = points[i];
if (p.Y < yMin) {
yMin = p.Y;
yMinIndex = i;
}
}
return yMinIndex;
}
export function findClosestPoint(point, akima, offset) {
// console.log(`offset: [${offset.X}, ${offset.Y}]`);
let currentDistance = Number.POSITIVE_INFINITY;
let index = -1;
for (let angle = 0; angle <= Math.PI * 2; angle += 0.001) {
const testPoint = getPointFromAkima(angle, akima);
const offsetTestPoint = new AkimaPoint(testPoint.X + offset.X, testPoint.Y + offset.Y);
const deltaX = Math.abs(offsetTestPoint.X - point.X);
const deltaY = Math.abs(offsetTestPoint.Y - point.Y);
const distance = Math.hypot(deltaX, deltaY);
if (distance < currentDistance) {
currentDistance = distance;
index = angle;
}
}
if (index === -1) {
throw new Error('No close point found');
}
return { index, distance: currentDistance };
}
export function getPointFromAkima(angle, akima) {
const radius = akima.Spline(angle);
const x = radius * Math.cos(angle);
const y = radius * Math.sin(angle);
return new AkimaPoint(x, y);
}
export function findPointWithYValue(yValue, akimaFunc, from, to, maxIterations = 1000) {
if (maxIterations <= 0) {
throw new Error('Maximum iterations exceeded');
}
const currentY = akimaFunc(yValue);
if (currentY !== null && currentY !== undefined) {
if (Math.abs(currentY - yValue) < 0.001) {
return yValue;
}
else if (currentY < yValue) {
return findPointWithYValue(yValue, akimaFunc, yValue, to, maxIterations - 1);
}
else {
return findPointWithYValue(yValue, akimaFunc, from, yValue, maxIterations - 1);
}
}
else {
throw new Error('No value from akima function');
}
}
export function transform(points, transform) {
// console.log(`transforming points by: [${transform.X}, ${transform.Y}]`);
return points.map((pt) => new AkimaPoint(pt.X - transform.X, pt.Y - transform.Y));
}
export function createJSON(points) {
console.log(`\n=== Akima Spline JSON ===`);
console.log(`{`);
console.log(` "points": [`);
for (const p of points) {
const x = Math.round(p.X);
const y = Math.round(p.Y);
console.log(` { "x": ${x}, "y": ${y} },`);
}
console.log(` ],`);
console.log(` "universeCenter": { "x": 300, "y": 200 }`);
console.log(`}`);
}
// export function createHauk(
// nrPoints: number,
// getValueFromAngle: (angle: number) => number | null
// ): void {
// if (getValueFromAngle) {
// console.log(`\n=== Akima Spline Hauk ===`);
// for (let loop = 0; loop < nrPoints; loop += 1) {
// const angle = (Math.PI * 2 * loop) / nrPoints;
// const radiusFromAkima = Number(getValueFromAngle(angle));
// const x = Math.round(radiusFromAkima * Math.cos(angle)) / 10;
// const y = Math.round(radiusFromAkima * Math.sin(angle)) / 10;
// console.log(
// ` ${x.toLocaleString('de-DE')} / ${y.toLocaleString('de-DE')}`
// );
// }
// }
// }
export function createHauk(points) {
console.log(`\n=== Akima Spline Hauk ===`);
for (let loop = 0; loop < points.length; loop += 1) {
const p = points[loop];
const angle = (Math.PI * 2 * loop) / points.length;
const radius = Math.hypot(p.X, p.Y);
const x = Math.round(radius * Math.cos(angle)) / 10;
const y = Math.round(radius * Math.sin(angle)) / 10;
console.log(` ${x.toLocaleString('de-DE')} / ${y.toLocaleString('de-DE')}`);
}
}
// export function createRT1(
// nrPoints: number,
// getValueFromAngle: (angle: number) => number | null
// ): void {
// if (getValueFromAngle) {
// let rt1 = '';
// console.log(`\n=== Akima Spline RT1 ===`);
// for (let loop = 0; loop < nrPoints; loop += 1) {
// const angle = (Math.PI * 2 * loop) / nrPoints;
// const radius = Number(getValueFromAngle(angle));
// rt1 += Math.round(radius * 10)
// .toString()
// .padStart(4, '0');
// }
// console.log(`rt1: ${rt1}`);
// }
// }
export function createRT1(points) {
let rt1 = '';
for (let i = 0; i < points.length; i += 1) {
const p = points[i];
const radius = Math.round(Math.hypot(p.X, p.Y) * 10);
rt1 += radius.toString().padStart(4, '0');
}
return rt1;
}
export function round(num, numDigits = 3) {
const factor = 10 ** numDigits;
return Math.round(num * factor) / factor;
}
export function createEquidistantPoints(points, nrPoints, print = false) {
if (points.length < 4) {
throw new Error('nrPoints must be at least 4');
}
// checkCenter(points, true);
const catmull = new CatmullRom(points);
const catmullPoints = [];
for (let loop = 0; loop < nrPoints; loop++) {
const angle = (loop * 2 * Math.PI) / nrPoints;
const point = catmull.atPolar(angle);
if (print) {
console.log(`createEquidistantPoints.point[${loop}]: [${point.X}, ${point.Y}], angle: ${round(angle)}`);
}
catmullPoints.push(point);
// createHauk(catmullPoints);
}
const { points: points128, akima, center, } = Akima.createPointsByAkima(catmullPoints, 128, false);
return { points: points128, akima, center };
}
function checkCenter(points, print = false) {
const center = getCenter(points, print);
if (print) {
console.log(`checkCenter: center at [${center.X}, ${center.Y}]`);
}
for (let loop = 0; loop < points.length; loop++) {
const p = points[loop];
const relativePoint = new AkimaPoint(p.X - center.X, p.Y - center.Y);
console.log(`checkCenter.point[${loop}]: [${relativePoint.X}, ${relativePoint.Y}] -> relAtan: ${round(relativePoint.relAtan(new AkimaPoint(0, 0)))}`);
}
}