UNPKG

frame.akima

Version:

A package for Akima interpolation

332 lines (331 loc) 13.9 kB
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)))}`); } }