acdream/src/AcDream.App/UI/UiMeter.cs
Erik fdc4fd496d fix(ui): gate — no void frames around the login wormhole; vitals icons centered
The login tunnel now covers from the first world-facing frame (the
sky-void backdrop can never present pre-tunnel) and holds through an
atomic tunnel-to-world swap at reveal completion — the void is
structurally unreachable on both edges, pinned by frame-sequence tests
across WorldSceneRenderer/WorldRevealCoordinator/LocalPlayerTeleport-
Controller/RuntimeWorldTransitState. Vitals detail icons draw at their
authored centered offsets in both stacked and side-by-side layouts.
Implemented and live-probed by the fix agent; finalized by the lead
after the agent parked post-verification (gates re-run green:
App 5512/3, Runtime 1747/0).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-17 12:30:51 +02:00

499 lines
25 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System.Numerics;
namespace AcDream.App.UI;
/// <summary>Justification of a meter's absorbed state caption (dat property
/// 0x14 on the caption child's own state: 0/2 = Left, 3/5 = Right, 1 = Center
/// — the same mapping ElementReader applies at element level).</summary>
public enum UiMeterLabelAlign : byte { Left = 0, Center = 1, Right = 2 }
/// <summary>
/// One absorbed vitals detail-icon overlay (the <c>0x100004A9</c> child of a
/// meter's back/front slice container): its ShowDetail sprite, authored rect
/// (local to the container, which spans the meter at 0,0), the authored raw
/// edge-anchor modes (<c>ElementDesc</c> Left/Top/Right/Bottom, values 04),
/// and the container's authored size. The overlays author <c>L3/R3</c> —
/// retail's CENTER anchors — so when the meter is resized away from its
/// authored width the icon re-centers through the exact
/// <see cref="UiLayoutPolicy"/> port of
/// <c>UIElement::UpdateForParentSizeChange @0x00462640</c> instead of staying
/// pinned at its authored X (the gate-observed left-drift).
/// </summary>
internal readonly record struct UiMeterDetailOverlaySpec(
uint Sprite,
float X, float Y, float W, float H,
uint LeftMode, uint TopMode, uint RightMode, uint BottomMode,
float ParentW, float ParentH);
/// <summary>
/// A horizontal vital bar (retail HP/Stamina/Mana style): a background rect, a
/// partial-width solid fill, and an optional centered "current/max" numeric
/// overlay. <see cref="Fill"/> returns 0..1 (null = no data → empty bar);
/// <see cref="Label"/> returns the overlay text (null = no number).
///
/// <para>
/// Solid-color fill + debug font for Spec 1. The retail gradient bar sprite
/// (glassy center highlight) and the retail dat font are a later polish pass —
/// retail's vitals are bars exactly like this, just sprited.
/// </para>
/// </summary>
public sealed class UiMeter : UiElement, IUiDatStateful
{
private readonly Dictionary<uint, uint> _stateFillSprites = new();
private readonly Dictionary<uint, (string Text, UiMeterLabelAlign Align)> _stateLabels = new();
private (string Text, UiMeterLabelAlign Align)? _activeStateLabel;
// Vitals ShowDetail icon overlays (see ConfigureDetailOverlay).
private bool _detailConfigured;
private bool _detailPassToChildren;
private UiMeterDetailOverlaySpec _detailBack;
private UiMeterDetailOverlaySpec _detailFront;
/// <summary>True when this meter absorbed the vitals detail-icon overlays. Exposed for tests.</summary>
internal bool HasDetailOverlay => _detailConfigured;
/// <summary>The dim back-container detail icon (ShowDetail media). Exposed for tests.</summary>
internal uint DetailBackSprite => _detailBack.Sprite;
/// <summary>The bright fill-clipped front-container detail icon. Exposed for tests.</summary>
internal uint DetailFrontSprite => _detailFront.Sprite;
/// <summary>The back overlay's authored meter-local rect. Exposed for tests.</summary>
internal (float X, float Y, float W, float H) DetailBackRect =>
(_detailBack.X, _detailBack.Y, _detailBack.W, _detailBack.H);
/// <summary>The complete absorbed back overlay. Exposed for tests.</summary>
internal UiMeterDetailOverlaySpec DetailBack => _detailBack;
/// <summary>The complete absorbed front overlay. Exposed for tests.</summary>
internal UiMeterDetailOverlaySpec DetailFront => _detailFront;
/// <summary>Dat element id, set by the layout importer so duplicated page copies can be scoped.</summary>
public uint ElementId { get; set; }
/// <summary>Fill fraction provider; a null result draws an empty bar.</summary>
public Func<float?> Fill { get; set; } = () => 0f;
/// <summary>Centered overlay text provider (e.g. "291/291"); null = none.</summary>
public Func<string?> Label { get; set; } = () => null;
public Vector4 BarColor { get; set; } = new(1f, 0f, 0f, 1f);
public Vector4 BgColor { get; set; } = new(0f, 0f, 0f, 0.5f);
public Vector4 LabelColor { get; set; } = new(1f, 1f, 1f, 1f);
/// <summary>Retail LayoutDesc property <c>0x21</c> (two-pass glyph outline,
/// <c>UIElement_Text::SetOutline @0x0046a81c</c>). Seeded by DatWidgetFactory
/// from the element's effective-default state, same as <see cref="UiText.Outline"/>
/// (round-5 review S2 — per-STATE switching is AP-192).</summary>
public bool Outline { get; set; }
/// <summary>Retail LayoutDesc property <c>0x22</c> (<c>m_curOutlineColor</c>,
/// ctor default black). Only meaningful when <see cref="Outline"/> is true.</summary>
public Vector4 OutlineColor { get; set; } = UiRenderContext.DefaultOutlineColor;
/// <summary>Retail dat font (Font 0x40000000) for the "cur/max" overlay. When
/// set, the label renders through the dat-font two-pass blit (outline + fill);
/// when null, the debug <see cref="UiRenderContext.DefaultFont"/> bitmap font
/// is used instead. Set by the host when the retail UI is active.</summary>
public UiDatFont? DatFont { get; set; }
/// <summary>Resolver from a RenderSurface DataId to (GL handle, w, h). When set
/// with the 9-slice ids below, the bar draws the retail sprites instead of solid color.</summary>
public Func<uint, (uint tex, int w, int h)>? SpriteResolve { get; set; }
// Retail vital bars are a horizontal 3-slice: a fixed-width bevelled left-cap,
// a TILED gradient middle (the "fill-tile" repeats at native width — it does not
// stretch), and a fixed-width right-cap. The "back" slice is the empty track
// (drawn full width); the "front" slice is the coloured fill (drawn full-geometry
// but CLIPPED to the fill fraction — its own right-cap shows at 100%, the back's
// shows through when partial). Ids come from the stacked vitals LayoutDesc
// (0x2100006C) via the dump-vitals-layout CLI; 0 = none.
/// <summary>Empty-track left-cap RenderSurface id.</summary>
public uint BackLeft { get; set; }
/// <summary>Empty-track middle (tiled gradient) RenderSurface id.</summary>
public uint BackTile { get; set; }
/// <summary>Empty-track right-cap RenderSurface id.</summary>
public uint BackRight { get; set; }
/// <summary>Coloured-fill left-cap RenderSurface id.</summary>
public uint FrontLeft { get; set; }
/// <summary>Coloured-fill middle (tiled gradient) RenderSurface id.</summary>
public uint FrontTile { get; set; }
/// <summary>Coloured-fill right-cap RenderSurface id.</summary>
public uint FrontRight { get; set; }
/// <summary>The active numeric DAT state for a stateful fill meter.</summary>
public uint ActiveRetailStateId { get; private set; }
/// <summary>The caption latched by the active retail state (null when the
/// state authors none). Exposed for tests.</summary>
internal string? ActiveStateLabel => _activeStateLabel?.Text;
/// <summary>The latched caption's authored justification. Exposed for tests.</summary>
internal UiMeterLabelAlign? ActiveStateLabelAlign => _activeStateLabel?.Align;
/// <summary>
/// Registers a fill sprite carried by a child state of the imported meter.
/// Used by the powerbar shape in LayoutDesc 0x21000072, whose track is on
/// the meter and whose Jump/Melee/Missile/DDD fills are states of one child.
/// </summary>
internal void ConfigureStateFill(uint stateId, uint spriteId)
{
if (stateId != 0 && spriteId != 0)
_stateFillSprites[stateId] = spriteId;
}
/// <summary>
/// Registers a caption carried by a state of the meter's absorbed Type-12
/// child (gmPowerbarUI's 0x10000035: JumpMode 'Height' / MeleeMode 'Power'
/// / MissileMode 'Accuracy'). Retail shows it via the meter's
/// PassToChildren state cascade; the absorbed equivalent latches it in
/// <see cref="TrySetRetailState"/> and draws it when no live
/// <see cref="Label"/> provider supplies text. Each mode STATE authors its
/// own justification (dat property 0x14 — the powerbar modes author 0x3 =
/// Right; only the element default is centered), so alignment rides the
/// state entry.
/// </summary>
internal void ConfigureStateLabel(uint stateId, string text, UiMeterLabelAlign align)
{
if (stateId != 0 && !string.IsNullOrEmpty(text))
_stateLabels[stateId] = (text, align);
}
/// <summary>
/// Registers the vitals "detail" icon overlays absorbed from the meter's
/// two slice containers (the <c>0x100004A9</c> children — health heart
/// 0x06007490/91, stamina sword 0x06007492/93, mana scepter 0x06007494/95).
/// Retail authors each icon TWICE: a dim version in the BACK container
/// (drawn unclipped over the empty track) and a bright version in the
/// FRONT container (fill-clipped with the rest of the front layer —
/// <c>UIElement_Meter::DrawChildren @0x0046FBD0</c> clips the whole
/// <c>m_pcChildImage</c> child, element id 2, to the 0x69 fraction), so
/// the icon itself fills up with the vital. Both overlays author media
/// ONLY for <c>ShowDetail</c> (<c>HideDetail</c> authors File=0), so they
/// draw solely in that state. Each spec carries the overlay's authored
/// rect (local to its container, which spans the meter at 0,0), its raw
/// edge-anchor modes, and the container's authored size — see
/// <see cref="ComputeDetailOverlayRect"/> for how those position the icon
/// on a resized meter.
/// </summary>
internal void ConfigureDetailOverlay(
in UiMeterDetailOverlaySpec back,
in UiMeterDetailOverlaySpec front,
bool passToChildren)
{
_detailBack = back;
_detailFront = front;
_detailConfigured = back.Sprite != 0 || front.Sprite != 0;
_detailPassToChildren = passToChildren;
}
/// <summary>
/// The overlay's effective meter-local rect at the meter's CURRENT size.
/// At the authored container size the authored rect stands verbatim —
/// retail's <c>UIElement::UpdateForParentSizeChange @0x00462640</c> runs
/// only when a parent actually resizes, and the designers hand-placed
/// rects that are not always the exact center formula (the stamina sword
/// authors X=32 where the mode-3 formula yields 33). On any other size
/// the authored edge modes are applied from the ORIGINAL child/parent
/// rects through <see cref="UiLayoutPolicy.Apply"/>, the exact port of
/// that retail routine — the overlays' authored <c>L3/R3</c> center
/// anchors are what keep the heart/sword/scepter centered when the
/// vitals window is resized (retail near mode 3 =
/// <c>curParentW/2 - origChildW/2</c> @0x004627ca; far mode 3 =
/// <c>curParentW/2 + origChildW/2 - 1</c> @0x00462827, integer
/// arithmetic, so odd authored widths lose one pixel exactly as retail
/// does).
/// </summary>
internal static (float X, float Y, float W, float H) ComputeDetailOverlayRect(
in UiMeterDetailOverlaySpec overlay, float meterW, float meterH)
{
int parentW = (int)meterW, parentH = (int)meterH;
int origParentW = (int)overlay.ParentW, origParentH = (int)overlay.ParentH;
if (origParentW <= 0 || origParentH <= 0
|| (parentW == origParentW && parentH == origParentH))
{
return (overlay.X, overlay.Y, overlay.W, overlay.H);
}
var originalChild = UiPixelRect.FromPositionAndSize(
(int)overlay.X, (int)overlay.Y, (int)overlay.W, (int)overlay.H);
var originalParent = UiPixelRect.FromPositionAndSize(
0, 0, origParentW, origParentH);
var currentParent = UiPixelRect.FromPositionAndSize(
0, 0, parentW, parentH);
UiPixelRect effective = UiLayoutPolicy.Apply(
overlay.LeftMode,
overlay.TopMode,
overlay.RightMode,
overlay.BottomMode,
originalChild,
originalParent,
originalChild,
currentParent);
return (effective.X0, effective.Y0, effective.Width, effective.Height);
}
public bool TrySetRetailState(uint stateId)
{
// Vitals detail toggle (gmVitalsUI::ListenToElementMessage @0x004BFC00
// flips the window root between HideDetail 0x10000006 and ShowDetail
// 0x10000007; the meter receives the state through the authored
// PassToChildren cascade — UIElement::SetState @0x00464E70). The
// meter's own HideDetail/ShowDetail StateDescs are media-less and
// exist purely to keep propagating (PassToChildren=true), so forward
// to the absorbed text child (whose per-state 0x3B hides the numbers
// in ShowDetail) and let OnDraw key the icon overlays off the state.
if (_detailConfigured
&& stateId is RetailUiStateIds.HideDetail or RetailUiStateIds.ShowDetail)
{
ActiveRetailStateId = stateId;
if (_detailPassToChildren)
foreach (UiElement child in Children)
if (child is IUiDatStateful stateful)
stateful.TrySetRetailState(stateId);
return true;
}
bool hasFill = _stateFillSprites.TryGetValue(stateId, out uint spriteId);
bool hasLabel = _stateLabels.TryGetValue(stateId, out var caption);
if (!hasFill && !hasLabel)
return false;
ActiveRetailStateId = stateId;
if (hasFill)
{
FrontLeft = 0;
FrontTile = spriteId;
FrontRight = 0;
}
_activeStateLabel = hasLabel ? caption : null;
return true;
}
/// <summary>Vertical orientation (retail <c>m_eDirection</c> 2/4). Default false =
/// horizontal (direction 1). The burden bar is the only vertical meter today.</summary>
public bool Vertical { get; set; }
/// <summary>For a vertical meter, fill grows from the bottom up (retail direction 4)
/// when true, top-down (direction 2) when false. Default bottom-up. Visual-confirmed.</summary>
public bool FillFromBottom { get; set; } = true;
public UiMeter() { ClickThrough = true; }
/// <summary>The meter draws its own 3-slice bars; the importer must not build its
/// grandchild slice/text elements as separate widgets.</summary>
public override bool ConsumesDatChildren => true;
private static float AlignX(UiMeterLabelAlign align, float width, float textWidth)
=> align switch
{
UiMeterLabelAlign.Left => 0f,
UiMeterLabelAlign.Right => width - textWidth,
_ => (width - textWidth) * 0.5f,
};
/// <summary>Clamp <paramref name="pct"/> to [0,1] and return the fill rect
/// (local px) for a bar of <paramref name="w"/> x <paramref name="h"/>.</summary>
public static (float x, float y, float w, float h) ComputeFillRect(
float pct, float w, float h)
{
if (pct < 0f) pct = 0f;
if (pct > 1f) pct = 1f;
return (0f, 0f, w * pct, h);
}
/// <summary>Clamp <paramref name="pct"/> to [0,1] and return the vertical fill rect
/// (local px). <paramref name="fromBottom"/> true → the fill occupies the bottom
/// <c>h*pct</c> px (retail direction 4); false → the top (direction 2).</summary>
public static (float x, float y, float w, float h) ComputeVFillRect(
float pct, float w, float h, bool fromBottom)
{
if (pct < 0f) pct = 0f;
if (pct > 1f) pct = 1f;
float fh = h * pct;
return (0f, fromBottom ? h - fh : 0f, w, fh);
}
protected override void OnDraw(UiRenderContext ctx)
{
float? pct = Fill();
float p = pct is float pf ? (pf < 0f ? 0f : pf > 1f ? 1f : pf) : 0f;
if (SpriteResolve is { } resolve && (BackLeft != 0 || BackTile != 0 || FrontTile != 0))
{
if (Vertical)
{
// Track full height, fill clipped to the fraction along the fill direction.
// The burden meter is a single-tile bar (BackTile/FrontTile, no caps).
DrawVBar(ctx, resolve, BackTile, Height, fromBottom: FillFromBottom, isFill: false);
if (pct is not null && p > 0f)
DrawVBar(ctx, resolve, FrontTile, Height * p, fromBottom: FillFromBottom, isFill: true);
}
else
{
// Retail meter (UIElement_Meter::DrawChildren): the BACK 3-slice is the
// empty track, drawn full width; the FRONT 3-slice is the coloured fill,
// drawn at FULL width too but horizontally CLIPPED to the fill fraction.
// The front carries its own right-cap (shown at 100%); clipping below 100%
// removes it and reveals the back track's right-cap — retail's scissor-fill.
//
// ShowDetail icon overlays ride their authored containers: the dim BACK
// icon draws over the full track (its 0x100004A9 child draws AFTER the
// three slice children — higher ReadOrder within the back container);
// the bright FRONT icon is clipped with the rest of the front layer to
// the fill fraction (retail clips the whole element-id-2 child).
bool detail = _detailConfigured
&& ActiveRetailStateId == RetailUiStateIds.ShowDetail;
DrawHBar(ctx, resolve, BackLeft, BackTile, BackRight, Width);
if (detail)
{
DrawDetailIcon(
ctx, resolve, _detailBack.Sprite,
ComputeDetailOverlayRect(in _detailBack, Width, Height),
Width);
}
if (pct is not null && p > 0f)
{
DrawHBar(ctx, resolve, FrontLeft, FrontTile, FrontRight, Width * p);
if (detail)
{
DrawDetailIcon(
ctx, resolve, _detailFront.Sprite,
ComputeDetailOverlayRect(in _detailFront, Width, Height),
Width * p);
}
}
}
}
else
{
// Placeholder solid-color fallback.
ctx.DrawRect(0, 0, Width, Height, BgColor);
if (pct is not null && p > 0f)
{
var (fx, fy, fw, fh) = ComputeFillRect(p, Width, Height);
if (fw > 0f) ctx.DrawRect(fx, fy, fw, fh, BarColor);
}
}
// A live provider (vitals "cur/max") wins and stays centered;
// otherwise the active retail-state caption shows with its own
// authored justification (the powerbar modes author Right).
string? label = Label();
UiMeterLabelAlign align = UiMeterLabelAlign.Center;
if (string.IsNullOrEmpty(label) && _activeStateLabel is { } stateLabel)
(label, align) = stateLabel;
if (!string.IsNullOrEmpty(label))
{
if (DatFont is { } datFont)
{
// Retail path: the dat font's two-pass blit.
float tw = datFont.MeasureWidth(label);
float tx = AlignX(align, Width, tw);
float ty = (Height - datFont.LineHeight) * 0.5f;
ctx.DrawStringDat(datFont, label, tx, ty, LabelColor, Outline, OutlineColor);
}
else if (ctx.DefaultFont is { } font)
{
// Fallback: debug bitmap font (no dat font available).
float tw = font.MeasureWidth(label);
float tx = AlignX(align, Width, tw);
float ty = (Height - font.LineHeight) * 0.5f;
ctx.DrawString(label, tx, ty, LabelColor);
}
}
}
/// <summary>
/// Draws the full-width horizontal 3-slice (native-width left-cap, stretched
/// middle, native-width right-cap) over this meter's rect, horizontally CLIPPED
/// so nothing past <paramref name="clipW"/> (local px from the left) is drawn.
/// The back track passes <c>clipW = Width</c>; the front fill passes
/// <c>clipW = Width * fraction</c>. Clipping UV-crops each slice proportionally,
/// so the fill ends cleanly and the back's right-cap shows through when partial.
/// A 0 id skips that slice.
/// </summary>
private void DrawHBar(
UiRenderContext ctx, Func<uint, (uint tex, int w, int h)> resolve,
uint leftId, uint midId, uint rightId, float clipW)
{
if (clipW <= 0f) return;
float w = Width, h = Height;
// Only resolve a slice when its id is non-zero. resolve(0) returns the 1x1 MAGENTA
// placeholder with a NON-ZERO GL handle, so resolving a zero (absent) cap id and then
// testing `tex != 0` would draw a 1px magenta cap. The single-image meter (toolbar
// selected-object bar) has no left/right caps (ids 0); the 3-slice vitals meter sets
// all six ids. Guard on the id, not the resolved handle.
var (lt, lw, _) = leftId != 0 ? resolve(leftId) : (0u, 0, 0);
var (mt, mw, _) = midId != 0 ? resolve(midId) : (0u, 0, 0);
var (rt, rw, _) = rightId != 0 ? resolve(rightId) : (0u, 0, 0);
float capL = lt != 0 ? MathF.Min(lw, w) : 0f;
float capR = rt != 0 ? MathF.Min(rw, w - capL) : 0f;
float midW = w - capL - capR;
// Each slice's texture repeats every NATIVE-width px (UV-repeat; the UI
// texture is GL_REPEAT-wrapped — TextureCache.UploadRgba8). Caps span their
// own native width → a single 1:1 copy. The wide middle spans many native
// widths → it TILES, matching retail's "fill-tile" + ImgTex::TileCSI rather
// than stretching one copy. (Same UV-repeat the chrome border already uses.)
DrawPiece(ctx, lt, 0f, capL, lw, h, clipW);
DrawPiece(ctx, mt, capL, midW, mw, h, clipW);
DrawPiece(ctx, rt, w - capR, capR, rw, h, clipW);
}
/// <summary>Draws a single-tile vertical bar slice. The track (<paramref name="isFill"/>
/// false) spans the full height; the fill spans <paramref name="visibleH"/> px from the
/// bottom (<paramref name="fromBottom"/>) or top, UV-cropped to that fraction of the
/// sprite so the fill reveals the matching part of the art (retail
/// UIElement_Meter::DrawChildren Box2D clip, direction 2/4). A 0 id is a no-op.</summary>
private void DrawVBar(UiRenderContext ctx, Func<uint, (uint tex, int w, int h)> resolve,
uint tileId, float visibleH, bool fromBottom, bool isFill)
{
if (tileId == 0 || visibleH <= 0f) return;
var (tex, _, _) = resolve(tileId);
if (tex == 0) return;
float w = Width, h = Height;
if (visibleH > h) visibleH = h;
float frac = h > 0f ? visibleH / h : 0f;
// Bottom-up fill: bottom of the rect AND bottom of the sprite (v in [1-frac, 1]).
// Top-down / track: top of the rect AND top of the sprite (v in [0, frac]).
float y = isFill && fromBottom ? h - visibleH : 0f;
float v0 = isFill && fromBottom ? 1f - frac : 0f;
float v1 = isFill && fromBottom ? 1f : frac;
ctx.DrawSprite(tex, 0f, y, w, visibleH, 0f, v0, 1f, v1, System.Numerics.Vector4.One);
}
/// <summary>
/// Draws one ShowDetail icon overlay at its authored meter-local rect,
/// horizontally clipped to <paramref name="clipW"/> local px (the back icon
/// passes the full width; the front icon passes <c>Width * fraction</c>,
/// mirroring retail's whole-front-container fill clip). The visible portion
/// is UV-cropped so the icon reveals left-to-right with the fill. Height is
/// clamped to the meter's box — retail clips children to the parent rect
/// (the stamina FRONT overlay authors H=28 in a 16px bar; retail shows 16).
/// </summary>
private void DrawDetailIcon(
UiRenderContext ctx, Func<uint, (uint tex, int w, int h)> resolve,
uint spriteId, (float X, float Y, float W, float H) rect, float clipW)
{
if (spriteId == 0 || rect.W <= 0f || rect.H <= 0f) return;
var (tex, _, _) = resolve(spriteId);
if (tex == 0) return;
float visibleW = MathF.Min(rect.W, clipW - rect.X);
if (visibleW <= 0f) return;
float h = MathF.Min(rect.H, Height - rect.Y);
if (h <= 0f) return;
float u1 = visibleW / rect.W;
ctx.DrawSprite(tex, rect.X, rect.Y, visibleW, h, 0f, 0f, u1, 1f, Vector4.One);
}
/// <summary>Draw a slice over local [<paramref name="pieceX"/>,
/// pieceX+<paramref name="pieceW"/>], with the texture repeating every
/// <paramref name="nativeW"/> px (UV-repeat — the UI texture is GL_REPEAT-wrapped).
/// Clipped so nothing past <paramref name="clipW"/> shows. For a cap (span == native)
/// this is one 1:1 copy; for the wide middle it tiles; a partial last copy is
/// UV-cropped.</summary>
private static void DrawPiece(
UiRenderContext ctx, uint tex, float pieceX, float pieceW, float nativeW, float h, float clipW)
{
if (tex == 0 || pieceW <= 0f || nativeW <= 0f) return;
float visibleW = MathF.Min(pieceW, clipW - pieceX);
if (visibleW <= 0f) return;
float u1 = visibleW / nativeW; // >1 ⇒ texture repeats (tiles); ≤1 ⇒ a partial copy
ctx.DrawSprite(tex, pieceX, 0f, visibleW, h, 0f, 0f, u1, 1f, Vector4.One);
}
}