feat: support segment midpoints in line editor (#5641)

* feat: support segment midpoints in line editor

* fix tests

* midpoints working in bezier curve

* midpoint working with non zero roughness

* calculate beizer curve control points for points >2

* unnecessary rerender

* don't show phantom points inside editor for short segments

* don't show phantom points for small curves

* improve the algo for plotting midpoints on bezier curve by taking arc lengths and doing binary search

* fix tests finally

* fix naming

* cache editor midpoints

* clear midpoint cache when undo

* fix caching

* calculate index properly when not all segments have midpoints

* make sure correct element version is fetched from cache

* chore

* fix

* direct comparison for equal points

* create arePointsEqual util

* upate name

* don't update cache except inside getter

* don't compute midpoints outside editor unless 2pointer lines

* update cache to object and burst when Zoom updated as well

* early return if midpoints not present outside editor

* don't early return

* cleanup

* Add specs

* fix
This commit is contained in:
Aakansha Doshi 2022-09-14 19:55:54 +05:30 committed by GitHub
parent c5869979c8
commit 0d1058a596
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 666 additions and 113 deletions

View file

@ -1,6 +1,8 @@
import { NormalizedZoomValue, Point, Zoom } from "./types";
import { LINE_CONFIRM_THRESHOLD } from "./constants";
import { ExcalidrawLinearElement } from "./element/types";
import { ExcalidrawLinearElement, NonDeleted } from "./element/types";
import { getShapeForElement } from "./renderer/renderElement";
import { getCurvePathOps } from "./element/bounds";
export const rotate = (
x1: number,
@ -263,3 +265,165 @@ export const getGridPoint = (
}
return [x, y];
};
export const getControlPointsForBezierCurve = (
element: NonDeleted<ExcalidrawLinearElement>,
endPoint: Point,
) => {
const shape = getShapeForElement(element as ExcalidrawLinearElement);
if (!shape) {
return null;
}
const ops = getCurvePathOps(shape[0]);
let currentP: Mutable<Point> = [0, 0];
let index = 0;
let minDistance = Infinity;
let controlPoints: Mutable<Point>[] | null = null;
while (index < ops.length) {
const { op, data } = ops[index];
if (op === "move") {
currentP = data as unknown as Mutable<Point>;
}
if (op === "bcurveTo") {
const p0 = currentP;
const p1 = [data[0], data[1]] as Mutable<Point>;
const p2 = [data[2], data[3]] as Mutable<Point>;
const p3 = [data[4], data[5]] as Mutable<Point>;
const distance = distance2d(p3[0], p3[1], endPoint[0], endPoint[1]);
if (distance < minDistance) {
minDistance = distance;
controlPoints = [p0, p1, p2, p3];
}
currentP = p3;
}
index++;
}
return controlPoints;
};
export const getBezierXY = (
p0: Point,
p1: Point,
p2: Point,
p3: Point,
t: number,
) => {
const equation = (t: number, idx: number) =>
Math.pow(1 - t, 3) * p3[idx] +
3 * t * Math.pow(1 - t, 2) * p2[idx] +
3 * Math.pow(t, 2) * (1 - t) * p1[idx] +
p0[idx] * Math.pow(t, 3);
const tx = equation(t, 0);
const ty = equation(t, 1);
return [tx, ty];
};
export const getPointsInBezierCurve = (
element: NonDeleted<ExcalidrawLinearElement>,
endPoint: Point,
) => {
const controlPoints: Mutable<Point>[] = getControlPointsForBezierCurve(
element,
endPoint,
)!;
if (!controlPoints) {
return [];
}
const pointsOnCurve: Mutable<Point>[] = [];
let t = 1;
// Take 20 points on curve for better accuracy
while (t > 0) {
const point = getBezierXY(
controlPoints[0],
controlPoints[1],
controlPoints[2],
controlPoints[3],
t,
);
pointsOnCurve.push([point[0], point[1]]);
t -= 0.05;
}
if (pointsOnCurve.length) {
if (arePointsEqual(pointsOnCurve.at(-1)!, endPoint)) {
pointsOnCurve.push([endPoint[0], endPoint[1]]);
}
}
return pointsOnCurve;
};
export const getBezierCurveArcLengths = (
element: NonDeleted<ExcalidrawLinearElement>,
endPoint: Point,
) => {
const arcLengths: number[] = [];
arcLengths[0] = 0;
const points = getPointsInBezierCurve(element, endPoint);
let index = 0;
let distance = 0;
while (index < points.length - 1) {
const segmentDistance = distance2d(
points[index][0],
points[index][1],
points[index + 1][0],
points[index + 1][1],
);
distance += segmentDistance;
arcLengths.push(distance);
index++;
}
return arcLengths;
};
export const getBezierCurveLength = (
element: NonDeleted<ExcalidrawLinearElement>,
endPoint: Point,
) => {
const arcLengths = getBezierCurveArcLengths(element, endPoint);
return arcLengths.at(-1) as number;
};
// This maps interval to actual interval t on the curve so that when t = 0.5, its actually the point at 50% of the length
export const mapIntervalToBezierT = (
element: NonDeleted<ExcalidrawLinearElement>,
endPoint: Point,
interval: number, // The interval between 0 to 1 for which you want to find the point on the curve,
) => {
const arcLengths = getBezierCurveArcLengths(element, endPoint);
const pointsCount = arcLengths.length - 1;
const curveLength = arcLengths.at(-1) as number;
const targetLength = interval * curveLength;
let low = 0;
let high = pointsCount;
let index = 0;
// Doing a binary search to find the largest length that is less than the target length
while (low < high) {
index = Math.floor(low + (high - low) / 2);
if (arcLengths[index] < targetLength) {
low = index + 1;
} else {
high = index;
}
}
if (arcLengths[index] > targetLength) {
index--;
}
if (arcLengths[index] === targetLength) {
return index / pointsCount;
}
return (
1 -
(index +
(targetLength - arcLengths[index]) /
(arcLengths[index + 1] - arcLengths[index])) /
pointsCount
);
};
export const arePointsEqual = (p1: Point, p2: Point) => {
return p1[0] === p2[0] && p1[1] === p2[1];
};