Merge pull request #17574 from calixteman/improve_bezier_bbox

Reduce memory use and improve perfs when computing the bounding box of a bezier curve (bug 1875547)
This commit is contained in:
calixteman 2024-01-25 10:07:48 +01:00 committed by GitHub
commit bf92360095
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 139 additions and 103 deletions

View File

@ -1386,17 +1386,17 @@ class PartialEvaluator {
const y = args[1] + args[3];
minMax = [
Math.min(args[0], x),
Math.max(args[0], x),
Math.min(args[1], y),
Math.max(args[0], x),
Math.max(args[1], y),
];
break;
case OPS.moveTo:
case OPS.lineTo:
minMax = [args[0], args[0], args[1], args[1]];
minMax = [args[0], args[1], args[0], args[1]];
break;
default:
minMax = [Infinity, -Infinity, Infinity, -Infinity];
minMax = [Infinity, Infinity, -Infinity, -Infinity];
break;
}
operatorList.addOp(OPS.constructPath, [[fn], args, minMax]);
@ -1420,15 +1420,15 @@ class PartialEvaluator {
const x = args[0] + args[2];
const y = args[1] + args[3];
minMax[0] = Math.min(minMax[0], args[0], x);
minMax[1] = Math.max(minMax[1], args[0], x);
minMax[2] = Math.min(minMax[2], args[1], y);
minMax[1] = Math.min(minMax[1], args[1], y);
minMax[2] = Math.max(minMax[2], args[0], x);
minMax[3] = Math.max(minMax[3], args[1], y);
break;
case OPS.moveTo:
case OPS.lineTo:
minMax[0] = Math.min(minMax[0], args[0]);
minMax[1] = Math.max(minMax[1], args[0]);
minMax[2] = Math.min(minMax[2], args[1]);
minMax[1] = Math.min(minMax[1], args[1]);
minMax[2] = Math.max(minMax[2], args[0]);
minMax[3] = Math.max(minMax[3], args[1]);
break;
}

View File

@ -529,18 +529,14 @@ class CanvasExtraState {
updateScalingPathMinMax(transform, minMax) {
Util.scaleMinMax(transform, minMax);
this.minX = Math.min(this.minX, minMax[0]);
this.maxX = Math.max(this.maxX, minMax[1]);
this.minY = Math.min(this.minY, minMax[2]);
this.minY = Math.min(this.minY, minMax[1]);
this.maxX = Math.max(this.maxX, minMax[2]);
this.maxY = Math.max(this.maxY, minMax[3]);
}
updateCurvePathMinMax(transform, x0, y0, x1, y1, x2, y2, x3, y3, minMax) {
const box = Util.bezierBoundingBox(x0, y0, x1, y1, x2, y2, x3, y3);
const box = Util.bezierBoundingBox(x0, y0, x1, y1, x2, y2, x3, y3, minMax);
if (minMax) {
minMax[0] = Math.min(minMax[0], box[0], box[2]);
minMax[1] = Math.max(minMax[1], box[0], box[2]);
minMax[2] = Math.min(minMax[2], box[1], box[3]);
minMax[3] = Math.max(minMax[3], box[1], box[3]);
return;
}
this.updateRectMinMax(transform, box);

View File

@ -651,52 +651,52 @@ class Util {
// Apply a scaling matrix to some min/max values.
// If a scaling factor is negative then min and max must be
// swaped.
// swapped.
static scaleMinMax(transform, minMax) {
let temp;
if (transform[0]) {
if (transform[0] < 0) {
temp = minMax[0];
minMax[0] = minMax[1];
minMax[1] = temp;
minMax[0] = minMax[2];
minMax[2] = temp;
}
minMax[0] *= transform[0];
minMax[1] *= transform[0];
minMax[2] *= transform[0];
if (transform[3] < 0) {
temp = minMax[2];
minMax[2] = minMax[3];
temp = minMax[1];
minMax[1] = minMax[3];
minMax[3] = temp;
}
minMax[2] *= transform[3];
minMax[1] *= transform[3];
minMax[3] *= transform[3];
} else {
temp = minMax[0];
minMax[0] = minMax[2];
minMax[2] = temp;
temp = minMax[1];
minMax[1] = minMax[3];
minMax[0] = minMax[1];
minMax[1] = temp;
temp = minMax[2];
minMax[2] = minMax[3];
minMax[3] = temp;
if (transform[1] < 0) {
temp = minMax[2];
minMax[2] = minMax[3];
temp = minMax[1];
minMax[1] = minMax[3];
minMax[3] = temp;
}
minMax[2] *= transform[1];
minMax[1] *= transform[1];
minMax[3] *= transform[1];
if (transform[2] < 0) {
temp = minMax[0];
minMax[0] = minMax[1];
minMax[1] = temp;
minMax[0] = minMax[2];
minMax[2] = temp;
}
minMax[0] *= transform[2];
minMax[1] *= transform[2];
minMax[2] *= transform[2];
}
minMax[0] += transform[4];
minMax[1] += transform[4];
minMax[2] += transform[5];
minMax[1] += transform[5];
minMax[2] += transform[4];
minMax[3] += transform[5];
}
@ -822,76 +822,116 @@ class Util {
return [xLow, yLow, xHigh, yHigh];
}
static #getExtremumOnCurve(x0, x1, x2, x3, y0, y1, y2, y3, t, minMax) {
if (t <= 0 || t >= 1) {
return;
}
const mt = 1 - t;
const tt = t * t;
const ttt = tt * t;
const x = mt * (mt * (mt * x0 + 3 * t * x1) + 3 * tt * x2) + ttt * x3;
const y = mt * (mt * (mt * y0 + 3 * t * y1) + 3 * tt * y2) + ttt * y3;
minMax[0] = Math.min(minMax[0], x);
minMax[1] = Math.min(minMax[1], y);
minMax[2] = Math.max(minMax[2], x);
minMax[3] = Math.max(minMax[3], y);
}
static #getExtremum(x0, x1, x2, x3, y0, y1, y2, y3, a, b, c, minMax) {
if (Math.abs(a) < 1e-12) {
if (Math.abs(b) >= 1e-12) {
this.#getExtremumOnCurve(
x0,
x1,
x2,
x3,
y0,
y1,
y2,
y3,
-c / b,
minMax
);
}
return;
}
const delta = b ** 2 - 4 * c * a;
if (delta < 0) {
return;
}
const sqrtDelta = Math.sqrt(delta);
const a2 = 2 * a;
this.#getExtremumOnCurve(
x0,
x1,
x2,
x3,
y0,
y1,
y2,
y3,
(-b + sqrtDelta) / a2,
minMax
);
this.#getExtremumOnCurve(
x0,
x1,
x2,
x3,
y0,
y1,
y2,
y3,
(-b - sqrtDelta) / a2,
minMax
);
}
// From https://github.com/adobe-webplatform/Snap.svg/blob/b365287722a72526000ac4bfcf0ce4cac2faa015/src/path.js#L852
static bezierBoundingBox(x0, y0, x1, y1, x2, y2, x3, y3) {
const tvalues = [],
bounds = [[], []];
let a, b, c, t, t1, t2, b2ac, sqrtb2ac;
for (let i = 0; i < 2; ++i) {
if (i === 0) {
b = 6 * x0 - 12 * x1 + 6 * x2;
a = -3 * x0 + 9 * x1 - 9 * x2 + 3 * x3;
c = 3 * x1 - 3 * x0;
} else {
b = 6 * y0 - 12 * y1 + 6 * y2;
a = -3 * y0 + 9 * y1 - 9 * y2 + 3 * y3;
c = 3 * y1 - 3 * y0;
}
if (Math.abs(a) < 1e-12) {
if (Math.abs(b) < 1e-12) {
continue;
}
t = -c / b;
if (0 < t && t < 1) {
tvalues.push(t);
}
continue;
}
b2ac = b * b - 4 * c * a;
sqrtb2ac = Math.sqrt(b2ac);
if (b2ac < 0) {
continue;
}
t1 = (-b + sqrtb2ac) / (2 * a);
if (0 < t1 && t1 < 1) {
tvalues.push(t1);
}
t2 = (-b - sqrtb2ac) / (2 * a);
if (0 < t2 && t2 < 1) {
tvalues.push(t2);
}
static bezierBoundingBox(x0, y0, x1, y1, x2, y2, x3, y3, minMax) {
if (minMax) {
minMax[0] = Math.min(minMax[0], x0, x3);
minMax[1] = Math.min(minMax[1], y0, y3);
minMax[2] = Math.max(minMax[2], x0, x3);
minMax[3] = Math.max(minMax[3], y0, y3);
} else {
minMax = [
Math.min(x0, x3),
Math.min(y0, y3),
Math.max(x0, x3),
Math.max(y0, y3),
];
}
let j = tvalues.length,
mt;
const jlen = j;
while (j--) {
t = tvalues[j];
mt = 1 - t;
bounds[0][j] =
mt * mt * mt * x0 +
3 * mt * mt * t * x1 +
3 * mt * t * t * x2 +
t * t * t * x3;
bounds[1][j] =
mt * mt * mt * y0 +
3 * mt * mt * t * y1 +
3 * mt * t * t * y2 +
t * t * t * y3;
}
bounds[0][jlen] = x0;
bounds[1][jlen] = y0;
bounds[0][jlen + 1] = x3;
bounds[1][jlen + 1] = y3;
bounds[0].length = bounds[1].length = jlen + 2;
return [
Math.min(...bounds[0]),
Math.min(...bounds[1]),
Math.max(...bounds[0]),
Math.max(...bounds[1]),
];
this.#getExtremum(
x0,
x1,
x2,
x3,
y0,
y1,
y2,
y3,
3 * (-x0 + 3 * (x1 - x2) + x3),
6 * (x0 - 2 * x1 + x2),
3 * (x1 - x0),
minMax
);
this.#getExtremum(
x0,
x1,
x2,
x3,
y0,
y1,
y2,
y3,
3 * (-y0 + 3 * (y1 - y2) + y3),
6 * (y0 - 2 * y1 + y2),
3 * (y1 - y0),
minMax
);
return minMax;
}
}

View File

@ -4548,7 +4548,7 @@ describe("annotation", function () {
expect(opList.argsArray[5][0]).toEqual([OPS.moveTo, OPS.curveTo]);
expect(opList.argsArray[5][1]).toEqual([1, 2, 3, 4, 5, 6, 7, 8]);
// Min-max.
expect(opList.argsArray[5][2]).toEqual([1, 1, 2, 2]);
expect(opList.argsArray[5][2]).toEqual([1, 2, 1, 2]);
});
});

View File

@ -771,7 +771,7 @@ describe("api", function () {
[
[OPS.moveTo, OPS.lineTo],
[0, 9.75, 0.5, 9.75],
[0, 0.5, 9.75, 9.75],
[0, 9.75, 0.5, 9.75],
],
null,
]);