Merge branch 'master' into mtolmacs/feat/prettier-arrow-curves
Signed-off-by: Mark Tolmacs <mark@lazycat.hu>
This commit is contained in:
@@ -13,7 +13,6 @@ import {
|
||||
import {
|
||||
pointFrom,
|
||||
pointDistance,
|
||||
lineSegment,
|
||||
type LocalPoint,
|
||||
pointRotateRads,
|
||||
} from "@excalidraw/math";
|
||||
@@ -1024,9 +1023,6 @@ const _generateElementShape = (
|
||||
generateRoughOptions(element, true, isDarkMode),
|
||||
),
|
||||
];
|
||||
if (window.visualDebug?.data) {
|
||||
debugRoundedArrowControlPoints(element.x, element.y, points);
|
||||
}
|
||||
}
|
||||
|
||||
// add lines only in arrow
|
||||
@@ -1110,229 +1106,6 @@ const _generateElementShape = (
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Debug helper to visualise C2 spline control points.
|
||||
*
|
||||
* Chords are grey, CP1 handles/circles are green, CP2 handles/diamonds are blue,
|
||||
* segment points are red X markers.
|
||||
*/
|
||||
const debugRoundedArrowControlPoints = (
|
||||
elementX: number,
|
||||
elementY: number,
|
||||
points: readonly LocalPoint[],
|
||||
) => {
|
||||
const nPts = points.length;
|
||||
if (nPts < 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
const g = (lx: number, ly: number): GlobalPoint =>
|
||||
pointFrom<GlobalPoint>(elementX + lx, elementY + ly);
|
||||
|
||||
const PERMANENT = { permanent: true } as const;
|
||||
const CP_RADIUS = 5;
|
||||
const DIAMOND_RADIUS = 6;
|
||||
|
||||
// // Segment points: red X
|
||||
// for (let i = 0; i < nPts; i++) {
|
||||
// debugDrawPoint(g(points[i][0], points[i][1]), {
|
||||
// color: "#ff3333",
|
||||
// ...PERMANENT,
|
||||
// });
|
||||
// }
|
||||
|
||||
if (nPts === 2) {
|
||||
debugDrawLine(
|
||||
lineSegment(g(points[0][0], points[0][1]), g(points[1][0], points[1][1])),
|
||||
{ color: "#888888", ...PERMANENT },
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Chord-length C2 spline – same algorithm as generateRoundedSimpleArrowShape
|
||||
const n = nPts - 1;
|
||||
const h = new Float64Array(n);
|
||||
for (let i = 0; i < n; i++) {
|
||||
h[i] = Math.max(
|
||||
1e-10,
|
||||
Math.hypot(
|
||||
points[i + 1][0] - points[i][0],
|
||||
points[i + 1][1] - points[i][1],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
const mx = new Float64Array(n + 1);
|
||||
const my = new Float64Array(n + 1);
|
||||
const diag = new Float64Array(n + 1);
|
||||
const rhsX = new Float64Array(n + 1);
|
||||
const rhsY = new Float64Array(n + 1);
|
||||
|
||||
diag[0] = 2;
|
||||
rhsX[0] = (3 * (points[1][0] - points[0][0])) / h[0];
|
||||
rhsY[0] = (3 * (points[1][1] - points[0][1])) / h[0];
|
||||
for (let i = 1; i < n; i++) {
|
||||
diag[i] = 2 * (h[i - 1] + h[i]);
|
||||
rhsX[i] =
|
||||
3 *
|
||||
((h[i] * (points[i][0] - points[i - 1][0])) / h[i - 1] +
|
||||
(h[i - 1] * (points[i + 1][0] - points[i][0])) / h[i]);
|
||||
rhsY[i] =
|
||||
3 *
|
||||
((h[i] * (points[i][1] - points[i - 1][1])) / h[i - 1] +
|
||||
(h[i - 1] * (points[i + 1][1] - points[i][1])) / h[i]);
|
||||
}
|
||||
diag[n] = 2;
|
||||
rhsX[n] = (3 * (points[n][0] - points[n - 1][0])) / h[n - 1];
|
||||
rhsY[n] = (3 * (points[n][1] - points[n - 1][1])) / h[n - 1];
|
||||
|
||||
for (let i = 1; i <= n; i++) {
|
||||
const sub = i < n ? h[i] : 1;
|
||||
const supPrev = i === 1 ? 1 : h[i - 2];
|
||||
const w = sub / diag[i - 1];
|
||||
diag[i] -= w * supPrev;
|
||||
rhsX[i] -= w * rhsX[i - 1];
|
||||
rhsY[i] -= w * rhsY[i - 1];
|
||||
}
|
||||
mx[n] = rhsX[n] / diag[n];
|
||||
my[n] = rhsY[n] / diag[n];
|
||||
for (let i = n - 1; i >= 0; i--) {
|
||||
const sup = i === 0 ? 1 : h[i - 1];
|
||||
mx[i] = (rhsX[i] - sup * mx[i + 1]) / diag[i];
|
||||
my[i] = (rhsY[i] - sup * my[i + 1]) / diag[i];
|
||||
}
|
||||
|
||||
// Normalised tangent directions; handle length scales sub-linearly with chord.
|
||||
const mlen = new Float64Array(n + 1);
|
||||
for (let i = 0; i <= n; i++) {
|
||||
mlen[i] = Math.max(1e-10, Math.hypot(mx[i], my[i]));
|
||||
}
|
||||
|
||||
// Mirror the angle-correction from generateRoundedSimpleArrowShape.
|
||||
for (let k = 1; k < n; k++) {
|
||||
const d1x = (points[k][0] - points[k - 1][0]) / h[k - 1];
|
||||
const d1y = (points[k][1] - points[k - 1][1]) / h[k - 1];
|
||||
const d2x = (points[k + 1][0] - points[k][0]) / h[k];
|
||||
const d2y = (points[k + 1][1] - points[k][1]) / h[k];
|
||||
const dot = d1x * d2x + d1y * d2y;
|
||||
const t = ((1 - dot) / 2) * CP_ANGLE_CORRECTION;
|
||||
if (t < 1e-6) {
|
||||
continue;
|
||||
}
|
||||
const bx = d1x + d2x;
|
||||
const by = d1y + d2y;
|
||||
const blen = Math.hypot(bx, by);
|
||||
if (blen < 1e-10) {
|
||||
continue;
|
||||
}
|
||||
// Blend target: the bisector direction itself (pick sign aligning with current tangent)
|
||||
let px = bx / blen;
|
||||
let py = by / blen;
|
||||
const tx = mx[k] / mlen[k];
|
||||
const ty = my[k] / mlen[k];
|
||||
if (tx * px + ty * py < 0) {
|
||||
px = -px;
|
||||
py = -py;
|
||||
}
|
||||
const blendX = tx + t * (px - tx);
|
||||
const blendY = ty + t * (py - ty);
|
||||
const blendLen = Math.max(1e-10, Math.hypot(blendX, blendY));
|
||||
mx[k] = (blendX / blendLen) * mlen[k];
|
||||
my[k] = (blendY / blendLen) * mlen[k];
|
||||
}
|
||||
|
||||
// Bisector at interior knots: orange line along bisector, yellow tick for
|
||||
// perpendicular-to-bisector (the ideal symmetric tangent direction).
|
||||
const BISECTOR_HALF_LEN = 20;
|
||||
const PERP_HALF_LEN = 12;
|
||||
for (let k = 1; k < n; k++) {
|
||||
const d1x = (points[k][0] - points[k - 1][0]) / h[k - 1];
|
||||
const d1y = (points[k][1] - points[k - 1][1]) / h[k - 1];
|
||||
const d2x = (points[k + 1][0] - points[k][0]) / h[k];
|
||||
const d2y = (points[k + 1][1] - points[k][1]) / h[k];
|
||||
const bx = d1x + d2x;
|
||||
const by = d1y + d2y;
|
||||
const blen = Math.hypot(bx, by);
|
||||
if (blen < 1e-10) {
|
||||
continue;
|
||||
}
|
||||
const bnx = bx / blen;
|
||||
const bny = by / blen;
|
||||
const pnx = -bny; // perpendicular to bisector = ideal tangent direction
|
||||
const pny = bnx;
|
||||
const pk = g(points[k][0], points[k][1]);
|
||||
// bisector (orange)
|
||||
debugDrawLine(
|
||||
lineSegment(
|
||||
pointFrom<GlobalPoint>(
|
||||
pk[0] - bnx * BISECTOR_HALF_LEN,
|
||||
pk[1] - bny * BISECTOR_HALF_LEN,
|
||||
),
|
||||
pointFrom<GlobalPoint>(
|
||||
pk[0] + bnx * BISECTOR_HALF_LEN,
|
||||
pk[1] + bny * BISECTOR_HALF_LEN,
|
||||
),
|
||||
),
|
||||
{ color: "#ff8800", ...PERMANENT },
|
||||
);
|
||||
// perpendicular tick / ideal tangent (yellow)
|
||||
debugDrawLine(
|
||||
lineSegment(
|
||||
pointFrom<GlobalPoint>(
|
||||
pk[0] - pnx * PERP_HALF_LEN,
|
||||
pk[1] - pny * PERP_HALF_LEN,
|
||||
),
|
||||
pointFrom<GlobalPoint>(
|
||||
pk[0] + pnx * PERP_HALF_LEN,
|
||||
pk[1] + pny * PERP_HALF_LEN,
|
||||
),
|
||||
),
|
||||
{ color: "#ffdd00", ...PERMANENT },
|
||||
);
|
||||
}
|
||||
|
||||
for (let i = 0; i < n; i++) {
|
||||
const cpDist = Math.pow(h[i], CP_CHORD_POWER) / 3;
|
||||
const p0 = g(points[i][0], points[i][1]);
|
||||
const p1 = g(points[i + 1][0], points[i + 1][1]);
|
||||
const cp1 = g(
|
||||
points[i][0] + (mx[i] / mlen[i]) * cpDist,
|
||||
points[i][1] + (my[i] / mlen[i]) * cpDist,
|
||||
);
|
||||
const cp2 = g(
|
||||
points[i + 1][0] - (mx[i + 1] / mlen[i + 1]) * cpDist,
|
||||
points[i + 1][1] - (my[i + 1] / mlen[i + 1]) * cpDist,
|
||||
);
|
||||
|
||||
// chord (grey)
|
||||
debugDrawLine(lineSegment(p0, p1), { color: "#888888", ...PERMANENT });
|
||||
|
||||
// CP1 handle + circle (green = outgoing from p0)
|
||||
debugDrawLine(lineSegment(p0, cp1), { color: "#00cc44", ...PERMANENT });
|
||||
debugDrawPolygon(
|
||||
Array.from({ length: 9 }, (_, k) =>
|
||||
pointFrom<GlobalPoint>(
|
||||
cp1[0] + Math.cos((k * Math.PI) / 4) * CP_RADIUS,
|
||||
cp1[1] + Math.sin((k * Math.PI) / 4) * CP_RADIUS,
|
||||
),
|
||||
),
|
||||
{ color: "#00cc44", close: true, ...PERMANENT },
|
||||
);
|
||||
|
||||
// CP2 handle + diamond (blue = incoming to p1)
|
||||
debugDrawLine(lineSegment(p1, cp2), { color: "#0088ff", ...PERMANENT });
|
||||
debugDrawPolygon(
|
||||
[
|
||||
pointFrom<GlobalPoint>(cp2[0], cp2[1] - DIAMOND_RADIUS),
|
||||
pointFrom<GlobalPoint>(cp2[0] + DIAMOND_RADIUS, cp2[1]),
|
||||
pointFrom<GlobalPoint>(cp2[0], cp2[1] + DIAMOND_RADIUS),
|
||||
pointFrom<GlobalPoint>(cp2[0] - DIAMOND_RADIUS, cp2[1]),
|
||||
],
|
||||
{ color: "#0088ff", close: true, ...PERMANENT },
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const generateRoundedSimpleArrowShape = (
|
||||
points: readonly LocalPoint[],
|
||||
): string => {
|
||||
|
||||
Reference in New Issue
Block a user