improve balls

This commit is contained in:
dwelle
2026-04-03 19:51:41 +02:00
parent c9d29ea600
commit 0256559b68
2 changed files with 72 additions and 3 deletions
+46 -3
View File
@@ -1233,6 +1233,11 @@ const roundPoint = (point: readonly number[]) =>
const averagePoint = (A: readonly number[], B: readonly number[]) => const averagePoint = (A: readonly number[], B: readonly number[]) =>
`${round((A[0] + B[0]) / 2, 2)},${round((A[1] + B[1]) / 2, 2)} `; `${round((A[0] + B[0]) / 2, 2)},${round((A[1] + B[1]) / 2, 2)} `;
const getReadonlyPointDistance = (
pointA: readonly number[],
pointB: readonly number[],
) => Math.hypot(pointA[0] - pointB[0], pointA[1] - pointB[1]);
// Trim SVG path data so number are each two decimal points. This // Trim SVG path data so number are each two decimal points. This
// improves SVG exports, and prevents rendering errors on points // improves SVG exports, and prevents rendering errors on points
// with long decimals. // with long decimals.
@@ -1263,6 +1268,9 @@ const FIXED_FREEDRAW_MIN_CORNER_ROUNDING = 0.75;
const FIXED_FREEDRAW_MAX_CORNER_ROUNDING = 6; const FIXED_FREEDRAW_MAX_CORNER_ROUNDING = 6;
const FIXED_FREEDRAW_CORNER_ROUNDING_FACTOR = 0.35; const FIXED_FREEDRAW_CORNER_ROUNDING_FACTOR = 0.35;
const FIXED_FREEDRAW_CORNER_ROUNDING_WIDTH_FACTOR = 1.5; const FIXED_FREEDRAW_CORNER_ROUNDING_WIDTH_FACTOR = 1.5;
const FIXED_FREEDRAW_MIN_TERMINAL_STUB = 0.75;
const FIXED_FREEDRAW_MAX_TERMINAL_STUB = 2.5;
const FIXED_FREEDRAW_TERMINAL_STUB_WIDTH_FACTOR = 1.5;
const shouldSmoothFixedFreeDrawPoint = ( const shouldSmoothFixedFreeDrawPoint = (
previousPoint: readonly number[], previousPoint: readonly number[],
@@ -1344,6 +1352,15 @@ const getFixedFreeDrawRoundedCorner = (
}; };
}; };
const getFixedFreeDrawTerminalStubThreshold = (strokeWidth: number) =>
Math.min(
Math.max(
strokeWidth * FIXED_FREEDRAW_TERMINAL_STUB_WIDTH_FACTOR,
FIXED_FREEDRAW_MIN_TERMINAL_STUB,
),
FIXED_FREEDRAW_MAX_TERMINAL_STUB,
);
const getSvgPathFromFixedFreeDrawPoints = ( const getSvgPathFromFixedFreeDrawPoints = (
points: ReadonlyArray<readonly number[]>, points: ReadonlyArray<readonly number[]>,
strokeWidth: number, strokeWidth: number,
@@ -1359,11 +1376,17 @@ const getSvgPathFromFixedFreeDrawPoints = (
} }
let path = `M${roundPoint(points[0])}`; let path = `M${roundPoint(points[0])}`;
const lastPoint = points[len - 1];
let endsAtLastPoint = false;
for (let index = 1; index < len - 1; index++) { for (let index = 1; index < len - 1; index++) {
const previousPoint = points[index - 1]; const previousPoint = points[index - 1];
const currentPoint = points[index]; const currentPoint = points[index];
const nextPoint = points[index + 1]; const nextPoint = points[index + 1];
const isLastCurveSegment = index === len - 2;
const terminalStubThreshold = getFixedFreeDrawTerminalStubThreshold(
strokeWidth,
);
const shouldSmooth = shouldSmoothFixedFreeDrawPoint( const shouldSmooth = shouldSmoothFixedFreeDrawPoint(
previousPoint, previousPoint,
currentPoint, currentPoint,
@@ -1377,17 +1400,37 @@ const getSvgPathFromFixedFreeDrawPoints = (
nextPoint, nextPoint,
strokeWidth, strokeWidth,
); );
const shouldCollapseSmoothTerminalStub =
isLastCurveSegment &&
shouldSmooth &&
getReadonlyPointDistance(currentPoint, nextPoint) / 2 <=
terminalStubThreshold;
const shouldCollapseRoundedTerminalStub =
isLastCurveSegment &&
!!roundedCorner &&
getReadonlyPointDistance(roundedCorner.exitPoint, lastPoint) <=
terminalStubThreshold;
path += shouldSmooth path += shouldSmooth
? `Q${roundPoint(currentPoint)}${averagePoint(currentPoint, nextPoint)}` ? `Q${roundPoint(currentPoint)}${
shouldCollapseSmoothTerminalStub
? roundPoint(lastPoint)
: averagePoint(currentPoint, nextPoint)
}`
: roundedCorner : roundedCorner
? `L${roundPoint(roundedCorner.entryPoint)}Q${roundPoint( ? `L${roundPoint(roundedCorner.entryPoint)}Q${roundPoint(
currentPoint, currentPoint,
)}${roundPoint(roundedCorner.exitPoint)}` )}${
shouldCollapseRoundedTerminalStub
? roundPoint(lastPoint)
: roundPoint(roundedCorner.exitPoint)
}`
: `L${roundPoint(currentPoint)}`; : `L${roundPoint(currentPoint)}`;
endsAtLastPoint =
shouldCollapseSmoothTerminalStub || shouldCollapseRoundedTerminalStub;
} }
return `${path}L${roundPoint(points[len - 1])}`; return endsAtLastPoint ? path : `${path}L${roundPoint(lastPoint)}`;
}; };
const getSvgPathFromStroke = ( const getSvgPathFromStroke = (
+26
View File
@@ -56,6 +56,32 @@ describe("freedraw stroke shape", () => {
expect(path).toBe("M0,0 L10,0 L9,1 "); expect(path).toBe("M0,0 L10,0 L9,1 ");
}); });
it("curves directly to the endpoint for tiny final smooth segments", () => {
const element = API.createElement({
type: "freedraw",
strokeShape: "fixed",
strokeWidth: 1,
points: [pointFrom(0, 0), pointFrom(0.5, 0.1), pointFrom(0.8, 0.3)],
});
const [path] = ShapeCache.generateElementShape(element, null);
expect(path).toBe("M0,0 Q0.5,0.1 0.8,0.3 ");
});
it("curves directly to the endpoint for tiny final rounded corners", () => {
const element = API.createElement({
type: "freedraw",
strokeShape: "fixed",
strokeWidth: 1,
points: [pointFrom(0, 0), pointFrom(10, 0), pointFrom(10, 1)],
});
const [path] = ShapeCache.generateElementShape(element, null);
expect(path).toBe("M0,0 L9.65,0 Q10,0 10,1 ");
});
it("smooths dense fixed stroke points without dropping them", () => { it("smooths dense fixed stroke points without dropping them", () => {
const element = API.createElement({ const element = API.createElement({
type: "freedraw", type: "freedraw",