renderer: improved the paint bounding box accuracy

previously, the bounding box calculation was simply
determined by comparing all the points, which led to
incorrect sizing due to Bezier control points.

Now, it accurately computes the curve boundary,
properly addressing this issue.
This commit is contained in:
Hermet Park 2025-03-03 16:21:29 +09:00 committed by Hermet Park
parent 82a9a05405
commit 30a5f2891b
5 changed files with 100 additions and 21 deletions

View file

@ -100,6 +100,13 @@ float _bezAt(const Bezier& bz, float at, float length, LengthFunc lineLengthFunc
namespace tvg { namespace tvg {
uint8_t lerp(const uint8_t &start, const uint8_t &end, float t)
{
return static_cast<uint8_t>(tvg::clamp(static_cast<int>(start + (end - start) * t), 0, 255));
}
float length(const PathCommand* cmds, uint32_t cmdsCnt, const Point* pts, uint32_t ptsCnt) float length(const PathCommand* cmds, uint32_t cmdsCnt, const Point* pts, uint32_t ptsCnt)
{ {
if (ptsCnt < 2) return 0.0f; if (ptsCnt < 2) return 0.0f;
@ -408,9 +415,38 @@ float Bezier::angle(float t) const
} }
uint8_t lerp(const uint8_t &start, const uint8_t &end, float t) void Bezier::bounds(Point& min, Point& max) const
{ {
return static_cast<uint8_t>(tvg::clamp(static_cast<int>(start + (end - start) * t), 0, 255)); if (min.x > start.x) min.x = start.x;
if (min.y > start.y) min.y = start.y;
if (min.x > end.x) min.x = end.x;
if (min.y > end.y) min.y = end.y;
if (max.x < start.x) max.x = start.x;
if (max.y < start.y) max.y = start.y;
if (max.x < end.x) max.x = end.x;
if (max.y < end.y) max.y = end.y;
//find x/y-direction extrema (solving derivative of Bezier curve)
auto findMinMax = [&](float start, float ctrl1, float ctrl2, float end, float& min, float& max) -> void {
auto a = -1.0f * start + 3.0f * ctrl1 - 3.0f * ctrl2 + end;
auto b = start - 2.0f * ctrl1 + ctrl2;
auto c = -1.0f * start + ctrl1;
auto h = b * b - a * c;
if (h <= 0.0f) return;
h = sqrtf(h);
float t[2] = {(-b - h) / a, (-b + h) / a};
for (int i = 0; i < 2; ++i) {
if (t[i] <= 0.0f || t[i] >= 1.0f) continue;
auto s = 1.0f - t[i];
auto q = s * s * s * start + 3.0f * s * s * t[i] * ctrl1 + 3.0f * s * t[i] * t[i] * ctrl2 + t[i] * t[i] * t[i] * end;
if (q < min) min = q;
if (q > max) max = q;
}
};
findMinMax(start.x, ctrl1.x, ctrl2.x, end.x, min.x, max.x);
findMinMax(start.y, ctrl1.y, ctrl2.y, end.y, min.y, max.y);
} }
} }

View file

@ -344,6 +344,7 @@ struct Bezier
float atApprox(float at, float length) const; float atApprox(float at, float length) const;
Point at(float t) const; Point at(float t) const;
float angle(float t) const; float angle(float t) const;
void bounds(Point& min, Point& max) const;
}; };

View file

@ -40,6 +40,64 @@ uint32_t RenderMethod::unref()
return (--refCnt); return (--refCnt);
} }
/************************************************************************/
/* RenderPath Class Implementation */
/************************************************************************/
bool RenderPath::bounds(float* x, float* y, float* w, float* h)
{
//unexpected
if (cmds.empty() || cmds.first() == PathCommand::CubicTo) return false;
auto min = Point{FLT_MAX, FLT_MAX};
auto max = Point{-FLT_MAX, -FLT_MAX};
auto pt = pts.begin();
auto cmd = cmds.begin();
auto assign = [&](Point* pt, Point& min, Point& max) -> void {
if (pt->x < min.x) min.x = pt->x;
if (pt->y < min.y) min.y = pt->y;
if (pt->x > max.x) max.x = pt->x;
if (pt->y > max.y) max.y = pt->y;
};
while (cmd < cmds.end()) {
switch (*cmd) {
case PathCommand::MoveTo: {
//skip the invalid assignments
if (cmd + 1 < cmds.end()) {
auto next = *(cmd + 1);
if (next == PathCommand::LineTo || next == PathCommand::CubicTo) {
assign(pt, min, max);
}
}
++pt;
break;
}
case PathCommand::LineTo: {
assign(pt, min, max);
++pt;
break;
}
case PathCommand::CubicTo: {
Bezier bz = {pt[-1], pt[0], pt[1], pt[2]};
bz.bounds(min, max);
pt += 3;
break;
}
default: break;
}
++cmd;
}
if (x) *x = min.x;
if (y) *y = min.y;
if (w) *w = max.x - min.x;
if (h) *h = max.y - min.y;
return true;
}
/************************************************************************/ /************************************************************************/
/* RenderRegion Class Implementation */ /* RenderRegion Class Implementation */

View file

@ -104,6 +104,7 @@ struct RenderPath
cmds.clear(); cmds.clear();
} }
bool bounds(float* x, float* y, float* w, float* h);
}; };
struct RenderTrimPath struct RenderTrimPath

View file

@ -118,24 +118,7 @@ struct Shape::Impl : Paint::Impl
bool bounds(float* x, float* y, float* w, float* h, bool stroking) bool bounds(float* x, float* y, float* w, float* h, bool stroking)
{ {
//Path bounding size if (!rs.path.bounds(x, y, w, h)) return false;
if (rs.path.pts.count > 0 ) {
auto pts = rs.path.pts.begin();
Point min = { pts->x, pts->y };
Point max = { pts->x, pts->y };
for (auto pts2 = pts + 1; pts2 < rs.path.pts.end(); ++pts2) {
if (pts2->x < min.x) min.x = pts2->x;
if (pts2->y < min.y) min.y = pts2->y;
if (pts2->x > max.x) max.x = pts2->x;
if (pts2->y > max.y) max.y = pts2->y;
}
if (x) *x = min.x;
if (y) *y = min.y;
if (w) *w = max.x - min.x;
if (h) *h = max.y - min.y;
}
//Stroke feathering //Stroke feathering
if (stroking && rs.stroke) { if (stroking && rs.stroke) {
@ -144,7 +127,7 @@ struct Shape::Impl : Paint::Impl
if (w) *w += rs.stroke->width; if (w) *w += rs.stroke->width;
if (h) *h += rs.stroke->width; if (h) *h += rs.stroke->width;
} }
return rs.path.pts.count > 0 ? true : false; return true;
} }
void reserveCmd(uint32_t cmdCnt) void reserveCmd(uint32_t cmdCnt)