#include "core/ui/stroke_aa.h" #include #include namespace reasampler::ui { namespace { inline float clamp01(float v) { return v < 0.0f ? 0.0f : (v > 1.0f ? 1.0f : v); } } // namespace void StrokeCanvas::reset(const Rect& bounds) { bounds_ = bounds; if (bounds_.empty()) { bounds_ = Rect{}; return; } const std::size_t area = static_cast(bounds_.width) * static_cast(bounds_.height); if (coverage_.size() < area) coverage_.resize(area); rowLo_.assign(static_cast(bounds_.height), bounds_.width); rowHi_.assign(static_cast(bounds_.height), 0); } float StrokeCanvas::coverageAt(int x, int y) const { if (!contains(bounds_, x, y)) return 0.0f; const int rel = x - bounds_.x; if (rel < rowLo(y) || rel >= rowHi(y)) return 0.0f; return rowData(y)[rel]; } // Grows a row's valid span to cover [x0, x1) (canvas-relative), zero-filling only the cells that // become valid. Cost is bounded by the growth, so a whole stroke stays O(ink). void StrokeCanvas::extendRow(int y, int x0, int x1) { const std::size_t r = static_cast(y - bounds_.y); float* row = coverage_.data() + r * static_cast(bounds_.width); int lo = rowLo_[r]; int hi = rowHi_[r]; if (hi <= lo) { std::fill(row + x0, row + x1, 0.0f); rowLo_[r] = x0; rowHi_[r] = x1; return; } // A stroke can revisit a row far from where it left it (an arc touches most rows on both // sides of the circle), so the gap between the old span and the new one must be zeroed too. if (x0 < lo) { std::fill(row + x0, row + lo, 0.0f); rowLo_[r] = x0; } if (x1 > hi) { std::fill(row + hi, row + x1, 0.0f); rowHi_[r] = x1; } } void StrokeCanvas::addPiece(float ax, float ay, float bx, float by, float halfWidth) { // addSegment (the only caller) guarantees finite inputs before this point. const float reach = halfWidth + 0.5f; // beyond this the coverage is 0 const float dx = bx - ax; const float dy = by - ay; const float len2 = dx * dx + dy * dy; const float invLen2 = len2 > 0.0f ? 1.0f / len2 : 0.0f; // A pixel can only take ink when its centre (x+0.5) is within `reach` of the piece, i.e. // x + 0.5 < maxX + reach — so the exclusive upper bound is floor(maxX + reach + 0.5), not // ceil(maxX + reach) + 1 (a whole extra pixel of guaranteed-zero coverage on every side). int x0 = static_cast(std::ceil((std::min)(ax, bx) - reach - 0.5f)); int x1 = static_cast(std::floor((std::max)(ax, bx) + reach + 0.5f)); int y0 = static_cast(std::ceil((std::min)(ay, by) - reach - 0.5f)); int y1 = static_cast(std::floor((std::max)(ay, by) + reach + 0.5f)); x0 = (std::max)(x0, bounds_.x); y0 = (std::max)(y0, bounds_.y); x1 = (std::min)(x1, bounds_.right()); y1 = (std::min)(y1, bounds_.bottom()); if (x0 >= x1 || y0 >= y1) return; const int relX0 = x0 - bounds_.x; const int relX1 = x1 - bounds_.x; for (int y = y0; y < y1; ++y) { extendRow(y, relX0, relX1); float* row = coverage_.data() + static_cast(y - bounds_.y) * static_cast(bounds_.width); const float pyc = static_cast(y) + 0.5f; const float qy = pyc - ay; for (int x = x0; x < x1; ++x) { const float qx = static_cast(x) + 0.5f - ax; const float t = clamp01((qx * dx + qy * dy) * invLen2); const float ex = qx - dx * t; const float ey = qy - dy * t; const float cov = clamp01(reach - std::sqrt(ex * ex + ey * ey)); float& dst = row[x - bounds_.x]; if (cov > dst) dst = cov; } } } void StrokeCanvas::addSegment(float ax, float ay, float bx, float by, float halfWidth) { if (bounds_.empty() || halfWidth <= 0.0f) return; // Must guard here, not in addPiece: len2 below goes NaN/Inf on a bad input too, so the // `len2 <= kMaxPieceLen^2` comparison is false either way (NaN compares false against // anything) and the pieces-count cast a few lines down is reached as UB regardless of which // branch is taken. if (!(std::isfinite(ax) && std::isfinite(ay) && std::isfinite(bx) && std::isfinite(by) && std::isfinite(halfWidth))) { return; } const float dx = bx - ax; const float dy = by - ay; const float len2 = dx * dx + dy * dy; if (len2 <= kMaxPieceLen * kMaxPieceLen) { addPiece(ax, ay, bx, by, halfWidth); return; } const int pieces = static_cast(std::sqrt(len2) / kMaxPieceLen) + 1; float px = ax; float py = ay; for (int i = 1; i <= pieces; ++i) { const float t = static_cast(i) / static_cast(pieces); const float qx = ax + dx * t; const float qy = ay + dy * t; addPiece(px, py, qx, qy, halfWidth); px = qx; py = qy; } } Rect strokeBounds(const StrokePoint* pts, std::size_t count, float halfWidth, const Rect& clip) { if (pts == nullptr || count == 0 || halfWidth <= 0.0f || clip.empty() || !std::isfinite(halfWidth)) { return Rect{}; } if (!(std::isfinite(pts[0].x) && std::isfinite(pts[0].y))) return Rect{}; float minX = pts[0].x, maxX = pts[0].x, minY = pts[0].y, maxY = pts[0].y; for (std::size_t i = 1; i < count; ++i) { // Checked per-point, not via isfinite(minX/maxX) after the reduction: std::min/max // against NaN silently returns the OTHER (finite) operand, so a NaN anywhere but pts[0] // would otherwise vanish from the reduction instead of rejecting the stroke. if (!(std::isfinite(pts[i].x) && std::isfinite(pts[i].y))) return Rect{}; minX = (std::min)(minX, pts[i].x); maxX = (std::max)(maxX, pts[i].x); minY = (std::min)(minY, pts[i].y); maxY = (std::max)(maxY, pts[i].y); } const float reach = halfWidth + 0.5f; // Same tightened box as addPiece (see its comment): a pixel only takes ink when its centre // is within `reach`, so this is [ceil(min-reach-0.5), floor(max+reach+0.5)) rather than the // old ceil/floor pair that padded a whole extra pixel on every side. const int x0 = (std::max)(clip.x, static_cast(std::ceil(minX - reach - 0.5f))); const int y0 = (std::max)(clip.y, static_cast(std::ceil(minY - reach - 0.5f))); const int x1 = (std::min)(clip.right(), static_cast(std::floor(maxX + reach + 0.5f))); const int y1 = (std::min)(clip.bottom(), static_cast(std::floor(maxY + reach + 0.5f))); if (x0 >= x1 || y0 >= y1) return Rect{}; return Rect::ltrb(x0, y0, x1, y1); } void strokePolyline(StrokeCanvas& canvas, const StrokePoint* pts, std::size_t count, float halfWidth, const Rect& clip) { canvas.reset(strokeBounds(pts, count, halfWidth, clip)); if (canvas.bounds().empty()) return; if (count == 1) { canvas.addSegment(pts[0].x, pts[0].y, pts[0].x, pts[0].y, halfWidth); return; } for (std::size_t i = 1; i < count; ++i) { canvas.addSegment(pts[i - 1].x, pts[i - 1].y, pts[i].x, pts[i].y, halfWidth); } } void appendArc(std::vector& out, float cx, float cy, float radius, float startRad, float endRad, float flatnessPx) { if (radius <= 0.0f) { out.push_back(StrokePoint{cx, cy}); return; } if (!(flatnessPx > 0.0f)) flatnessPx = kArcFlatnessPx; // Chord sagitta: r*(1 - cos(step/2)) <= flatness. A flatness at or past the diameter admits // the whole sweep in one chord, which is what keeps `maxStep` strictly positive. const float cosHalf = (std::max)(-1.0f, 1.0f - flatnessPx / radius); const float maxStep = 2.0f * std::acos(cosHalf); const float sweep = endRad - startRad; int segments = 1; if (maxStep > 0.0f) { const float wanted = std::ceil(std::fabs(sweep) / maxStep); segments = wanted >= static_cast(kMaxArcSegments) ? kMaxArcSegments : (std::max)(1, static_cast(wanted)); } else { // A radius large enough that flatness/radius underflows the cosine's resolution. The cap // is the termination guarantee, not a quality choice. segments = kMaxArcSegments; } out.reserve(out.size() + static_cast(segments) + 1); for (int i = 0; i <= segments; ++i) { const float a = startRad + sweep * (static_cast(i) / static_cast(segments)); out.push_back(StrokePoint{cx + radius * std::sin(a), cy - radius * std::cos(a)}); } } } // namespace reasampler::ui