fix: enlarge every sub-2px stroker width to 2px, per Daniel's ruling
Knob track arc, inner-dial needle, and mini velocity-trace all sat below the analytic stroker's opaque-core floor. Raised to 2px; updated visual-design-language.md and core/ui/CLAUDE.md to match; kept the 1px-behavior test, corrected its stale comment.
This commit is contained in:
@@ -808,15 +808,15 @@ concession. Everything with a slope or a curve must draw through a primitive tha
|
||||
|
||||
| Surface | Where | Disposition |
|
||||
|---|---|---|
|
||||
| Radial knob track arc | `editor_internal.h` `drawKnobFace` | **Fixed (2026-08-01)** — the stacked-radius `LICE_Arc` ring never reached an opaque core. Now ONE analytic stroke (`strokeArcAA`) at `kKnobTrackArcPx` = 1 px, **below the ≥2 px opaque-core threshold** (`core/ui/CLAUDE.md`) — peak alpha still modulates with the arc's exact pixel-grid alignment (measured ~128–255/255 across the sweep) rather than pinning to 255. An improvement over the old stacked-radius ripple, not full opacity; see `test_stroke_aa.cpp`'s 1 px case for the pinned behaviour. |
|
||||
| Radial knob track arc | `editor_internal.h` `drawKnobFace` | **Fixed (2026-08-01), widened (2026-08-01)** — the stacked-radius `LICE_Arc` ring never reached an opaque core. Now ONE analytic stroke (`strokeArcAA`); `kKnobTrackArcPx` was initially left at 1 px, below the ≥2 px opaque-core threshold (`core/ui/CLAUDE.md`), and surfaced to Daniel as a by-eye call — he ruled to enlarge all sub-2 px stroker widths, so it is now 2 px and reaches a guaranteed opaque core. |
|
||||
| Radial knob value arc | `editor_internal.h` `drawKnobFace` | **Fixed (2026-08-01)** — same stroke, `kKnobValueArcPx` = 3 px, clear of the opaque-core threshold. Measured: peak **255/255** at every cross-section, weight **2.95–3.11 px** (5% ripple). |
|
||||
| Knob needle | `drawKnobFace` | **Fixed (2026-08-01)** — `LICE_ThickFLine`'s minor-axis width thinned it to `cos θ` as the knob swept. Now `strokeLineAA`, 2 px. |
|
||||
| Inner curve dial arc | `drawInnerDial` | **Fixed (2026-08-01)** — the arc shared the knob track/value arc's stacked-radius opacity defect. Same one analytic fix, at `kInnerDialArcPx` = 2 px (at the opaque-core threshold). |
|
||||
| Inner curve dial needle | `drawInnerDial` | **Converted (2026-08-01), not a defect fix** — this needle was already `LICE_FLine` (float endpoints, always AA), not `LICE_ThickFLine`; a 1 px AA line has no width to lay along a minor axis, so it never had the knob needle's `cos θ` defect. Moved to `strokeLineAA` anyway for one-seam consistency with every other stroke on the editor, not because it was broken. |
|
||||
| Inner curve dial needle | `drawInnerDial` | **Converted (2026-08-01), widened (2026-08-01)** — this needle was already `LICE_FLine` (float endpoints, always AA), not `LICE_ThickFLine`; a 1 px AA line has no width to lay along a minor axis, so it never had the knob needle's `cos θ` defect. Moved to `strokeLineAA` at 1 px for one-seam consistency, not because it was broken — but 1 px is below the analytic stroker's opaque-core threshold, so it fell under Daniel's later blanket ruling and is now `kInnerDialNeedlePx` = 2 px. |
|
||||
| Staged envelope segment slopes | `editor_paint_waveform.cpp` | **Fixed (2026-08-01)** — one `strokePolylineAA` over the whole polyline, so the stage joints blend once. Vertices stay INTEGER by design: they are the positions the draggable handles are drawn at. |
|
||||
| Spline (drawn EG) contour | `editor_paint_waveform.cpp` `paintSplineOverlay` | **Fixed (2026-08-01)** — the trace was never gapped; it was fully aliased (every pixel full or empty) because the loop passed INTEGER `cy`, quantizing the slope into alternating 1/2 px steps. Now sub-pixel y (`subpixelFromPoint`) through `strokePolylineAA`. Measured: peak **255/255**, weight **1.95–2.01 px** (3% ripple). |
|
||||
| Velocity-curve popup trace | `editor_paint_curve.cpp` | **Fixed (2026-08-01)** — same cause, same treatment. |
|
||||
| Velocity-curve mini thumbnail | `editor_paint_curve.cpp` | **Fixed (2026-08-01)** — stays a 1 px hairline (a 2 px trace blots at thumbnail scale), but strokes analytically at sub-pixel y instead of integer-endpoint `LICE_Line`. |
|
||||
| Velocity-curve mini thumbnail | `editor_paint_curve.cpp` | **Fixed (2026-08-01), widened (2026-08-01)** — strokes analytically at sub-pixel y instead of integer-endpoint `LICE_Line`. Initially kept as a 1 px hairline (a 2 px trace was thought to blot at thumbnail scale), but 1 px is below the opaque-core threshold; Daniel's ruling raised `kMiniTracePx` to 2 px, same as the popup trace. |
|
||||
| Waveform min/max columns | `draw_kit.cpp` `drawWaveform` | **Fixed** — column fill unchanged (it cannot alias), plus an AA `LICE_FLine` stroke joining each column's extremes to its neighbour's, in the same ink. Shared with the docked bank panel and the browser cards. **Measured cost** (Release, MSVC 14.44, real LICE, 24 stereo cards × 136 columns = 6528 columns): fill alone 0.070 ms per full-grid repaint, fill+stroke 0.48 ms — the stroke is ~0.41 ms, about 2.5% of a 60 Hz frame, and the grid repaints on hover/scroll/drag, not continuously. One-off scratchpad measurement, 2026-08-01, harness not committed — not a standing regression guard; re-measure before relying on it again. |
|
||||
| Preview play triangle | `editor_paint_chrome.cpp` | **Fixed** — `LICE_FillTriangle` has no `aa`; its two sloped edges are re-stroked with AA `LICE_FLine`. |
|
||||
| Envelope/spline node handles (squares) | `editor_paint_waveform.cpp` | Already clean — axis-aligned `LICE_FillRect`. |
|
||||
|
||||
@@ -103,7 +103,7 @@ L7 sub-pass, 2026-07-27):
|
||||
- `card_meta` — pure card-metadata formatters: bars.beats.subdivisions and seconds.milliseconds; blank when the sample is unstamped.
|
||||
- `stroke_aa` — analytic antialiased thick-stroke COVERAGE (the shell blends it): `StrokeCanvas`, a reusable mask holding distance-to-polyline coverage MAX-accumulated across segments, plus `strokePolyline` / `strokeBounds` / `appendArc` / `rasterRowOffset` (the row-major offset
|
||||
math for a possibly bottom-up raster, pulled out of the shell's LICE blend so its flipped
|
||||
branch is pinned by a host-free test). An arc is just a flattened polyline, so ONE path serves the knob arcs, the inner dial, the envelope polyline and both spline traces. Coverage is `clamp(halfWidth + 0.5 - distance, 0, 1)`, which makes perpendicular weight exactly `2·halfWidth` at every angle. **The guaranteed-opaque-core threshold is width ≥ 2 px, not any width above 1 px**: opacity needs `distance <= halfWidth - 0.5`, and the worst-case distance from a pixel centre to the centreline is 0.5, so a 1 px stroke (`halfWidth = 0.5`) has zero slack — its peak alpha modulates with the stroke's exact alignment to the pixel grid instead of pinning to 255 (the knob track arc and the mini curve-thumbnail trace are both 1 px and both live with this). Long segments are subdivided before rasterizing — EXACT, not an approximation (min-distance to a partition of a segment is min-distance to the whole), purely to keep each piece's bounding box tight, since one long diagonal's box has area O(len²).
|
||||
branch is pinned by a host-free test). An arc is just a flattened polyline, so ONE path serves the knob arcs, the inner dial, the envelope polyline and both spline traces. Coverage is `clamp(halfWidth + 0.5 - distance, 0, 1)`, which makes perpendicular weight exactly `2·halfWidth` at every angle. **The guaranteed-opaque-core threshold is width ≥ 2 px, not any width above 1 px**: opacity needs `distance <= halfWidth - 0.5`, and the worst-case distance from a pixel centre to the centreline is 0.5, so a 1 px stroke (`halfWidth = 0.5`) has zero slack — its peak alpha modulates with the stroke's exact alignment to the pixel grid instead of pinning to 255 (Daniel's ruling, 2026-08-01: every stroker-drawn width on the editor is now >= 2 px for this reason — `testSubOpaqueCoreAtOnePixelWidth` in `tests/test_stroke_aa.cpp` still pins the 1 px case as a property of the stroker, independent of whether any surface ships at that width). Long segments are subdivided before rasterizing — EXACT, not an approximation (min-distance to a partition of a segment is min-distance to the whole), purely to keep each piece's bounding box tight, since one long diagonal's box has area O(len²).
|
||||
|
||||
## Gotchas
|
||||
|
||||
|
||||
@@ -113,11 +113,14 @@ inline void drawTitleBand(LICE_IBitmap* bmp, const instrument::ui::Rect& title,
|
||||
}
|
||||
|
||||
// Stroke widths for the radial faces. Every arc is ONE analytic stroke (editor_stroke.h) whose
|
||||
// outer edge sits on the knob's radius, so the centerline is inset by half the width.
|
||||
inline constexpr float kKnobTrackArcPx = 1.0f;
|
||||
// outer edge sits on the knob's radius, so the centerline is inset by half the width. Every
|
||||
// width here is >= 2 px, the stroker's guaranteed-opaque-core floor (core/ui/CLAUDE.md) —
|
||||
// anything narrower modulates peak alpha with pixel-grid alignment instead of pinning solid.
|
||||
inline constexpr float kKnobTrackArcPx = 2.0f;
|
||||
inline constexpr float kKnobValueArcPx = 3.0f;
|
||||
inline constexpr float kInnerDialArcPx = 2.0f;
|
||||
inline constexpr float kKnobNeedlePx = 2.0f;
|
||||
inline constexpr float kInnerDialNeedlePx = 2.0f;
|
||||
|
||||
// Draws one radial knob face: param_slider owns the value<->angle map; this turns it into
|
||||
// LICE calls. LICE takes radians, and drawing the 7->5 o'clock sweep through the top needs
|
||||
@@ -198,7 +201,7 @@ inline void drawInnerDial(LICE_IBitmap* bmp, const instrument::ui::Rect& innerRe
|
||||
toLice(ui::roleColor(arcRole)));
|
||||
const KnobPoint tip = instrument::ui::knobNeedlePoint(kg, arc, v);
|
||||
strokeLineAA(bmp, static_cast<float>(kg.centerX), static_cast<float>(kg.centerY),
|
||||
static_cast<float>(tip.x), static_cast<float>(tip.y), 1.0f,
|
||||
static_cast<float>(tip.x), static_cast<float>(tip.y), kInnerDialNeedlePx,
|
||||
toLice(ui::roleColor(disabled ? ui::Role::TextDim : ui::Role::AccentTertiary)));
|
||||
}
|
||||
|
||||
|
||||
@@ -19,8 +19,9 @@ using namespace reasampler::instrument::ui; // popup geometry
|
||||
|
||||
namespace {
|
||||
|
||||
// The deck thumbnail traces a hairline; the full editor matches the envelope traces' weight.
|
||||
constexpr float kMiniTracePx = 1.0f;
|
||||
// Both at the stroker's opaque-core floor (core/ui/CLAUDE.md) — below 2 px, peak alpha
|
||||
// modulates with pixel-grid alignment instead of pinning solid.
|
||||
constexpr float kMiniTracePx = 2.0f;
|
||||
constexpr float kCurveTracePx = 2.0f;
|
||||
|
||||
const char* curveTitle(CurveTarget target) {
|
||||
|
||||
@@ -91,8 +91,9 @@ static void testSubOpaqueCoreAtOnePixelWidth() {
|
||||
// Below the >= 2 px opaque-core threshold: a 1 px stroke (halfWidth = 0.5) has zero slack
|
||||
// against the 0.5 px worst-case pixel-centre distance (core/ui/CLAUDE.md), so peak alpha
|
||||
// tracks the stroke's alignment to the pixel grid instead of reaching 255 everywhere. Pin
|
||||
// both ends of that modulation — this is the knob track arc's and the mini curve-trace's
|
||||
// actual behaviour, not a hypothetical.
|
||||
// both ends of that modulation — no shipping surface draws at 1 px anymore (Daniel's
|
||||
// ruling, 2026-08-01), but this stays a real property of the stroker worth guarding
|
||||
// against reintroducing.
|
||||
const StrokePoint onRowCentre[2] = {{20.0f, 100.5f}, {220.0f, 100.5f}}; // centred on row 100
|
||||
StrokeCanvas aligned;
|
||||
strokePolyline(aligned, onRowCentre, 2, 0.5f, kBig);
|
||||
|
||||
Reference in New Issue
Block a user