From 2137f2b8062fe6bc7d0b5a73d2aef9fb16570a7c Mon Sep 17 00:00:00 2001 From: Mark Tolmacs Date: Tue, 14 Jan 2025 18:34:05 +0100 Subject: [PATCH] Type refactor Signed-off-by: Mark Tolmacs --- excalidraw-app/components/DebugCanvas.tsx | 10 +- packages/excalidraw/shapes.tsx | 4 +- packages/excalidraw/visualdebug.ts | 6 +- packages/math/curve.ts | 229 ++-------------------- packages/math/types.ts | 30 +-- packages/utils/geometry/shape.ts | 48 +++-- 6 files changed, 64 insertions(+), 263 deletions(-) diff --git a/excalidraw-app/components/DebugCanvas.tsx b/excalidraw-app/components/DebugCanvas.tsx index eb429b089..0d4b2021a 100644 --- a/excalidraw-app/components/DebugCanvas.tsx +++ b/excalidraw-app/components/DebugCanvas.tsx @@ -12,10 +12,10 @@ import { TrashIcon, } from "../../packages/excalidraw/components/icons"; import { STORAGE_KEYS } from "../app_constants"; -import type { Arc, CubicBezier } from "../../packages/math"; +import type { Arc, Curve } from "../../packages/math"; import { isArc, - isBezier, + isCurve, isSegment, type GlobalPoint, type Segment, @@ -39,7 +39,7 @@ const renderLine = ( const renderCubicBezier = ( context: CanvasRenderingContext2D, zoom: number, - { start, control1, control2, end }: CubicBezier, + [start, control1, control2, end]: Curve, color: string, ) => { context.save(); @@ -113,11 +113,11 @@ const render = ( el.color, ); break; - case isBezier(el.data): + case isCurve(el.data): renderCubicBezier( context, appState.zoom.value, - el.data as CubicBezier, + el.data as Curve, el.color, ); break; diff --git a/packages/excalidraw/shapes.tsx b/packages/excalidraw/shapes.tsx index a556652dd..0c7010430 100644 --- a/packages/excalidraw/shapes.tsx +++ b/packages/excalidraw/shapes.tsx @@ -164,14 +164,14 @@ export const getElementShape = ( const [, , , , cx, cy] = getElementAbsoluteCoords(element, elementsMap); return shouldTestInside(element) - ? getClosedCurveShape( + ? getClosedCurveShape( element, roughShape, pointFrom(element.x, element.y), element.angle, pointFrom(cx, cy), ) - : getCurveShape( + : getCurveShape( roughShape, pointFrom(element.x, element.y), element.angle, diff --git a/packages/excalidraw/visualdebug.ts b/packages/excalidraw/visualdebug.ts index f64697ac2..f8d226ab8 100644 --- a/packages/excalidraw/visualdebug.ts +++ b/packages/excalidraw/visualdebug.ts @@ -1,4 +1,4 @@ -import type { Arc, CubicBezier, Segment } from "../math"; +import type { Arc, Curve, Segment } from "../math"; import { isSegment, segment, pointFrom, type GlobalPoint } from "../math"; import { isBounds } from "./element/typeChecks"; import type { Bounds } from "./element/types"; @@ -15,12 +15,12 @@ declare global { export type DebugElement = { color: string; - data: Segment | Arc | CubicBezier; + data: Segment | Arc | Curve; permanent: boolean; }; export const debugDrawCubicBezier = ( - c: CubicBezier, + c: Curve, opts?: { color?: string; permanent?: boolean; diff --git a/packages/math/curve.ts b/packages/math/curve.ts index 2afd47e6b..94c01168f 100644 --- a/packages/math/curve.ts +++ b/packages/math/curve.ts @@ -1,5 +1,5 @@ -import { isPoint, pointFrom, pointRotateRads } from "./point"; -import type { CubicBezier, Curve, GenericPoint, Radians } from "./types"; +import { isPoint, pointRotateRads } from "./point"; +import type { Curve, GenericPoint, Radians } from "./types"; /** * @@ -10,12 +10,12 @@ import type { CubicBezier, Curve, GenericPoint, Radians } from "./types"; * @returns */ export function curve( - a: Point, - b: Point, - c: Point, - d: Point, + start: Point, + control1: Point, + control2: Point, + end: Point, ) { - return [a, b, c, d] as Curve; + return [start, control1, control2, end] as Curve; } export const curveRotate = ( @@ -26,215 +26,16 @@ export const curveRotate = ( return curve.map((p) => pointRotateRads(p, origin, angle)); }; -/** - * - * @param pointsIn - * @param curveTightness - * @returns - */ -export function curveToBezier( - pointsIn: readonly Point[], - curveTightness = 0, -): Point[] { - const len = pointsIn.length; - if (len < 3) { - throw new Error("A curve must have at least three points."); - } - const out: Point[] = []; - if (len === 3) { - out.push( - pointFrom(pointsIn[0][0], pointsIn[0][1]), // Points need to be cloned - pointFrom(pointsIn[1][0], pointsIn[1][1]), // Points need to be cloned - pointFrom(pointsIn[2][0], pointsIn[2][1]), // Points need to be cloned - pointFrom(pointsIn[2][0], pointsIn[2][1]), // Points need to be cloned - ); - } else { - const points: Point[] = []; - points.push(pointsIn[0], pointsIn[0]); - for (let i = 1; i < pointsIn.length; i++) { - points.push(pointsIn[i]); - if (i === pointsIn.length - 1) { - points.push(pointsIn[i]); - } - } - const b: Point[] = []; - const s = 1 - curveTightness; - out.push(pointFrom(points[0][0], points[0][1])); - for (let i = 1; i + 2 < points.length; i++) { - const cachedVertArray = points[i]; - b[0] = pointFrom(cachedVertArray[0], cachedVertArray[1]); - b[1] = pointFrom( - cachedVertArray[0] + (s * points[i + 1][0] - s * points[i - 1][0]) / 6, - cachedVertArray[1] + (s * points[i + 1][1] - s * points[i - 1][1]) / 6, - ); - b[2] = pointFrom( - points[i + 1][0] + (s * points[i][0] - s * points[i + 2][0]) / 6, - points[i + 1][1] + (s * points[i][1] - s * points[i + 2][1]) / 6, - ); - b[3] = pointFrom(points[i + 1][0], points[i + 1][1]); - out.push(b[1], b[2], b[3]); - } - } - return out; -} - -/** - * - * @param t - * @param controlPoints - * @returns - */ -export const cubicBezierPoint = ( - t: number, - controlPoints: Curve, -): Point => { - const [p0, p1, p2, p3] = controlPoints; - - const x = - Math.pow(1 - t, 3) * p0[0] + - 3 * Math.pow(1 - t, 2) * t * p1[0] + - 3 * (1 - t) * Math.pow(t, 2) * p2[0] + - Math.pow(t, 3) * p3[0]; - - const y = - Math.pow(1 - t, 3) * p0[1] + - 3 * Math.pow(1 - t, 2) * t * p1[1] + - 3 * (1 - t) * Math.pow(t, 2) * p2[1] + - Math.pow(t, 3) * p3[1]; - - return pointFrom(x, y); -}; - -/** - * - * @param point - * @param controlPoints - * @returns - */ -export const cubicBezierDistance = ( - point: Point, - controlPoints: Curve, -) => { - // Calculate the closest point on the Bezier curve to the given point - const t = findClosestParameter(point, controlPoints); - - // Calculate the coordinates of the closest point on the curve - const [closestX, closestY] = cubicBezierPoint(t, controlPoints); - - // Calculate the distance between the given point and the closest point on the curve - const distance = Math.sqrt( - (point[0] - closestX) ** 2 + (point[1] - closestY) ** 2, - ); - - return distance; -}; - -const solveCubic = (a: number, b: number, c: number, d: number) => { - // This function solves the cubic equation ax^3 + bx^2 + cx + d = 0 - const roots: number[] = []; - - const discriminant = - 18 * a * b * c * d - - 4 * Math.pow(b, 3) * d + - Math.pow(b, 2) * Math.pow(c, 2) - - 4 * a * Math.pow(c, 3) - - 27 * Math.pow(a, 2) * Math.pow(d, 2); - - if (discriminant >= 0) { - const C = Math.cbrt((discriminant + Math.sqrt(discriminant)) / 2); - const D = Math.cbrt((discriminant - Math.sqrt(discriminant)) / 2); - - const root1 = (-b - C - D) / (3 * a); - const root2 = (-b + (C + D) / 2) / (3 * a); - const root3 = (-b + (C + D) / 2) / (3 * a); - - roots.push(root1, root2, root3); - } else { - const realPart = -b / (3 * a); - - const root1 = - 2 * Math.sqrt(-b / (3 * a)) * Math.cos(Math.acos(realPart) / 3); - const root2 = - 2 * - Math.sqrt(-b / (3 * a)) * - Math.cos((Math.acos(realPart) + 2 * Math.PI) / 3); - const root3 = - 2 * - Math.sqrt(-b / (3 * a)) * - Math.cos((Math.acos(realPart) + 4 * Math.PI) / 3); - - roots.push(root1, root2, root3); - } - - return roots; -}; - -const findClosestParameter = ( - point: Point, - controlPoints: Curve, -) => { - // This function finds the parameter t that minimizes the distance between the point - // and any point on the cubic Bezier curve. - - const [p0, p1, p2, p3] = controlPoints; - - // Use the direct formula to find the parameter t - const a = p3[0] - 3 * p2[0] + 3 * p1[0] - p0[0]; - const b = 3 * p2[0] - 6 * p1[0] + 3 * p0[0]; - const c = 3 * p1[0] - 3 * p0[0]; - const d = p0[0] - point[0]; - - const rootsX = solveCubic(a, b, c, d); - - // Do the same for the y-coordinate - const e = p3[1] - 3 * p2[1] + 3 * p1[1] - p0[1]; - const f = 3 * p2[1] - 6 * p1[1] + 3 * p0[1]; - const g = 3 * p1[1] - 3 * p0[1]; - const h = p0[1] - point[1]; - - const rootsY = solveCubic(e, f, g, h); - - // Select the real root that is between 0 and 1 (inclusive) - const validRootsX = rootsX.filter((root) => root >= 0 && root <= 1); - const validRootsY = rootsY.filter((root) => root >= 0 && root <= 1); - - if (validRootsX.length === 0 || validRootsY.length === 0) { - // No valid roots found, use the midpoint as a fallback - return 0.5; - } - - // Choose the parameter t that minimizes the distance - let minDistance = Infinity; - let closestT = 0; - - for (const rootX of validRootsX) { - for (const rootY of validRootsY) { - const distance = Math.sqrt( - (rootX - point[0]) ** 2 + (rootY - point[1]) ** 2, - ); - if (distance < minDistance) { - minDistance = distance; - closestT = (rootX + rootY) / 2; // Use the average for a smoother result - } - } - } - - return closestT; -}; - -export const isBezier = ( +export const isCurve = ( c: unknown, -): c is CubicBezier => { +): c is Curve => { return ( c != null && - typeof c === "object" && - Object.hasOwn(c, "start") && - Object.hasOwn(c, "end") && - Object.hasOwn(c, "control1") && - Object.hasOwn(c, "control2") && - isPoint((c as CubicBezier).start) && - isPoint((c as CubicBezier).end) && - isPoint((c as CubicBezier).control1) && - isPoint((c as CubicBezier).control2) + Array.isArray(c) && + c.length === 4 && + isPoint((c as Curve)[0]) && + isPoint((c as Curve)[1]) && + isPoint((c as Curve)[2]) && + isPoint((c as Curve)[3]) ); }; diff --git a/packages/math/types.ts b/packages/math/types.ts index 93b59378d..7ceff7079 100644 --- a/packages/math/types.ts +++ b/packages/math/types.ts @@ -101,9 +101,22 @@ export type Polygon = Point[] & { }; /** - * Cubic bezier curve with four control points - */ -export type Curve = [Point, Point, Point, Point] & { + * Cubic bezier curve where the start and end points are at the 0 and 3 index + * respectively, and the control points are at the 1 and 2 index respectively. + * + * It conveniently maps into the following code: + * + * ```javascript + * canvasCtx.moveTo(start); + * canvasCtx.bezierCurveTo(control1, control2, end); + * ``` + */ +export type Curve = [ + start: Point, + control1: Point, + control2: Point, + end: Point, +] & { _brand: "excalimath_curve"; }; @@ -144,14 +157,3 @@ export type Ellipse = { } & { _brand: "excalimath_ellipse"; }; - -/** - * Represents a cubic bezier with 2 control points on the point space of your - * choosing. - */ -export type CubicBezier

= { - start: P; - end: P; - control1: P; - control2: P; -}; diff --git a/packages/utils/geometry/shape.ts b/packages/utils/geometry/shape.ts index ac6aa7d39..ced542eae 100644 --- a/packages/utils/geometry/shape.ts +++ b/packages/utils/geometry/shape.ts @@ -197,13 +197,13 @@ export const getCurvePathOps = (shape: Drawable): Op[] => { }; // linear -export const getCurveShape = ( +export const getCurveShape = ( roughShape: Drawable, - startingPoint: Point = pointFrom(0, 0), + startingPoint: GlobalPoint, angleInRadian: Radians, - center: Point, -): GeometricShape => { - const transform = (p: Point): Point => + center: GlobalPoint, +): GeometricShape => { + const transform = (p: GlobalPoint): GlobalPoint => pointRotateRads( pointFrom(p[0] + startingPoint[0], p[1] + startingPoint[1]), center, @@ -211,20 +211,20 @@ export const getCurveShape = ( ); const ops = getCurvePathOps(roughShape); - const polycurve: Polycurve = []; - let p0 = pointFrom(0, 0); + const polycurve: Polycurve = []; + let p0 = pointFrom(0, 0); for (const op of ops) { if (op.op === "move") { - const p = pointFromArray(op.data); + const p = pointFromArray(op.data); invariant(p != null, "Ops data is not a point"); p0 = transform(p); } if (op.op === "bcurveTo") { - const p1 = transform(pointFrom(op.data[0], op.data[1])); - const p2 = transform(pointFrom(op.data[2], op.data[3])); - const p3 = transform(pointFrom(op.data[4], op.data[5])); - polycurve.push(curve(p0, p1, p2, p3)); + const p1 = transform(pointFrom(op.data[0], op.data[1])); + const p2 = transform(pointFrom(op.data[2], op.data[3])); + const p3 = transform(pointFrom(op.data[4], op.data[5])); + polycurve.push(curve(p0, p1, p2, p3)); p0 = p3; } } @@ -281,16 +281,16 @@ export const getFreedrawShape = ( ) as GeometricShape; }; -export const getClosedCurveShape = ( +export const getClosedCurveShape = ( element: ExcalidrawLinearElement, roughShape: Drawable, - startingPoint: Point = pointFrom(0, 0), + startingPoint: GlobalPoint, angleInRadian: Radians, - center: Point, -): GeometricShape => { - const transform = (p: Point) => + center: GlobalPoint, +): GeometricShape => { + const transform = (p: LocalPoint) => pointRotateRads( - pointFrom(p[0] + startingPoint[0], p[1] + startingPoint[1]), + pointFrom(p[0] + startingPoint[0], p[1] + startingPoint[1]), center, angleInRadian, ); @@ -298,15 +298,13 @@ export const getClosedCurveShape = ( if (element.roundness === null) { return { type: "polygon", - data: polygonFromPoints( - element.points.map((p) => transform(p as Point)) as Point[], - ), + data: polygonFromPoints(element.points.map((p) => transform(p))), }; } const ops = getCurvePathOps(roughShape); - const points: Point[] = []; + const points: GlobalPoint[] = []; let odd = false; for (const operation of ops) { if (operation.op === "move") { @@ -328,12 +326,12 @@ export const getClosedCurveShape = ( } const polygonPoints = pointsOnBezierCurves(points, 10, 5).map((p) => - transform(p as Point), - ) as Point[]; + transform(p as LocalPoint), + ); return { type: "polygon", - data: polygonFromPoints(polygonPoints), + data: polygonFromPoints(polygonPoints), }; };