fix: stroke arcs and splines analytically — opaque core, angle-independent weight
LICE_Arc never reaches opacity and ThickFLine's width is minor-axis. One distance-to-polyline coverage mask, blended once, replaces both.
This commit is contained in:
@@ -103,6 +103,7 @@ declared ahead of the instrument slots at that member in `reasampler_processor.h
|
||||
- `reasampler_processor` (`shell/instrument/`: `reasampler_processor.cpp` lifecycle + `process()`, `processor_state.cpp` component-state I/O + UI-thread parameter accessors, `processor_reload.cpp` the off-audio-thread `reloadInstrument`/publish family — Q-W2v, T4-12 split; `process()` and its per-block work stay ONE TU on purpose, no cross-TU call on the per-sample path) — VST3 `SingleComponentEffect` shell: declares event-input bus + **permanently stereo** output (GA fix: dynamic mono↔stereo bus renegotiation deleted; `ChannelMode` is now decode-only), marshals MIDI note-on/off into the VoiceEngine, renders audio; owns off-audio-thread `reloadInstrument` + atomic pointer swap so `process()` does no allocation, no file I/O, no bridge calls. The instance state is `{loaded capture id, one InstrumentParams}`, and `reloadInstrument` resolves + decodes exactly that one capture into the `SampleData` the engine plays. **Self-contained playback (pS):** `ComponentState` v10 adds a `SampleRefs` table — per referenced sample, a project-relative path + decode intrinsics (root, loop, channels, displayName); `reloadInstrument` decodes directly from `SampleRefs`, bank-free (plays with the extension absent). The bank/bridge is a browser source: loading a capture copies its reference in; the reopen-heal timer + poll-to-play apparatus are removed. `retireIdleDrain()` retires fully-idle drain snapshots on the UI-timer cadence. Voice-param edits (`setVoiceCount`/`setVoiceMode`/`setMonoTrigger`) rebuild the engine from the already-decoded `SampleData` via the drain-slot swap — no bank re-read, no WAV re-decode, no audible cut to ringing tails. **FB1:** applies the post-mixer `masterGainLinear` (from `ComponentState` v8) as a per-sample ramp over the summed output — no zipper noise. **GA v9:** `channelModeExplicit_` flag persisted; `channelModeFor()` auto-defaults the mode from the loaded capture's channel count when the flag is not set. **pS:** `ComponentState` bumped v9→v10 (`SampleRefs` table); pre-v10 blobs lift to empty refs and re-save self-contained. **pS-usage:** publishes instance usage (held `SampleRefs` paths) to `rsusage_<instanceGuid>` at the tail of `reloadInstrument` (off audio thread) via `reaper_bridge::writeUsageExtState`; `ComponentState` bumped v10→**v11** (`instanceGuid` field); pre-v11 blobs mint guid on first publish.
|
||||
- `reasampler_editor` — VST3 `IPlugView` LICE editor shell: hosts a LICE-drawn child window; the Sample face is home and Browse is a modal picker over it. Split on the Sample face's BAND axis, mirroring the pure `sample_bands` allocator: `editor_session` (session/bridge state, caches, commit-and-reload), `editor_controls` (the ONE `faceLayout` band resolve every paint and hit-test path shares, the node-drag bounds, the value labels, and the per-instance controls the parameter set does not carry — the parameter-set binding itself is the pure `core/instrument/ui/deck_values` module this only adapts int ids onto), `editor_models` (the orthogonal half: which stored struct each transient editor selection names — the staged-envelope pack/unpack, the drawn contour, and the three velocity curves), then matching paint and input sets — `editor_paint`/`editor_input` (dispatch + drag router + hover dispatch), `_chrome`, `_waveform`, `_deck` — plus the two band-independent surfaces (`_browse` for the modal picker, `_curve` for the velocity-curve popup) and `editor_platform` (IPlugView/Win32 window plumbing). Shared internals in `editor_internal.h`, no TU of its own. Drop-onto-editor ingest is NOT shipped (deferred).
|
||||
- `reasampler_embed` — implements `IReaperUIEmbedInterface` so the instrument draws inline in the TCP/MCP without a plugin-owned HWND; delegates layout to `embed_strip`. A read-only readout: the loaded capture across the keyboard span with its root marked, plus the activity level. It takes no mouse input (there is nothing on the strip to select).
|
||||
- `editor_stroke` — the editor's LICE side of the analytic stroker: builds a coverage mask with the pure `core/ui/stroke_aa` and blends it into the bitmap ONCE, writing straight to the bitmap's bits (the arithmetic matches LICE's own mode-0 combine, so a stroke composites identically to every other kit draw). Every radial and spline stroke on the editor routes through `strokeArcAA` / `strokePolylineAA` / `strokeLineAA`. Holds the draw-thread-only scratch mask and arc point list — reuse, not a hidden dependency: threading a canvas through the eight paint sites would grow those signatures to carry an allocation detail. Deliberately does NOT touch `shell/panel/draw_kit`: the waveform stroke, the docked bank panel and the browse cards are out of this seam's blast radius.
|
||||
- `vst_entry` — VST3 entry point: `GetPluginFactory` export, class registration, channel-forked class UIDs.
|
||||
- `editor_internal.h` — INTERNAL shared helpers for the `reasampler_editor` TU family, included only by the editor's own shell TUs (`editor_session` / `editor_controls` / `editor_paint_*` / `editor_input_*` / `editor_platform`), never a public seam: the `Rect`↔kit adapters, small draw primitives (knob face / title band), label helpers, and the velocity-curve box derivation — the helpers more than one band TU needs. The deck's control ids, group ids and group composition are the pure `deck_groups` module's, not this file's. The piano-strip and root-key draws live in `editor_paint_chrome`, their only consumer, not here.
|
||||
- `reasampler_vst.h` — shared identity constants for the ReaSampler VST3 instrument (Phase S): the plugin's class UID (the channel-selected `Steinberg::FUID`, built from the FOREVER-FROZEN macros in `core/wire/reasampler_uid.h`), vendor name/URL/email, so the processor, factory, and editor agree. A class UID is FOREVER-STABLE once shipped — minted once, never regenerated. *(Newly authored per this dispatch's brief — no existing root-CLAUDE.md bullet; verified by reading `src/shell/instrument/reasampler_vst.h` directly.)*
|
||||
|
||||
@@ -64,6 +64,7 @@ if(WIN32 AND EXISTS "${VST3_SDK}/public.sdk/source/main/pluginfactory.cpp")
|
||||
editor_input_deck.cpp
|
||||
editor_input_browse.cpp
|
||||
editor_input_curve.cpp
|
||||
editor_stroke.cpp
|
||||
editor_platform.cpp
|
||||
reasampler_embed.cpp
|
||||
reaper_bridge.cpp
|
||||
@@ -87,7 +88,7 @@ if(WIN32 AND EXISTS "${VST3_SDK}/public.sdk/source/main/pluginfactory.cpp")
|
||||
waveform_view bank_sync browser_scroll param_slider tooltip
|
||||
theme component_geometry bank_grid trigger_seam envelope_overlay envelope_edit
|
||||
knob_deck deck_groups deck_values curve_popup spline_edit master_gain sample_usage
|
||||
file_bytes curve_law)
|
||||
file_bytes curve_law stroke_aa)
|
||||
# SDK_INC gives the REAPER VST3 interfaces + API header for the bridge; WDL_INC gives
|
||||
# LICE for the editor. The VST3 SDK headers arrive via vst3_sdk PUBLIC.
|
||||
target_include_directories(reasampler_vst PRIVATE ${REASAMPLER_SRC_DIR} ${SDK_INC} ${WDL_INC})
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
#include "core/instrument/ui/param_slider.h" // KnobGeometry / KnobArc (drawKnobFace)
|
||||
#include "core/ui/component_geometry.h" // KitBox / waveformColumnCount
|
||||
#include "core/ui/theme.h" // Role / InteractionState / KitColor / spectralColor
|
||||
#include "shell/instrument/editor_stroke.h" // strokeArcAA / strokePolylineAA
|
||||
#include "shell/panel/draw_kit.h" // the L1 draw kit: fillSurface/text/drawWaveform/toLice
|
||||
#endif
|
||||
|
||||
@@ -111,11 +112,11 @@ inline void drawTitleBand(LICE_IBitmap* bmp, const instrument::ui::Rect& title,
|
||||
kitText(bmp, titleText, readout.c_str(), Font::Title, ui::Role::TextPrimary);
|
||||
}
|
||||
|
||||
// Stroke widths for the radial faces. The value arc is drawn as adjacent 1px AA arcs rather
|
||||
// than one thick primitive — LICE has no thick-arc call, and stacking radii is what keeps every
|
||||
// ring antialiased.
|
||||
inline constexpr int kKnobValueArcPx = 3;
|
||||
inline constexpr int kInnerDialArcPx = 2;
|
||||
// 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;
|
||||
inline constexpr float kKnobValueArcPx = 3.0f;
|
||||
inline constexpr float kInnerDialArcPx = 2.0f;
|
||||
inline constexpr int kKnobNeedlePx = 2;
|
||||
|
||||
// Draws one radial knob face: param_slider owns the value<->angle map; this turns it into
|
||||
@@ -143,27 +144,26 @@ inline void drawKnobFace(LICE_IBitmap* bmp, const instrument::ui::Rect& knobRect
|
||||
const float a0 = static_cast<float>((arc.startDeg - 360.0) * kDegToRad);
|
||||
const float a1 = static_cast<float>(
|
||||
(arc.startDeg + instrument::ui::knobSweepDeg(arc) - 360.0) * kDegToRad);
|
||||
LICE_Arc(bmp, cx, cy, rOuter, a0, a1, toLice(ui::roleColor(ui::Role::LineHairline)), 1.0f, 0,
|
||||
true);
|
||||
strokeArcAA(bmp, cx, cy, rOuter - kKnobTrackArcPx * 0.5f, a0, a1, kKnobTrackArcPx,
|
||||
toLice(ui::roleColor(ui::Role::LineHairline)));
|
||||
const double v = value01 < 0.0 ? 0.0 : (value01 > 1.0 ? 1.0 : value01);
|
||||
if (v > 0.0) {
|
||||
const float av = static_cast<float>(
|
||||
(arc.startDeg + v * instrument::ui::knobSweepDeg(arc) - 360.0) * kDegToRad);
|
||||
const ui::Role valueRole = disabled ? ui::Role::TextDim
|
||||
: (hot ? ui::Role::AccentHot : ui::Role::AccentPrimary);
|
||||
const LICE_pixel valueCol = toLice(ui::roleColor(valueRole));
|
||||
for (int i = 0; i < kKnobValueArcPx; ++i) {
|
||||
LICE_Arc(bmp, cx, cy, rOuter - static_cast<float>(i), a0, av, valueCol, 1.0f, 0, true);
|
||||
}
|
||||
strokeArcAA(bmp, cx, cy, rOuter - kKnobValueArcPx * 0.5f, a0, av, kKnobValueArcPx,
|
||||
toLice(ui::roleColor(valueRole)));
|
||||
}
|
||||
// Needle: from ~35% radius out to the rim at the value's angle. ThickFLine keeps the float
|
||||
// endpoints AND is always antialiased, so the needle is smooth at every angle.
|
||||
// Needle: from ~35% radius out to the rim at the value's angle, stroked through the same
|
||||
// analytic path as the arcs so it holds its weight at every knob position.
|
||||
const KnobPoint tip = instrument::ui::knobNeedlePoint(kg, arc, v);
|
||||
const double ix = kg.centerX + (tip.x - kg.centerX) * 0.35;
|
||||
const double iy = kg.centerY + (tip.y - kg.centerY) * 0.35;
|
||||
const ui::Role needleRole = disabled ? ui::Role::TextDim : ui::Role::TextPrimary;
|
||||
LICE_ThickFLine(bmp, ix, iy, tip.x, tip.y, toLice(ui::roleColor(needleRole)), 1.0f, 0,
|
||||
kKnobNeedlePx);
|
||||
strokeLineAA(bmp, static_cast<float>(ix), static_cast<float>(iy),
|
||||
static_cast<float>(tip.x), static_cast<float>(tip.y),
|
||||
static_cast<float>(kKnobNeedlePx), toLice(ui::roleColor(needleRole)));
|
||||
}
|
||||
|
||||
// The concentric INNER dial: a second value on the same cell, drawn in the categorical
|
||||
@@ -194,15 +194,12 @@ inline void drawInnerDial(LICE_IBitmap* bmp, const instrument::ui::Rect& innerRe
|
||||
(arc.startDeg + v * instrument::ui::knobSweepDeg(arc) - 360.0) * kDegToRad);
|
||||
const ui::Role arcRole = disabled ? ui::Role::TextDim
|
||||
: (hot ? ui::Role::AccentHot : ui::Role::AccentTertiary);
|
||||
const LICE_pixel arcCol = toLice(ui::roleColor(arcRole));
|
||||
for (int i = 0; i < kInnerDialArcPx; ++i) {
|
||||
LICE_Arc(bmp, cx, cy, r - static_cast<float>(i), a0, av, arcCol, 1.0f, 0, true);
|
||||
}
|
||||
strokeArcAA(bmp, cx, cy, r - kInnerDialArcPx * 0.5f, a0, av, kInnerDialArcPx,
|
||||
toLice(ui::roleColor(arcRole)));
|
||||
const KnobPoint tip = instrument::ui::knobNeedlePoint(kg, arc, v);
|
||||
LICE_FLine(bmp, static_cast<float>(kg.centerX), static_cast<float>(kg.centerY),
|
||||
static_cast<float>(tip.x), static_cast<float>(tip.y),
|
||||
toLice(ui::roleColor(disabled ? ui::Role::TextDim : ui::Role::AccentTertiary)),
|
||||
1.0f, 0, true);
|
||||
strokeLineAA(bmp, static_cast<float>(kg.centerX), static_cast<float>(kg.centerY),
|
||||
static_cast<float>(tip.x), static_cast<float>(tip.y), 1.0f,
|
||||
toLice(ui::roleColor(disabled ? ui::Role::TextDim : ui::Role::AccentTertiary)));
|
||||
}
|
||||
|
||||
#endif // _WIN32
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
|
||||
#ifdef _WIN32
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "core/instrument/ui/curve_popup.h" // centered curve-popup sheet geometry
|
||||
#include "shell/instrument/editor_internal.h" // kit adapters + curveBoxFromRect
|
||||
#include "shell/instrument/reasampler_processor.h"
|
||||
@@ -17,6 +19,10 @@ 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;
|
||||
constexpr float kCurveTracePx = 2.0f;
|
||||
|
||||
const char* curveTitle(CurveTarget target) {
|
||||
switch (target) {
|
||||
case CurveTarget::kPitch: return "VELOCITY -> PITCH";
|
||||
@@ -57,15 +63,15 @@ void ReaSamplerEditor::paintCurveButton(LICE_IBitmap* bmp, const Rect& r, CurveT
|
||||
}
|
||||
const LICE_pixel trace =
|
||||
toLice(roleColor(disabled ? Role::LineHairline : Role::AccentSecondary));
|
||||
int prevX = 0, prevY = 0;
|
||||
static thread_local std::vector<ui::StrokePoint> pts;
|
||||
pts.clear();
|
||||
for (int px = 0; px <= mini.width; ++px) {
|
||||
const int mx = mini.left + px;
|
||||
const double vel = curve.pointFromPixel(mini, mx, mini.top).velocity;
|
||||
const int my = curve.pixelFromPoint(mini, {vel, curve.eval(vel)}).y;
|
||||
if (px > 0) LICE_Line(bmp, prevX, prevY, mx, my, trace, 1.0f, 0, true);
|
||||
prevX = mx;
|
||||
prevY = my;
|
||||
const auto p = curve.subpixelFromPoint(mini, {vel, curve.eval(vel)});
|
||||
pts.push_back(ui::StrokePoint{static_cast<float>(mx), static_cast<float>(p.y)});
|
||||
}
|
||||
strokePolylineAA(bmp, pts, kMiniTracePx, trace);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,19 +120,18 @@ void ReaSamplerEditor::paintVelocityCurve(LICE_IBitmap* bmp, const Rect& r) {
|
||||
// Trace the monotone spline — ONE eval per x column over the mapping box, in the categorical
|
||||
// secondary accent (the same grammar as the envelope trace over the waveform). The x ->
|
||||
// velocity and value -> y mappings both go through the pure module so the trace, the node
|
||||
// handles, and the hit-test all share one coordinate system.
|
||||
// handles, and the hit-test all share one coordinate system (sub-pixel for the trace — see
|
||||
// subpixelFromPoint).
|
||||
const LICE_pixel line = toLice(roleColor(Role::AccentSecondary));
|
||||
int prevX = 0, prevY = 0;
|
||||
static thread_local std::vector<ui::StrokePoint> pts;
|
||||
pts.clear();
|
||||
for (int px = 0; px <= box.width; ++px) {
|
||||
const int cx = box.left + px;
|
||||
const double vel = curve.pointFromPixel(box, cx, box.top).velocity;
|
||||
const int cy = curve.pixelFromPoint(box, {vel, curve.eval(vel)}).y;
|
||||
// Same weight and antialiasing the envelope traces use — one trace grammar across every
|
||||
// curve surface (editor_paint_waveform.cpp owns why ThickFLine and not LICE_Line).
|
||||
if (px > 0) LICE_ThickFLine(bmp, prevX, prevY, cx, cy, line, 1.0f, 0, 2);
|
||||
prevX = cx;
|
||||
prevY = cy;
|
||||
const auto p = curve.subpixelFromPoint(box, {vel, curve.eval(vel)});
|
||||
pts.push_back(ui::StrokePoint{static_cast<float>(cx), static_cast<float>(p.y)});
|
||||
}
|
||||
strokePolylineAA(bmp, pts, kCurveTracePx, line);
|
||||
|
||||
// Draggable node handles (mirror of the envelope overlay's): accent-primary squares lifted
|
||||
// to accent-hot when grabbed or hovered, or warn when a drag-off delete is armed (cursor
|
||||
|
||||
@@ -42,11 +42,9 @@ constexpr int kEnvHandleRadius = 3;
|
||||
constexpr int kEnvHandleGrabbedRadius = 5;
|
||||
constexpr int kEnvHandleRingPx = 2;
|
||||
|
||||
// Both envelope traces — staged and drawn — are one grammar and one weight. LICE_ThickFLine is
|
||||
// ALWAYS antialiased (unlike LICE_Line, whose aa flag does nothing on an axis-aligned run), and
|
||||
// the second pixel of width is what stops a shallow slope reading as a staircase over the
|
||||
// waveform behind it.
|
||||
constexpr int kEnvTracePx = 2;
|
||||
// Both envelope traces — staged and drawn — are one grammar and one weight. Two pixels is what
|
||||
// reads as a trace rather than a hairline over the waveform behind it.
|
||||
constexpr float kEnvTracePx = 2.0f;
|
||||
} // namespace
|
||||
|
||||
void ReaSamplerEditor::paintWaveform(LICE_IBitmap* bmp, const Rect& band) {
|
||||
@@ -143,21 +141,22 @@ void ReaSamplerEditor::paintSplineOverlay(LICE_IBitmap* bmp, const OverlayArea&
|
||||
if (box.width <= 0 || box.height <= 1) return;
|
||||
const VelocityCurve& curve = splineFor(overlayEnv_);
|
||||
|
||||
// One eval per drawn column, through the curve's own pixel maps, so the trace and the
|
||||
// handles share the coordinate system the hit-test resolves against.
|
||||
// One eval per drawn column, through the curve's own pixel maps, so the trace and the handles
|
||||
// share the coordinate system the hit-test resolves against (sub-pixel here — see
|
||||
// subpixelFromPoint).
|
||||
const LICE_pixel line = toLice(roleColor(Role::OverlayTrace));
|
||||
int prevX = 0, prevY = 0;
|
||||
static thread_local std::vector<ui::StrokePoint> trace;
|
||||
trace.clear();
|
||||
// < not <=: box.left + box.width is the overlay's own EXCLUSIVE right edge (the box has no
|
||||
// inset, unlike the popup's), so a <= column paints one pixel into the next band's pad —
|
||||
// and it is redundant with the clamped endpoint handle below anyway.
|
||||
for (int px = 0; px < box.width; ++px) {
|
||||
const int cx = box.left + px;
|
||||
const double t = curve.pointFromPixel(box, cx, box.top).velocity;
|
||||
const int cy = curve.pixelFromPoint(box, {t, curve.eval(t)}).y;
|
||||
if (px > 0) LICE_ThickFLine(bmp, prevX, prevY, cx, cy, line, 1.0f, 0, kEnvTracePx);
|
||||
prevX = cx;
|
||||
prevY = cy;
|
||||
const auto p = curve.subpixelFromPoint(box, {t, curve.eval(t)});
|
||||
trace.push_back(ui::StrokePoint{static_cast<float>(cx), static_cast<float>(p.y)});
|
||||
}
|
||||
strokePolylineAA(bmp, trace, kEnvTracePx, line);
|
||||
|
||||
// Handles carry two independent states on the same mark, so they use two independent
|
||||
// channels: SIZE is the grab (the staged painter's grammar — a hotter hue reads as lower
|
||||
@@ -211,18 +210,18 @@ void ReaSamplerEditor::paintEnvelopeOverlay(LICE_IBitmap* bmp, const OverlayArea
|
||||
const StageEnvelope env = packEnvelope(overlayEnv_, params_.play, frames, startFrame);
|
||||
const std::vector<EnvVertex> poly = buildEnvelopePolyline(env, waveArea, totalSeconds);
|
||||
|
||||
// Clip x to the wave rect. Knots are handles, not line vertices.
|
||||
// Clip x to the wave rect. Knots are handles, not line vertices. Vertices stay INTEGER here
|
||||
// — unlike the spline traces above — because they are the same positions the draggable
|
||||
// handles are drawn at, and a sub-pixel trace would sit off its own handles.
|
||||
const LICE_pixel line = toLice(roleColor(Role::OverlayTrace));
|
||||
const EnvVertex* prev = nullptr;
|
||||
static thread_local std::vector<ui::StrokePoint> trace;
|
||||
trace.clear();
|
||||
for (const EnvVertex& v : poly) {
|
||||
if (v.knot) continue;
|
||||
if (prev != nullptr) {
|
||||
const int x0 = (std::max)(area.x, (std::min)(area.right() - 1, prev->x));
|
||||
const int x1 = (std::max)(area.x, (std::min)(area.right() - 1, v.x));
|
||||
LICE_ThickFLine(bmp, x0, prev->y, x1, v.y, line, 1.0f, 0, kEnvTracePx);
|
||||
}
|
||||
prev = &v;
|
||||
const int vx = (std::max)(area.x, (std::min)(area.right() - 1, v.x));
|
||||
trace.push_back(ui::StrokePoint{static_cast<float>(vx), static_cast<float>(v.y)});
|
||||
}
|
||||
strokePolylineAA(bmp, trace, kEnvTracePx, line);
|
||||
// Handles: a square per draggable stage node, a ROUND knot per curvable segment. Every
|
||||
// vertex is guaranteed in-bounds; the handle is additionally clamped inside the band so one
|
||||
// on an edge node never overhangs into the neighbouring bands.
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
#include "shell/instrument/editor_stroke.h"
|
||||
|
||||
#ifdef _WIN32
|
||||
|
||||
#include <cstddef>
|
||||
|
||||
namespace reasampler::vst {
|
||||
namespace {
|
||||
|
||||
// Draw-thread-only scratch. It lives here rather than on the editor because a stroke is a leaf
|
||||
// draw call reached from eight paint sites — threading a canvas through every one of them would
|
||||
// grow those signatures to carry an allocation detail. Reuse is the point: after the first paint
|
||||
// the mask and the point list are resized, never reallocated.
|
||||
thread_local ui::StrokeCanvas g_canvas;
|
||||
thread_local std::vector<ui::StrokePoint> g_arcPoints;
|
||||
|
||||
// One blend of the finished mask. The arithmetic matches LICE's own mode-0 combine
|
||||
// (src + (dst-src)*(256-a)/256 on all four channels, alpha in .8 fixed point) so a stroke
|
||||
// composites identically to every other kit draw on the same surface — written straight to the
|
||||
// bitmap's bits rather than through LICE_PutPixel, which re-derives the row pointer per pixel.
|
||||
void blendCanvas(LICE_IBitmap* bmp, const ui::StrokeCanvas& canvas, LICE_pixel color,
|
||||
float alpha) {
|
||||
const ui::Rect& b = canvas.bounds();
|
||||
if (b.empty() || alpha <= 0.0f) return;
|
||||
LICE_pixel* const bits = bmp->getBits();
|
||||
const int span = bmp->getRowSpan();
|
||||
if (bits == nullptr || span <= 0) return;
|
||||
const bool flipped = bmp->isFlipped();
|
||||
const int lastRow = bmp->getHeight() - 1;
|
||||
|
||||
const int sr = LICE_GETR(color);
|
||||
const int sg = LICE_GETG(color);
|
||||
const int sb = LICE_GETB(color);
|
||||
const int sa = LICE_GETA(color);
|
||||
for (int y = b.y; y < b.bottom(); ++y) {
|
||||
const int lo = canvas.rowLo(y);
|
||||
const int hi = canvas.rowHi(y);
|
||||
if (hi <= lo) continue;
|
||||
const float* const cov = canvas.rowData(y);
|
||||
LICE_pixel* const row =
|
||||
bits + static_cast<std::size_t>(flipped ? lastRow - y : y) *
|
||||
static_cast<std::size_t>(span) + static_cast<std::size_t>(b.x);
|
||||
for (int i = lo; i < hi; ++i) {
|
||||
const int ia = static_cast<int>(cov[i] * alpha * 256.0f);
|
||||
if (ia <= 0) continue;
|
||||
if (ia >= 256) {
|
||||
row[i] = color;
|
||||
continue;
|
||||
}
|
||||
const int sc = 256 - ia;
|
||||
LICE_pixel_chan* const d = reinterpret_cast<LICE_pixel_chan*>(row + i);
|
||||
d[LICE_PIXEL_R] = static_cast<LICE_pixel_chan>(sr + ((d[LICE_PIXEL_R] - sr) * sc) / 256);
|
||||
d[LICE_PIXEL_G] = static_cast<LICE_pixel_chan>(sg + ((d[LICE_PIXEL_G] - sg) * sc) / 256);
|
||||
d[LICE_PIXEL_B] = static_cast<LICE_pixel_chan>(sb + ((d[LICE_PIXEL_B] - sb) * sc) / 256);
|
||||
d[LICE_PIXEL_A] = static_cast<LICE_pixel_chan>(sa + ((d[LICE_PIXEL_A] - sa) * sc) / 256);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ui::Rect bitmapRect(LICE_IBitmap* bmp) {
|
||||
return ui::Rect{0, 0, bmp->getWidth(), bmp->getHeight()};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void strokePolylineAA(LICE_IBitmap* bmp, const ui::StrokePoint* pts, std::size_t count,
|
||||
float widthPx, LICE_pixel color, float alpha) {
|
||||
if (bmp == nullptr || pts == nullptr || count == 0 || widthPx <= 0.0f) return;
|
||||
ui::strokePolyline(g_canvas, pts, count, widthPx * 0.5f, bitmapRect(bmp));
|
||||
blendCanvas(bmp, g_canvas, color, alpha);
|
||||
}
|
||||
|
||||
void strokeArcAA(LICE_IBitmap* bmp, float cx, float cy, float radius, float startRad, float endRad,
|
||||
float widthPx, LICE_pixel color, float alpha) {
|
||||
if (bmp == nullptr || widthPx <= 0.0f) return;
|
||||
g_arcPoints.clear();
|
||||
ui::appendArc(g_arcPoints, cx, cy, radius, startRad, endRad);
|
||||
strokePolylineAA(bmp, g_arcPoints.data(), g_arcPoints.size(), widthPx, color, alpha);
|
||||
}
|
||||
|
||||
} // namespace reasampler::vst
|
||||
|
||||
#endif // _WIN32
|
||||
@@ -0,0 +1,50 @@
|
||||
// editor_stroke.h — the editor's LICE side of the analytic stroker: build a coverage mask with
|
||||
// the pure `core/ui/stroke_aa` module, blend it into the bitmap ONCE.
|
||||
//
|
||||
// Every radial and spline stroke on the editor goes through here. LICE's own primitives cannot
|
||||
// serve these two shapes: LICE_Arc rasterizes a whole circle clipped per 90-degree box and splits
|
||||
// one unit of ink across two pixels by the radius's fractional part (so a stacked-radii arc never
|
||||
// reaches an opaque core), and LICE_ThickFLine lays its width along the MINOR axis (so a curve's
|
||||
// perpendicular weight falls off as cos(theta) and thins at every diagonal).
|
||||
|
||||
#pragma once
|
||||
|
||||
#ifdef _WIN32
|
||||
|
||||
#include <cstddef>
|
||||
#include <vector>
|
||||
|
||||
#include "lice/lice.h"
|
||||
#include "wdltypes.h"
|
||||
|
||||
#include "core/ui/rect.h"
|
||||
#include "core/ui/stroke_aa.h"
|
||||
|
||||
namespace reasampler::vst {
|
||||
|
||||
// Blends a polyline as one antialiased stroke of `widthPx`. Clipped to the bitmap. Takes a raw
|
||||
// span so a short stroke (a knob needle) can pass a stack array and cost no allocation.
|
||||
void strokePolylineAA(LICE_IBitmap* bmp, const ui::StrokePoint* pts, std::size_t count,
|
||||
float widthPx, LICE_pixel color, float alpha = 1.0f);
|
||||
|
||||
inline void strokePolylineAA(LICE_IBitmap* bmp, const std::vector<ui::StrokePoint>& pts,
|
||||
float widthPx, LICE_pixel color, float alpha = 1.0f) {
|
||||
strokePolylineAA(bmp, pts.data(), pts.size(), widthPx, color, alpha);
|
||||
}
|
||||
|
||||
// A single antialiased segment — the two-point polyline, spelled out for the call sites that
|
||||
// have exactly two endpoints.
|
||||
inline void strokeLineAA(LICE_IBitmap* bmp, float x0, float y0, float x1, float y1, float widthPx,
|
||||
LICE_pixel color, float alpha = 1.0f) {
|
||||
const ui::StrokePoint pts[2] = {{x0, y0}, {x1, y1}};
|
||||
strokePolylineAA(bmp, pts, 2, widthPx, color, alpha);
|
||||
}
|
||||
|
||||
// Blends a circular arc as one antialiased stroke of `widthPx`. Angles are radians in LICE's
|
||||
// convention (0 = 12 o'clock, increasing clockwise), matching what LICE_Arc took.
|
||||
void strokeArcAA(LICE_IBitmap* bmp, float cx, float cy, float radius, float startRad, float endRad,
|
||||
float widthPx, LICE_pixel color, float alpha = 1.0f);
|
||||
|
||||
} // namespace reasampler::vst
|
||||
|
||||
#endif // _WIN32
|
||||
Reference in New Issue
Block a user