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.
98 lines
4.6 KiB
C++
98 lines
4.6 KiB
C++
// stroke_aa.h — analytic antialiased thick-stroke coverage: distance-to-polyline, MAX-accumulated
|
|
// into a scratch mask that a shell blends ONCE. Pure geometry; no LICE, no host types.
|
|
//
|
|
// The single blend is the load-bearing part. Compositing a stroke segment-by-segment (or as a
|
|
// stack of 1px arcs) re-lays ink over the previous segment's antialiased fringe, which is what
|
|
// makes a stroke read as a soft glow that never reaches an opaque core.
|
|
|
|
#pragma once
|
|
|
|
#include <cstddef>
|
|
#include <vector>
|
|
|
|
#include "core/ui/rect.h"
|
|
|
|
namespace reasampler::ui {
|
|
|
|
struct StrokePoint {
|
|
float x = 0.0f;
|
|
float y = 0.0f;
|
|
};
|
|
|
|
// Chord-flatness bound for arc flattening: an order of magnitude under the AA fringe the arc is
|
|
// drawn with, so the polyline is indistinguishable from the true arc at any radius the deck uses.
|
|
inline constexpr float kArcFlatnessPx = 0.05f;
|
|
|
|
// Hard cap on flattening output, so a pathological radius/flatness pair terminates with a coarse
|
|
// arc rather than allocating without bound.
|
|
inline constexpr int kMaxArcSegments = 512;
|
|
|
|
// Long segments are split before rasterizing. This is EXACT, not an approximation: the minimum
|
|
// distance to a partition of a segment is the minimum distance to the whole segment. It exists
|
|
// because a piece is rasterized over its bounding box, and one long diagonal's box has area
|
|
// O(len^2) — subdivision is what keeps a stroke's cost linear in its length.
|
|
inline constexpr float kMaxPieceLen = 4.0f;
|
|
|
|
// A reusable coverage mask. Allocation is amortized across calls: `reset` grows the buffer but
|
|
// never clears it, because per-row valid extents make a clear unnecessary.
|
|
class StrokeCanvas {
|
|
public:
|
|
// Grows the buffer to fit `bounds` and marks every row empty. O(height), not O(area).
|
|
void reset(const Rect& bounds);
|
|
|
|
// MAX-accumulates one segment. Endpoints are round-capped, so a zero-length segment is a dot
|
|
// of radius `halfWidth` and a polyline's joints are round by construction.
|
|
void addSegment(float ax, float ay, float bx, float by, float halfWidth);
|
|
|
|
const Rect& bounds() const { return bounds_; }
|
|
|
|
// Only [rowLo, rowHi) of a row holds meaningful coverage; outside that span the buffer is
|
|
// deliberately uninitialized, which is what keeps cost proportional to ink, not to the box.
|
|
int rowLo(int y) const { return rowLo_[static_cast<std::size_t>(y - bounds_.y)]; }
|
|
int rowHi(int y) const { return rowHi_[static_cast<std::size_t>(y - bounds_.y)]; }
|
|
|
|
// Row base pointer; index it by (x - bounds().x) within [rowLo, rowHi).
|
|
const float* rowData(int y) const {
|
|
return coverage_.data() +
|
|
static_cast<std::size_t>(y - bounds_.y) * static_cast<std::size_t>(bounds_.width);
|
|
}
|
|
|
|
// Bounds-checked single read — 0 outside the valid span. For tests and cold callers; the
|
|
// blend loop walks rows directly.
|
|
float coverageAt(int x, int y) const;
|
|
|
|
private:
|
|
void addPiece(float ax, float ay, float bx, float by, float halfWidth);
|
|
void extendRow(int y, int x0, int x1);
|
|
|
|
Rect bounds_{};
|
|
std::vector<float> coverage_;
|
|
std::vector<int> rowLo_;
|
|
std::vector<int> rowHi_;
|
|
};
|
|
|
|
// The pixel box a polyline of this half-width can touch, intersected with `clip`.
|
|
Rect strokeBounds(const StrokePoint* pts, std::size_t count, float halfWidth, const Rect& clip);
|
|
|
|
// A whole stroke in one pass: bounds, reset, every segment MAX-accumulated. Blending the finished
|
|
// canvas exactly once is the caller's half of the contract.
|
|
void strokePolyline(StrokeCanvas& canvas, const StrokePoint* pts, std::size_t count,
|
|
float halfWidth, const Rect& clip);
|
|
|
|
// Appends a flattened arc (n+1 points for n chords) to `out`. Angles are radians in LICE's
|
|
// convention: 0 is 12 o'clock, increasing clockwise — x = cx + r*sin(a), y = cy - r*cos(a).
|
|
void appendArc(std::vector<StrokePoint>& out, float cx, float cy, float radius, float startRad,
|
|
float endRad, float flatnessPx = kArcFlatnessPx);
|
|
|
|
// The row-major element offset of row `y` within a `rowSpan`-elements-per-row pixel buffer,
|
|
// accounting for a possibly bottom-up (`flipped`) layout. Pulled out of the shell's LICE blend
|
|
// so its flipped branch — dead for every bitmap type the shell actually constructs, and
|
|
// otherwise unverifiable without a live LICE surface — is pinned by a host-free test. Matches
|
|
// LICE's own row math (`lice.cpp`'s free-function `LICE_GetPixel`: `(h-1-y)*rowspan + x`).
|
|
inline std::size_t rasterRowOffset(int y, int height, int rowSpan, bool flipped) {
|
|
const int row = flipped ? height - 1 - y : y;
|
|
return static_cast<std::size_t>(row) * static_cast<std::size_t>(rowSpan);
|
|
}
|
|
|
|
} // namespace reasampler::ui
|