Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(Path): bbox stroke projection #8040

Closed
wants to merge 49 commits into from
Closed
Show file tree
Hide file tree
Changes from 46 commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
7bb3bbd
WIP fix bbox
ShaMan123 Jun 30, 2022
c8741c8
Update path.class.js
ShaMan123 Jun 30, 2022
35a4050
fix(): return points
ShaMan123 Jun 30, 2022
8ef5be7
project bbox
ShaMan123 Jun 30, 2022
f37ee08
Update path.class.js
ShaMan123 Jun 30, 2022
60fb86f
Update polyline.class.js
ShaMan123 Jun 30, 2022
e33d4b2
chore(): move `_setPositionDimensions` to Path
ShaMan123 Jun 30, 2022
f63381b
deprecate `exactBoundingBox` flag
ShaMan123 Jun 30, 2022
dd0174f
fix positioning
ShaMan123 Jun 30, 2022
f90c896
test
ShaMan123 Jun 30, 2022
0184144
Update generic_rendering.js
ShaMan123 Jun 30, 2022
c9696b9
test + position
ShaMan123 Jun 30, 2022
8ec9d35
rename
ShaMan123 Jun 30, 2022
790d392
lint
ShaMan123 Jun 30, 2022
f8807b4
fix
ShaMan123 Jul 1, 2022
d6d1276
revert
ShaMan123 Jul 1, 2022
6dba673
fix?
ShaMan123 Jul 1, 2022
aad27ad
fix bounds usage
ShaMan123 Jul 1, 2022
ba6fb30
Update path.class.js
ShaMan123 Jul 1, 2022
016e61f
Update path.class.js
ShaMan123 Jul 1, 2022
c85b99a
Update path.class.js
ShaMan123 Jul 1, 2022
e51fa03
Update path.class.js
ShaMan123 Jul 1, 2022
6ea773d
Update path.class.js
ShaMan123 Jul 1, 2022
869320b
Update path.class.js
ShaMan123 Jul 1, 2022
22d2a19
checkout
ShaMan123 Jul 1, 2022
8f62ded
Merge branch 'master' into path-bbox-stroke-projection
ShaMan123 Jul 1, 2022
e758365
Update path.class.js
ShaMan123 Jul 1, 2022
0d03b0d
Revert "Update path.class.js"
ShaMan123 Jul 1, 2022
6b952c5
Update path.class.js
ShaMan123 Jul 1, 2022
1de68eb
??
ShaMan123 Jul 1, 2022
fa4f46b
Update path.class.js
ShaMan123 Jul 1, 2022
49c037a
Update misc.js
ShaMan123 Jul 1, 2022
15771b1
Update pencil_brush.class.js
ShaMan123 Jul 1, 2022
ad70555
Update path.class.js
ShaMan123 Jul 1, 2022
9a3119d
Update path.class.js
ShaMan123 Jul 1, 2022
48e13ad
Update path.class.js
ShaMan123 Jul 1, 2022
1a6b957
MAJOR fix
ShaMan123 Jul 1, 2022
dc058a9
Update path.class.js
ShaMan123 Jul 1, 2022
94ac1a7
tests
ShaMan123 Jul 1, 2022
a203254
Update textpath5.png
ShaMan123 Jul 1, 2022
d41b643
Update misc.js
ShaMan123 Jul 1, 2022
10aa26a
Update misc.js
ShaMan123 Jul 1, 2022
18f026b
Update path.class.js
ShaMan123 Jul 2, 2022
82e892c
Update z_svg_export.js
ShaMan123 Jul 2, 2022
df3c0c5
no defaults
ShaMan123 Jul 2, 2022
1555e8d
Update z_svg_export.js
ShaMan123 Jul 2, 2022
44723c3
goldens
ShaMan123 Jul 2, 2022
e133fa0
Update misc.js
ShaMan123 Jul 4, 2022
cb476a0
Update stroke_projection.svg
ShaMan123 Jul 8, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/brushes/pencil_brush.class.js
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,7 @@
this.shadow.affectStroke = true;
path.shadow = new fabric.Shadow(this.shadow);
}

path.pathOffset.scalarAddEquals(path.strokeWidth / 2);
return path;
},

Expand Down
174 changes: 141 additions & 33 deletions src/shapes/path.class.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@
Array.isArray(path) ? path : fabric.util.parsePath(path)
);

fabric.Polyline.prototype._setPositionDimensions.call(this, options || {});
this._setPositionDimensions(options);
},

/**
Expand Down Expand Up @@ -237,6 +237,37 @@
return this.path.length;
},

/**
* @private
*/
_projectStrokeOnSegmentBBox: function (point, pointBefore, pointAfter, bboxPoints) {
var v1 = pointBefore.subtract(point);
var v2 = pointAfter.subtract(point);
var angle = fabric.util.calcAngleBetweenVectors(v1, v2);
if (Math.abs(angle) < Math.PI / 2) {
Copy link
Contributor Author

@ShaMan123 ShaMan123 Jul 2, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we project all angles some bboxes of visual tests fix while other visual tests break

  • clipping13
    image

  • pathWithGradientSvg
    image

  • arc3 image

var projectionVector = fabric.util.calcStrokeProjection(
pointBefore,
point,
pointAfter,
this
);
var projectedPoints = [
point.add(projectionVector),
point.subtract(projectionVector),
];
Array.isArray(bboxPoints) && bboxPoints.forEach(function (point) {
projectedPoints.push(
point.add(projectionVector),
point.subtract(projectionVector)
);
});
return projectedPoints;
}
else {
return [];
}
},

/**
* @private
*/
Expand All @@ -245,86 +276,163 @@
var aX = [],
aY = [],
current, // current instruction
subpathStartX = 0,
subpathStartY = 0,
x = 0, // current x
y = 0, // current y
bounds;
subpathStart = new fabric.Point(0, 0),
subpathSecond = new fabric.Point(0, 0),
point = new fabric.Point(0, 0),
prev = new fabric.Point(0, 0),
beforePrev = new fabric.Point(0, 0),
opening = false,
second = false,
closing = false,
projectedPoints = [],
prevBounds,
bounds = [];

for (var i = 0, len = this.path.length; i < len; ++i) {

current = this.path[i];
if (opening) {
second = true;
}
opening = closing = false;
beforePrev.setFromPoint(prev);
prev.setFromPoint(point);
prevBounds = bounds;
bounds = [];

switch (current[0]) { // first letter

case 'L': // lineto, absolute
x = current[1];
y = current[2];
point.setXY(current[1], current[2]);
bounds = [];
break;

case 'M': // moveTo, absolute
x = current[1];
y = current[2];
subpathStartX = x;
subpathStartY = y;
point.setXY(current[1], current[2]);
subpathStart.setFromPoint(point);
prev.setFromPoint(point);
beforePrev.setFromPoint(point);
opening = true;
bounds = [];
break;

case 'C': // bezierCurveTo, absolute
bounds = fabric.util.getBoundsOfCurve(x, y,
bounds = fabric.util.getBoundsOfCurve(point.x, point.y,
current[1],
current[2],
current[3],
current[4],
current[5],
current[6]
);
x = current[5];
y = current[6];
point.setXY(current[5], current[6]);
break;

case 'Q': // quadraticCurveTo, absolute
bounds = fabric.util.getBoundsOfCurve(x, y,
bounds = fabric.util.getBoundsOfCurve(point.x, point.y,
current[1],
current[2],
current[1],
current[2],
current[3],
current[4]
);
x = current[3];
y = current[4];
point.setXY(current[3], current[4]);
break;

case 'z':
case 'Z':
x = subpathStartX;
y = subpathStartY;
point.setFromPoint(subpathStart);
closing = true;
break;
}

if (this.strokeLineJoin === 'miter') {
if (!opening && !second) {
projectedPoints.push.apply(
projectedPoints,
this._projectStrokeOnSegmentBBox(prev, beforePrev, point, prevBounds)
);
}
if (closing) {
// project stroke on sub path start
projectedPoints.push.apply(
projectedPoints,
this._projectStrokeOnSegmentBBox(subpathStart, prev, subpathSecond, bounds)
);
}
}

aX.push(point.x);
aY.push(point.y);

bounds.forEach(function (point) {
aX.push(point.x);
aY.push(point.y);
});
aX.push(x);
aY.push(y);

if (second) {
subpathSecond.setFromPoint(point);
second = false;
}

}

var minX = min(aX) || 0,
minY = min(aY) || 0,
maxX = max(aX) || 0,
maxY = max(aY) || 0,
deltaX = maxX - minX,
deltaY = maxY - minY;
projectedPoints.forEach(function (point) {
aX.push(point.x);
aY.push(point.y);
});

var minPoint = new fabric.Point(min(aX) || 0, min(aY) || 0),
maxPoint = new fabric.Point(max(aX) || 0, max(aY) || 0),
delta = maxPoint.subtract(minPoint);

return {
left: minX,
top: minY,
width: deltaX,
height: deltaY
left: minPoint.x,
top: minPoint.y,
width: delta.x,
height: delta.y
};
}
},

_setPositionDimensions: function (options) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a wretched function

options || (options = {});
var calcDim = this._calcDimensions(options), origin;
this.width = calcDim.width;
this.height = calcDim.height;
if (!options.fromSVG) {
origin = this.translateToGivenOrigin(
// this looks bad, but is one way to keep it optional for now.
new fabric.Point(
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't like this. Because it doesn't take into account the translation caused by stroke width

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resolved I believe

calcDim.left,
calcDim.top
),
'left',
'top',
this.originX,
this.originY
);
}
if (typeof options.left === 'undefined') {
this.left = options.fromSVG ? calcDim.left : origin.x;
}
if (typeof options.top === 'undefined') {
this.top = options.fromSVG ? calcDim.top : origin.y;
}
this.pathOffset = new fabric.Point(
calcDim.left + this.width / 2,
calcDim.top + this.height / 2
);
},

/**
* @override stroke is already accounted for in size
* @returns {fabric.Point} dimensions
*/
_getNonTransformedDimensions: function () {
return new fabric.Point(this.width, this.height);
},

});

/**
Expand Down
70 changes: 44 additions & 26 deletions src/util/misc.js
Original file line number Diff line number Diff line change
Expand Up @@ -180,17 +180,18 @@
},

/**
* Let A, B, C be vertexes of a triangle, calculate the bisector of A
* @static
* @memberOf fabric.util
* @param {Point} A
* @param {Point} B
* @param {Point} C
* @param {Point} A the vertex of the bisector
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wrong desc

* @param {Point} B a vertex that defines a side of the angle
* @param {Point} C a second vertex that defines the other side of the angle
* @returns {{ vector: Point, angle: number }} vector representing the bisector of A and A's angle
*/
getBisector: function (A, B, C) {
var AB = fabric.util.createVector(A, B), AC = fabric.util.createVector(A, C);
var alpha = fabric.util.calcAngleBetweenVectors(AB, AC);
// check if alpha is relative to AB->BC
// check if alpha is relative to AB->AC
var ro = fabric.util.calcAngleBetweenVectors(fabric.util.rotateVector(AB, alpha), AC);
var phi = alpha * (ro === 0 ? 1 : -1) / 2;
return {
Expand All @@ -199,6 +200,44 @@
};
},

/**
* Calculates the stroke projection vector to apply to `point`
* @static
* @memberOf fabric.util
* @param {Point} point the point to project
* @param {Point} pointBefore a vertex that defines one side of the angle
* @param {Point} pointAfter a vertex that defines a second side of the angle
* @param {Object} options
* @param {number} options.strokeWidth
* @param {'miter'|'bevel'|'round'} options.strokeLineJoin
* @param {number} options.strokeMiterLimit https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/stroke-miterlimit
* @param {boolean} options.strokeUniform
* @param {number} options.scaleX
* @param {number} options.scaleY
* @returns {fabric.Point} a vector representing the stroke projection (direction sign can't be determined)
*/
calcStrokeProjection: function (point, pointBefore, pointAfter, options) {
var s = options.strokeWidth / 2,
strokeUniformScalar = options.strokeUniform ?
new fabric.Point(1 / options.scaleX, 1 / options.scaleY) :
new fabric.Point(1, 1),
bisector = fabric.util.getBisector(point, pointBefore, pointAfter),
bisectorVector = bisector.vector.multiply(strokeUniformScalar),
alpha = bisector.angle,
scalar = -s / Math.sin(alpha / 2),
miterVector = bisectorVector.scalarMultiply(scalar);
if (options.strokeLineJoin === 'miter' &&
Math.hypot(miterVector.x, miterVector.y) / s <= options.strokeMiterLimit) {
return miterVector;
}
else {
// calculate bevel, round projections
// incorrect approximation
scalar = -s * Math.SQRT2;
return bisectorVector.scalarMultiply(scalar);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

doesn't cover bevel/round cases properly

}
},

/**
* Project stroke width on points returning 2 projections for each point as follows:
* - `miter`: 2 points corresponding to the outer boundary and the inner boundary of stroke.
Expand Down Expand Up @@ -241,28 +280,7 @@
B = points[index - 1];
C = points[index + 1];
}
var bisector = fabric.util.getBisector(A, B, C),
bisectorVector = bisector.vector,
alpha = bisector.angle,
scalar,
miterVector;
if (options.strokeLineJoin === 'miter') {
scalar = -s / Math.sin(alpha / 2);
miterVector = new fabric.Point(
bisectorVector.x * scalar * strokeUniformScalar.x,
bisectorVector.y * scalar * strokeUniformScalar.y
);
if (Math.hypot(miterVector.x, miterVector.y) / s <= options.strokeMiterLimit) {
coords.push(A.add(miterVector));
coords.push(A.subtract(miterVector));
return;
}
}
scalar = -s * Math.SQRT2;
miterVector = new fabric.Point(
bisectorVector.x * scalar * strokeUniformScalar.x,
bisectorVector.y * scalar * strokeUniformScalar.y
);
var miterVector = fabric.util.calcStrokeProjection(A, B, C, options);
coords.push(A.add(miterVector));
coords.push(A.subtract(miterVector));
});
Expand Down
10 changes: 2 additions & 8 deletions src/util/path.js
Original file line number Diff line number Diff line change
Expand Up @@ -187,14 +187,8 @@
bounds[0][jlen + 1] = x3;
bounds[1][jlen + 1] = y3;
var result = [
{
x: min.apply(null, bounds[0]),
y: min.apply(null, bounds[1])
},
{
x: max.apply(null, bounds[0]),
y: max.apply(null, bounds[1])
}
new fabric.Point(min.apply(null, bounds[0]), min.apply(null, bounds[1])),
new fabric.Point(max.apply(null, bounds[0]), max.apply(null, bounds[1]))
];
if (fabric.cachesBoundsOfCurve) {
fabric.boundsOfCurveCache[argsString] = result;
Expand Down
Loading