07628a2059
Moves the NaN/Inf finiteness check to addSegment where the UB-causing cast actually happens, makes strokeBounds reject an interior non-finite point instead of swallowing it, corrects a LICE_GetPixel misattribution, and documents rasterRowOffset.
202 lines
8.6 KiB
C++
202 lines
8.6 KiB
C++
#include "core/ui/stroke_aa.h"
|
|
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
|
|
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<std::size_t>(bounds_.width) * static_cast<std::size_t>(bounds_.height);
|
|
if (coverage_.size() < area) coverage_.resize(area);
|
|
rowLo_.assign(static_cast<std::size_t>(bounds_.height), bounds_.width);
|
|
rowHi_.assign(static_cast<std::size_t>(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<std::size_t>(y - bounds_.y);
|
|
float* row = coverage_.data() + r * static_cast<std::size_t>(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<int>(std::ceil((std::min)(ax, bx) - reach - 0.5f));
|
|
int x1 = static_cast<int>(std::floor((std::max)(ax, bx) + reach + 0.5f));
|
|
int y0 = static_cast<int>(std::ceil((std::min)(ay, by) - reach - 0.5f));
|
|
int y1 = static_cast<int>(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<std::size_t>(y - bounds_.y) *
|
|
static_cast<std::size_t>(bounds_.width);
|
|
const float pyc = static_cast<float>(y) + 0.5f;
|
|
const float qy = pyc - ay;
|
|
for (int x = x0; x < x1; ++x) {
|
|
const float qx = static_cast<float>(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<int>(std::sqrt(len2) / kMaxPieceLen) + 1;
|
|
float px = ax;
|
|
float py = ay;
|
|
for (int i = 1; i <= pieces; ++i) {
|
|
const float t = static_cast<float>(i) / static_cast<float>(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<int>(std::ceil(minX - reach - 0.5f)));
|
|
const int y0 = (std::max)(clip.y, static_cast<int>(std::ceil(minY - reach - 0.5f)));
|
|
const int x1 = (std::min)(clip.right(), static_cast<int>(std::floor(maxX + reach + 0.5f)));
|
|
const int y1 = (std::min)(clip.bottom(), static_cast<int>(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<StrokePoint>& 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<float>(kMaxArcSegments)
|
|
? kMaxArcSegments
|
|
: (std::max)(1, static_cast<int>(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<std::size_t>(segments) + 1);
|
|
for (int i = 0; i <= segments; ++i) {
|
|
const float a = startRad + sweep * (static_cast<float>(i) / static_cast<float>(segments));
|
|
out.push_back(StrokePoint{cx + radius * std::sin(a), cy - radius * std::cos(a)});
|
|
}
|
|
}
|
|
|
|
} // namespace reasampler::ui
|