-
-
Notifications
You must be signed in to change notification settings - Fork 7.9k
Stroked path width #17198
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
base: main
Are you sure you want to change the base?
Stroked path width #17198
Conversation
5a401ee
to
7d84966
Compare
7d84966
to
0a92c2f
Compare
0a92c2f
to
265caec
Compare
Looks good to me. I wonder what's going to with the AppVeyor and Azure checks. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Approving modulo CI passing
Once this is merged, we could use this in |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I only really read docs so far.
lib/matplotlib/path.py
Outdated
form a corner, the angle swept out by the two lines that meet at the | ||
vertex. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm confused what this means with relation to the next member, especially since that one has corner in the name.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch. This was bad copy/paste from an older version where the names were too difficult to remember (phi/theta, as below). I fixed the description here to actually make sense.
---------- | ||
extents : 4*[float] | ||
The extents (xmin, ymin, xmax, ymax) of the `~.transforms.Bbox` of the | ||
vertices. Modified in place so that the corner described by *vinfo* |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The type should be more explicit then; it has to a mutable reference of some sort to work (i.e., not tuples.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is (4,) array_like of float
better?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe, I'm not sure we have a convention for must-be-mutable, @timhoffm?
265caec
to
48fe47a
Compare
Thanks @QuLogic as usual for your detailed documentation read, I'm glad you caught these lingering copy/paste errors, as they made the code hard to go back to even for me. Hopefully these documentation fixes help the code make lots more sense now. Let me know if you have any issues parsing what something is supposed to be doing. I assume you'll ask this question when you actually read the code itself: "why did he get the angle information in global coordinates, then include the logic to pretend all corners point in the positive x direction right at the end?" The formulas are much simpler if you fix the corners to be aligned in on specific direction, it turns out. I tried both ways (the other way being to include the switching logic for which direction the corner is pointing directly in the code that's currently in "stroke_x_overflow") and I found this one much more readable. |
48fe47a
to
efceb57
Compare
efceb57
to
dad23ba
Compare
The original idea was that ``4*[float]`` was at least not ``4*(float, )``...
…On Mon, Aug 31, 2020 at 4:42 PM Elliott Sales de Andrade < ***@***.***> wrote:
***@***.**** commented on this pull request.
------------------------------
In lib/matplotlib/path.py
<#17198 (comment)>
:
> + elif joinstyle == 'round':
+ return width/2 # hemispherical cap, so always same padding
+ else:
+ raise ValueError(f"Unknown joinstyle: {joinstyle}")
+
+
+def _pad_extents_stroked_vertex(extents, vinfo, markeredgewidth, joinstyle,
+ capstyle):
+ """
+ Accumulator for building true extents from `.VertexInfo`s.
+
+ Parameters
+ ----------
+ extents : 4*[float]
+ The extents (xmin, ymin, xmax, ymax) of the `~.transforms.Bbox` of the
+ vertices. Modified in place so that the corner described by *vinfo*
Maybe, I'm not sure we have a convention for must-be-mutable, @timhoffm
<https://github.com/timhoffm>?
—
You are receiving this because you authored the thread.
Reply to this email directly, view it on GitHub
<#17198 (comment)>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AALIGPUEAIH5ZFF7Q26TRTTSDQYPHANCNFSM4MML7XCA>
.
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was pretty-well commented, which made it easier to review. I mean, it still took hours, but probably less than if it weren't.
cap_angles = [first_tan_angle, prev_tan_angle] | ||
cap_vertices = [first_vertex, prev_vertex] | ||
for cap_angle, cap_vertex in zip(cap_angles, cap_vertices): | ||
yield VertexInfo(cap_vertex, cap_angle, None) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just yielding the two things seems more straightforward?
cap_angles = [first_tan_angle, prev_tan_angle] | |
cap_vertices = [first_vertex, prev_vertex] | |
for cap_angle, cap_vertex in zip(cap_angles, cap_vertices): | |
yield VertexInfo(cap_vertex, cap_angle, None) | |
yield VertexInfo(first_vertex, first_tan_angle, None) | |
yield VertexInfo(prev_vertex, prev_tan_angle, None) |
# deal with capping ends of previous polyline, if it exists | ||
if prev_tan_angle is not None and is_capped: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Capping occurs if the polyline was unclosed, so I'm 50/50 whether is_capped
should be is_closed
(with opposite values), or this comment should mention that.
for cap_angle, cap_vertex in [(first_tan_angle, first_vertex), | ||
(prev_tan_angle, prev_vertex)]: | ||
yield VertexInfo(cap_vertex, cap_angle, None) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same here.
for cap_angle, cap_vertex in [(first_tan_angle, first_vertex), | |
(prev_tan_angle, prev_vertex)]: | |
yield VertexInfo(cap_vertex, cap_angle, None) | |
yield VertexInfo(first_vertex, first_tan_angle, None) | |
yield VertexInfo(prev_vertex, prev_tan_angle, None) |
# often CLOSEPOLY is used when the curve has already reached | ||
# it's initial point in order to prevent there from being a |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
# often CLOSEPOLY is used when the curve has already reached | |
# it's initial point in order to prevent there from being a | |
# Often CLOSEPOLY is used when the curve has already reached | |
# its initial point in order to prevent there from being a |
---------- | ||
markeredgewidth : float | ||
Width, in points, of the stroke used to create the marker's edge. | ||
For ``markeredgewidth = 0``, same as `.get_extents`. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For ``markeredgewidth = 0``, same as `.get_extents`. | |
For ``markeredgewidth = 0``, the result will become the same as `.get_extents`. |
phi1 = phi + theta/2 | ||
phi2 = phi - theta/2 | ||
return (width/2) * max(np.abs(np.cos(phi1)), np.abs(np.cos(phi2))) | ||
# finally, _joinstyle = "round" is just _joinstyle = "bevel" but with |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
?
# finally, _joinstyle = "round" is just _joinstyle = "bevel" but with | |
# finally, joinstyle = "round" is just joinstyle = "bevel" but with |
cap_perp = vinfo.incidence_angle + perp_dir | ||
x += (markeredgewidth/2) * np.cos(cap_perp) | ||
if x < extents[xmin]: | ||
extents[xmin] = x | ||
if x > extents[xmax]: | ||
extents[xmax] = x | ||
y += (markeredgewidth/2) * np.sin(cap_perp) | ||
if y < extents[ymin]: | ||
extents[ymin] = y | ||
if y > extents[ymax]: | ||
extents[ymax] = y |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure if it looks better this way (ditto the others), or worse and slower...
cap_perp = vinfo.incidence_angle + perp_dir
x += (markeredgewidth/2) * np.cos(cap_perp)
extents[xmin] = min(extents[xmin], x)
extents[xmax] = max(extents[xmax], x)
y += (markeredgewidth/2) * np.sin(cap_perp)
extents[ymin] = min(extents[ymin], y)
extents[ymax] = max(extents[ymax], y)
# of the bbox, and see if the stroked vertex expands the extents... | ||
x, y = vinfo.apex | ||
if np.cos(vinfo.incidence_angle) > 0: | ||
incidence_angle = vinfo.incidence_angle + np.pi/2 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In these cases, I'd name the local variable theta
, as the reference for the angle is different (I think?), so it's different than incidence_angle
.
For vertices with one incoming line, *phi* is the incidence angle that | ||
line forms with the positive y-axis. For corners (vertices with two |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Clockwise, or counter-clockwise?
@image_comparison(['stroked_bbox'], remove_text=True, | ||
extensions=['pdf', 'svg', 'png']) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These are the default extensions?
@image_comparison(['stroked_bbox'], remove_text=True, | |
extensions=['pdf', 'svg', 'png']) | |
@image_comparison(['stroked_bbox'], remove_text=True) |
Since this Pull Request has not been updated in 60 days, it has been marked "inactive." This does not mean that it will be closed, though it may be moved to a "Draft" state. This helps maintainers prioritize their reviewing efforts. You can pick the PR back up anytime - please ping us if you need a review or guidance to move the PR forward! If you do not plan on continuing the work, please let us know so that we can either find someone to take the PR over, or close it. |
PR Summary
Introduces code to
path.py
andbezier.py
that allows us to compute the extents of strokedPath
s analytically. Required for bugfix PRs #17182 and #17199, which fixes #16606.Previous Behavior
The following code should nominally get the extents of the path, but the current
get_extents
only takes into account the control points:New Behavior
Now we can use the
Path.get_stroked_extents
method:Saving as a PDF or PNG produces the following:
Remaining bugs
If we instead output an SVG, it's wrong (because the default miterlimit is different). To fix this, we would need to deal with #9830, because matplotlib doesn't set the miterlimit right now. This feels acceptably out of scope for this PR. This PR contains all the code required to deal with an arbitrary miterlimit, but it has been hardcoded to 10, since that's the nominal miterlimit for Agg, which is in charge of rasterizing, allowing us to fix #17182.
PR Checklist