d2d1: Implement path_geometry_StrokeContainsPoint() for Bézier segments.

Signed-off-by: Henri Verbeet <hverbeet@codeweavers.com>
Signed-off-by: Alexandre Julliard <julliard@winehq.org>
This commit is contained in:
David White 2021-12-08 20:14:22 +01:00 committed by Alexandre Julliard
parent f467f6385e
commit b495ff5cc8
2 changed files with 285 additions and 6 deletions

View file

@ -586,6 +586,172 @@ static BOOL d2d_point_on_line_segment(const D2D1_POINT_2F *q, const D2D1_POINT_2
return fabsf(d2d_point_dot(&v_q, &v_n)) < tolerance;
}
/* Approximate the Bézier segment with a (wide) line segment. If the point
* lies outside the approximation, we're done. If the width of the
* approximation is less than the tolerance and the point lies inside, we're
* also done. If neither of those is the case, we subdivide the Bézier segment
* and try again. */
static BOOL d2d_point_on_bezier_segment(const D2D1_POINT_2F *q, const D2D1_POINT_2F *p0,
const D2D1_BEZIER_SEGMENT *b, const D2D1_MATRIX_3X2_F *transform, float stroke_width, float tolerance)
{
float d1, d2, d3, d4, d, l, m, w, w2;
D2D1_POINT_2F t[7], start, end, v_p;
D2D1_BEZIER_SEGMENT b0, b1;
m = 1.0f;
w = stroke_width * 0.5f;
d2d_point_subtract(&v_p, &b->point3, p0);
/* If the endpoints coincide, use the line through the control points as
* the direction vector. That choice is somewhat arbitrary; other choices
* with tighter error bounds exist. */
if ((l = d2d_point_dot(&v_p, &v_p)) == 0.0f)
{
d2d_point_subtract(&v_p, &b->point2, &b->point1);
/* If the control points also coincide, the curve is in fact a line. */
if ((l = d2d_point_dot(&v_p, &v_p)) == 0.0f)
{
d2d_point_subtract(&v_p, &b->point1, p0);
end.x = p0->x + 0.75f * v_p.x;
end.y = p0->y + 0.75f * v_p.y;
return d2d_point_on_line_segment(q, p0, &end, transform, w, tolerance);
}
m = 0.0f;
}
l = sqrtf(l);
d2d_point_scale(&v_p, 1.0f / l);
m *= l;
/* Calculate the width w2 of the approximation. */
end.x = p0->x + v_p.x;
end.y = p0->y + v_p.y;
/* Here, d1 and d2 are the maximum (signed) distance of the control points
* from the line through the start and end points. */
d1 = d2d_point_ccw(p0, &end, &b->point1);
d2 = d2d_point_ccw(p0, &end, &b->point2);
/* It can be shown that if the control points of a cubic Bézier curve lie
* on the same side of the line through the endpoints, the distance of the
* curve itself to that line will be within 3/4 of the distance of the
* control points to that line; if the control points lie on opposite
* sides, that distance will be within 4/9 of the distance of the
* corresponding control point. We're taking that as a given here. */
if (d1 * d2 > 0.0f)
{
d1 *= 0.75f;
d2 *= 0.75f;
}
else
{
d1 = (d1 * 4.0f) / 9.0f;
d2 = (d2 * 4.0f) / 9.0f;
}
w2 = max(fabsf(d1), fabsf(d2));
/* Project the control points onto the line through the endpoints of the
* curve. We will use these to calculate the endpoints of the
* approximation. */
d2d_point_subtract(&t[1], &b->point1, p0);
d1 = d2d_point_dot(&v_p, &t[1]);
d2d_point_subtract(&t[2], &b->point2, p0);
d2 = d2d_point_dot(&v_p, &t[2]);
/* Calculate the start point of the approximation. Like further above, the
* actual curve is somewhat closer to the endpoints than the control
* points are. */
d = min(min(d1, d2), 0);
if (d1 * d2 > 0.0f)
d *= 0.75f;
else
d = (d * 4.0f) / 9.0f;
/* Account for the stroke width and tolerance around the endpoints by
* adjusting the endpoints here. This matters because there are no joins
* in the original geometry for the places where we subdivide the original
* curve. We do this here because it's easy; alternatively we could
* explicitly test for this when subdividing the curve further below. */
d -= min(w + tolerance, w2);
start.x = p0->x + d * v_p.x;
start.y = p0->y + d * v_p.y;
/* Calculate the end point of the approximation. */
d1 -= m;
d2 -= m;
d = max(max(d1, d2), 0);
if (d1 * d2 > 0.0f)
d = m + d * 0.75f;
else
d = m + (d * 4.0f) / 9.0f;
d += min(w2, w + tolerance);
end.x = p0->x + d * v_p.x;
end.y = p0->y + d * v_p.y;
/* Calculate the error bounds of the approximation. We do this in
* transformed space because we need these to be relative to the given
* tolerance. */
d2d_point_transform(&t[0], transform, p0->x, p0->y);
d2d_point_transform(&t[1], transform, b->point1.x, b->point1.y);
d2d_point_transform(&t[2], transform, b->point2.x, b->point2.y);
d2d_point_transform(&t[3], transform, b->point3.x, b->point3.y);
d2d_point_transform(&t[4], transform, start.x, start.y);
d2d_point_transform(&t[5], transform, end.x, end.y);
d2d_point_subtract(&t[6], &t[5], &t[4]);
l = d2d_point_length(&t[6]);
/* Here, d1 and d2 are the maximum (signed) distance of the control points
* from the line through the start and end points. */
d1 = d2d_point_ccw(&t[4], &t[5], &t[1]) / l;
d2 = d2d_point_ccw(&t[4], &t[5], &t[2]) / l;
if (d1 * d2 > 0.0f)
{
d1 *= 0.75f;
d2 *= 0.75f;
}
else
{
d1 = (d1 * 4.0f) / 9.0f;
d2 = (d2 * 4.0f) / 9.0f;
}
l = max(max(d1, d2), 0) - min(min(d1, d2), 0);
/* d3 and d4 are the (unsigned) distance of the endpoints of the
* approximation from the original endpoints. */
d2d_point_subtract(&t[6], &t[4], &t[0]);
d3 = d2d_point_length(&t[6]);
d2d_point_subtract(&t[6], &t[5], &t[3]);
d4 = d2d_point_length(&t[6]);
l = max(max(d3, d4), l);
/* If the error of the approximation is less than the tolerance, and Q
* lies on the approximation, the distance of Q to the stroked curve is
* definitely within the tolerance. */
if (l <= tolerance && d2d_point_on_line_segment(q, &start, &end, transform, w, tolerance - l))
return TRUE;
/* On the other hand, if the distance of Q to the stroked curve is more
* than the sum of the tolerance and d, the distance of Q to the stroked
* curve can't possibly be within the tolerance. */
if (!d2d_point_on_line_segment(q, &start, &end, transform, w + w2, tolerance))
return FALSE;
/* Subdivide the curve. Note that simply splitting the segment in half
* here works and is easy, but may not be optimal. We could potentially
* reduce the number of iterations we need to do by splitting based on
* curvature or segment length. */
d2d_point_lerp(&t[0], &b->point1, &b->point2, 0.5f);
b1.point3 = b->point3;
d2d_point_lerp(&b1.point2, &b->point3, &b->point2, 0.5f);
d2d_point_lerp(&b1.point1, &t[0], &b1.point2, 0.5f);
d2d_point_lerp(&b0.point1, p0, &b->point1, 0.5f);
d2d_point_lerp(&b0.point2, &t[0], &b0.point1, 0.5f);
d2d_point_lerp(&b0.point3, &b0.point2, &b1.point1, 0.5f);
return d2d_point_on_bezier_segment(q, p0, &b0, transform, stroke_width, tolerance)
|| d2d_point_on_bezier_segment(q, &b0.point3, &b1, transform, stroke_width, tolerance);
}
static void d2d_rect_union(D2D1_RECT_F *l, const D2D1_RECT_F *r)
{
l->left = min(l->left, r->left);
@ -3411,8 +3577,9 @@ static HRESULT STDMETHODCALLTYPE d2d_path_geometry_StrokeContainsPoint(ID2D1Path
{
struct d2d_geometry *geometry = impl_from_ID2D1PathGeometry(iface);
enum d2d_vertex_type type = D2D_VERTEX_TYPE_NONE;
unsigned int i, j, bezier_idx;
D2D1_BEZIER_SEGMENT b;
D2D1_POINT_2F p, p1;
unsigned int i, j;
TRACE("iface %p, point %s, stroke_width %.8e, stroke_style %p, transform %p, tolerance %.8e, contains %p.\n",
iface, debug_d2d_point_2f(&point), stroke_width, stroke_style, transform, tolerance, contains);
@ -3441,7 +3608,7 @@ static HRESULT STDMETHODCALLTYPE d2d_path_geometry_StrokeContainsPoint(ID2D1Path
break;
}
for (++j; j < figure->vertex_count; ++j)
for (bezier_idx = 0, ++j; j < figure->vertex_count; ++j)
{
if (figure->vertex_types[j] == D2D_VERTEX_TYPE_NONE
|| d2d_vertex_type_is_split_bezier(figure->vertex_types[j]))
@ -3455,6 +3622,14 @@ static HRESULT STDMETHODCALLTYPE d2d_path_geometry_StrokeContainsPoint(ID2D1Path
p = p1;
break;
case D2D_VERTEX_TYPE_BEZIER:
b.point1 = figure->original_bezier_controls[bezier_idx++];
b.point2 = figure->original_bezier_controls[bezier_idx++];
b.point3 = figure->vertices[j];
*contains = d2d_point_on_bezier_segment(&point, &p, &b, transform, stroke_width, tolerance);
p = b.point3;
break;
default:
FIXME("Unhandled vertex type %#x.\n", type);
p = figure->vertices[j];
@ -3465,11 +3640,22 @@ static HRESULT STDMETHODCALLTYPE d2d_path_geometry_StrokeContainsPoint(ID2D1Path
type = figure->vertex_types[j];
}
if (figure->flags & D2D_FIGURE_FLAG_CLOSED && (!*contains) && type == D2D_VERTEX_TYPE_LINE)
if (figure->flags & D2D_FIGURE_FLAG_CLOSED && (!*contains))
{
p1 = figure->vertices[0];
*contains = d2d_point_on_line_segment(&point, &p, &p1, transform, stroke_width * 0.5f, tolerance);
p = p1;
if (type == D2D_VERTEX_TYPE_LINE)
{
p1 = figure->vertices[0];
*contains = d2d_point_on_line_segment(&point, &p, &p1, transform, stroke_width * 0.5f, tolerance);
p = p1;
}
else if (d2d_vertex_type_is_bezier(type))
{
b.point1 = figure->original_bezier_controls[bezier_idx++];
b.point2 = figure->original_bezier_controls[bezier_idx++];
b.point3 = figure->vertices[0];
*contains = d2d_point_on_bezier_segment(&point, &p, &b, transform, stroke_width, tolerance);
p = b.point3;
}
}
if (*contains)

View file

@ -10244,6 +10244,67 @@ static void test_stroke_contains_point(BOOL d3d11)
{{{{0.0f}}}, {239.41f, 600.0f}, 0.1f, 1.0f, FALSE, TRUE},
{{{{0.0f}}}, {240.59f, 600.0f}, 0.1f, 1.0f, FALSE, TRUE},
{{{{0.0f}}}, {240.61f, 600.0f}, 0.1f, 1.0f, FALSE, FALSE},
/* 13. Curves. */
{{{{1.0f, 0.0f, 0.0f, 1.0f, 10.0f, 10.0f}}}, {170.0f, 418.6f}, 0.0f, 1.0f, TRUE, TRUE},
{{{{1.0f, 0.0f, 0.0f, 1.0f, 10.0f, 10.0f}}}, {170.0f, 420.1f}, 0.0f, 1.0f, TRUE, FALSE},
{{{{1.0f, 0.0f, 0.0f, 1.0f, 10.0f, 10.0f}}}, {170.0f, 417.7f}, 0.0f, 1.0f, TRUE, FALSE},
{{{{1.0f, 0.0f, 0.0f, 1.0f, 10.0f, 10.0f}}}, { 89.5f, 485.3f}, 0.1f, 1.0f, TRUE, TRUE},
{{{{1.0f, 0.0f, 0.0f, 1.0f, 10.0f, 10.0f}}}, { 90.5f, 485.3f}, 0.1f, 1.0f, TRUE, TRUE},
{{{{1.0f, 0.0f, 0.0f, 1.0f, 10.0f, 10.0f}}}, { 91.5f, 485.3f}, 0.1f, 1.0f, TRUE, FALSE},
{{{{1.0f, 0.0f, 0.0f, 1.0f, 10.0f, 10.0f}}}, { 89.0f, 485.3f}, 0.1f, 1.0f, TRUE, FALSE},
/* 20. A curve where the control points project beyond the endpoints
* onto the line through the endpoints. */
{{{{0.0f}}}, {306.97f, 791.67f}, 0.0f, 1.0f, FALSE, FALSE},
{{{{0.0f}}}, {307.27f, 791.67f}, 0.0f, 1.0f, FALSE, TRUE},
{{{{0.0f}}}, {308.47f, 791.67f}, 0.0f, 1.0f, FALSE, TRUE},
{{{{0.0f}}}, {308.77f, 791.67f}, 0.0f, 1.0f, FALSE, FALSE},
{{{{0.0f}}}, {350.00f, 824.10f}, 0.0f, 1.0f, FALSE, FALSE},
{{{{0.0f}}}, {350.00f, 824.40f}, 0.0f, 1.0f, FALSE, TRUE},
{{{{0.0f}}}, {350.00f, 825.60f}, 0.0f, 1.0f, FALSE, TRUE},
{{{{0.0f}}}, {350.00f, 825.90f}, 0.0f, 1.0f, FALSE, FALSE},
{{{{0.0f}}}, {391.23f, 791.67f}, 0.0f, 1.0f, FALSE, FALSE},
{{{{0.0f}}}, {391.53f, 791.67f}, 0.0f, 1.0f, FALSE, TRUE},
{{{{0.0f}}}, {392.73f, 791.67f}, 0.0f, 1.0f, FALSE, TRUE},
{{{{0.0f}}}, {393.03f, 791.67f}, 0.0f, 1.0f, FALSE, FALSE},
/* 32. A curve where the endpoints coincide. */
{{{{0.0f}}}, {570.23f, 799.77f}, 0.0f, 1.0f, FALSE, FALSE},
{{{{0.0f}}}, {570.53f, 799.77f}, 0.0f, 1.0f, FALSE, TRUE},
{{{{0.0f}}}, {571.73f, 799.77f}, 0.0f, 1.0f, FALSE, TRUE},
{{{{0.0f}}}, {572.03f, 799.77f}, 0.0f, 1.0f, FALSE, FALSE},
{{{{0.0f}}}, {600.00f, 824.10f}, 0.0f, 1.0f, FALSE, FALSE},
{{{{0.0f}}}, {600.00f, 824.40f}, 0.0f, 1.0f, FALSE, TRUE},
{{{{0.0f}}}, {600.00f, 825.60f}, 0.0f, 1.0f, FALSE, TRUE},
{{{{0.0f}}}, {600.00f, 825.90f}, 0.0f, 1.0f, FALSE, FALSE},
{{{{0.0f}}}, {627.97f, 799.77f}, 0.0f, 1.0f, FALSE, FALSE},
{{{{0.0f}}}, {628.27f, 799.77f}, 0.0f, 1.0f, FALSE, TRUE},
{{{{0.0f}}}, {629.47f, 799.77f}, 0.0f, 1.0f, FALSE, TRUE},
{{{{0.0f}}}, {629.77f, 799.77f}, 0.0f, 1.0f, FALSE, FALSE},
/* 44. A curve with coinciding endpoints, as well as coinciding
* control points. */
{{{{0.0f}}}, {825.00f, 800.00f}, 0.0f, 1.0f, FALSE, TRUE},
{{{{0.0f}}}, {861.00f, 824.00f}, 0.0f, 1.0f, FALSE, TRUE},
{{{{0.0f}}}, {861.00f, 826.00f}, 0.0f, 1.0f, FALSE, FALSE},
{{{{0.0f}}}, {862.50f, 825.00f}, 0.0f, 1.0f, FALSE, TRUE},
{{{{0.0f}}}, {864.00f, 824.00f}, 0.0f, 1.0f, FALSE, FALSE},
{{{{0.0f}}}, {864.00f, 826.00f}, 0.0f, 1.0f, FALSE, FALSE},
/* 50. Shear transforms. */
{{{{1.0f, 0.0f, 1.0f, 1.0f}}}, { 837.2f, 600.0f}, 0.1f, 5.0f, TRUE, FALSE},
{{{{1.0f, 0.0f, 1.0f, 1.0f}}}, { 837.5f, 600.0f}, 0.1f, 5.0f, TRUE, TRUE},
{{{{1.0f, 0.0f, 1.0f, 1.0f}}}, {1186.3f, 791.7f}, 0.1f, 5.0f, TRUE, TRUE},
{{{{1.0f, 0.0f, 1.0f, 1.0f}}}, {1186.6f, 791.7f}, 0.1f, 5.0f, TRUE, FALSE},
{{{{1.0f, 0.0f, 1.0f, 1.0f}}}, {1425.0f, 827.3f}, 0.1f, 5.0f, TRUE, TRUE},
{{{{1.0f, 0.0f, 1.0f, 1.0f}}}, {1425.0f, 827.6f}, 0.1f, 5.0f, TRUE, FALSE},
{{{{1.0f, 0.0f, 1.0f, 1.0f}}}, {1620.1f, 800.0f}, 0.1f, 5.0f, TRUE, FALSE},
{{{{1.0f, 0.0f, 1.0f, 1.0f}}}, {1620.4f, 800.0f}, 0.1f, 5.0f, TRUE, TRUE},
};
hr = D2D1CreateFactory(D2D1_FACTORY_TYPE_SINGLE_THREADED, &IID_ID2D1Factory, NULL, (void **)&factory);
@ -10273,6 +10334,19 @@ static void test_stroke_contains_point(BOOL d3d11)
hr = ID2D1PathGeometry_Open(path, &sink);
ok(hr == S_OK, "Got unexpected hr %#x.\n", hr);
/* A limaçon. */
set_point(&point, 160.0f, 720.0f);
ID2D1GeometrySink_BeginFigure(sink, point, D2D1_FIGURE_BEGIN_FILLED);
cubic_to(sink, 119.0f, 720.0f, 83.0f, 600.0f, 80.0f, 474.0f);
cubic_to(sink, 78.0f, 349.0f, 108.0f, 245.0f, 135.0f, 240.0f);
cubic_to(sink, 163.0f, 235.0f, 180.0f, 318.0f, 176.0f, 370.0f);
cubic_to(sink, 171.0f, 422.0f, 149.0f, 422.0f, 144.0f, 370.0f);
cubic_to(sink, 140.0f, 318.0f, 157.0f, 235.0f, 185.0f, 240.0f);
cubic_to(sink, 212.0f, 245.0f, 242.0f, 349.0f, 240.0f, 474.0f);
cubic_to(sink, 238.0f, 600.0f, 201.0f, 720.0f, 160.0f, 720.0f);
ID2D1GeometrySink_EndFigure(sink, D2D1_FIGURE_END_CLOSED);
/* Some straight lines. */
set_point(&point, 160.0f, 240.0f);
ID2D1GeometrySink_BeginFigure(sink, point, D2D1_FIGURE_BEGIN_FILLED);
line_to(sink, 240.0f, 240.0f);
@ -10280,6 +10354,25 @@ static void test_stroke_contains_point(BOOL d3d11)
line_to(sink, 160.0f, 720.0f);
ID2D1GeometrySink_EndFigure(sink, D2D1_FIGURE_END_OPEN);
/* Projected control points extending beyond the line segment through the
* endpoints. */
set_point(&point, 325.0f, 750.0f);
ID2D1GeometrySink_BeginFigure(sink, point, D2D1_FIGURE_BEGIN_FILLED);
cubic_to(sink, 250.0f, 850.0f, 450.0f, 850.0f, 375.0f, 750.0f);
ID2D1GeometrySink_EndFigure(sink, D2D1_FIGURE_END_OPEN);
/* Coinciding endpoints. */
set_point(&point, 600.0f, 750.0f);
ID2D1GeometrySink_BeginFigure(sink, point, D2D1_FIGURE_BEGIN_FILLED);
cubic_to(sink, 500.0f, 850.0f, 700.0f, 850.0f, 600.0f, 750.0f);
ID2D1GeometrySink_EndFigure(sink, D2D1_FIGURE_END_OPEN);
/* Coinciding endpoints, as well as coinciding control points. */
set_point(&point, 750.0f, 750.0f);
ID2D1GeometrySink_BeginFigure(sink, point, D2D1_FIGURE_BEGIN_FILLED);
cubic_to(sink, 900.0f, 850.0f, 900.0f, 850.0f, 750.0f, 750.0f);
ID2D1GeometrySink_EndFigure(sink, D2D1_FIGURE_END_OPEN);
hr = ID2D1GeometrySink_Close(sink);
ok(hr == S_OK, "Got unexpected hr %#x.\n", hr);
ID2D1GeometrySink_Release(sink);