import { assertExists } from '@blockeditor/global/utils'; import { CURVETIME_EPSILON, isZero } from './math-utils.js'; import type { PointLocation } from './point-location.js'; import { type IVec, Vec } from './vec.js'; // control coords are not relative to start or end export type BezierCurveParameters = [ start: IVec, control1: IVec, control2: IVec, end: IVec, ]; function evaluate( v: BezierCurveParameters, t: number, type: number, normalized: boolean ): IVec | null { if (t == null || t < 0 || t > 1) return null; const x0 = v[0][0], y0 = v[0][1], x3 = v[3][0], y3 = v[3][1]; let x1 = v[1][0], y1 = v[1][1], x2 = v[2][0], y2 = v[2][1]; if (isZero(x1 - x0) && isZero(y1 - y0)) { x1 = x0; y1 = y0; } if (isZero(x2 - x3) && isZero(y2 - y3)) { x2 = x3; y2 = y3; } // Calculate the polynomial coefficients. const cx = 3 * (x1 - x0), bx = 3 * (x2 - x1) - cx, ax = x3 - x0 - cx - bx, cy = 3 * (y1 - y0), by = 3 * (y2 - y1) - cy, ay = y3 - y0 - cy - by; let x, y; if (type === 0) { // type === 0: getPoint() x = t === 0 ? x0 : t === 1 ? x3 : ((ax * t + bx) * t + cx) * t + x0; y = t === 0 ? y0 : t === 1 ? y3 : ((ay * t + by) * t + cy) * t + y0; } else { // type === 1: getTangent() // type === 2: getNormal() // type === 3: getCurvature() const tMin = CURVETIME_EPSILON, tMax = 1 - tMin; if (t < tMin) { x = cx; y = cy; } else if (t > tMax) { x = 3 * (x3 - x2); y = 3 * (y3 - y2); } else { x = (3 * ax * t + 2 * bx) * t + cx; y = (3 * ay * t + 2 * by) * t + cy; } if (normalized) { if (x === 0 && y === 0 && (t < tMin || t > tMax)) { x = x2 - x1; y = y2 - y1; } const len = Math.sqrt(x * x + y * y); if (len) { x /= len; y /= len; } } if (type === 3) { const x2 = 6 * ax * t + 2 * bx, y2 = 6 * ay * t + 2 * by, d = Math.pow(x * x + y * y, 3 / 2); x = d !== 0 ? (x * y2 - y * x2) / d : 0; y = 0; } } return type === 2 ? [y, -x] : [x, y]; } export function getBezierPoint(values: BezierCurveParameters, t: number) { return evaluate(values, t, 0, false); } export function getBezierTangent(values: BezierCurveParameters, t: number) { return evaluate(values, t, 1, true); } export function getBezierNormal(values: BezierCurveParameters, t: number) { return evaluate(values, t, 2, true); } export function getBezierCurvature(values: BezierCurveParameters, t: number) { return evaluate(values, t, 3, false)?.[0]; } export function getBezierNearestTime( values: BezierCurveParameters, point: IVec ) { const count = 100; let minDist = Infinity, minT = 0; function refine(t: number) { if (t >= 0 && t <= 1) { const tmpPoint = getBezierPoint(values, t); assertExists(tmpPoint); const dist = Vec.dist2(point, tmpPoint); if (dist < minDist) { minDist = dist; minT = t; return true; } } return false; } for (let i = 0; i <= count; i++) refine(i / count); let step = 1 / (count * 2); while (step > CURVETIME_EPSILON) { if (!refine(minT - step) && !refine(minT + step)) step /= 2; } return minT; } export function getBezierNearestPoint( values: BezierCurveParameters, point: IVec ) { const t = getBezierNearestTime(values, point); const pointOnCurve = getBezierPoint(values, t); assertExists(pointOnCurve); return pointOnCurve; } export function getBezierParameters( points: PointLocation[] ): BezierCurveParameters { return [points[0], points[0].absOut, points[1].absIn, points[1]]; }