feat(ui): D.2b-B — UiMeter vertical fill (burden bar, retail m_eDirection 2/4)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
fb050aed4d
commit
5e75d2ac76
2 changed files with 96 additions and 8 deletions
|
|
@ -55,6 +55,14 @@ public sealed class UiMeter : UiElement
|
|||
/// <summary>Coloured-fill right-cap RenderSurface id.</summary>
|
||||
public uint FrontRight { get; set; }
|
||||
|
||||
/// <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
|
||||
|
|
@ -71,6 +79,18 @@ public sealed class UiMeter : UiElement
|
|||
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();
|
||||
|
|
@ -78,14 +98,25 @@ public sealed class UiMeter : UiElement
|
|||
|
||||
if (SpriteResolve is { } resolve && (BackLeft != 0 || BackTile != 0 || FrontTile != 0))
|
||||
{
|
||||
// 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.
|
||||
DrawHBar(ctx, resolve, BackLeft, BackTile, BackRight, Width);
|
||||
if (pct is not null && p > 0f)
|
||||
DrawHBar(ctx, resolve, FrontLeft, FrontTile, FrontRight, Width * p);
|
||||
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.
|
||||
DrawHBar(ctx, resolve, BackLeft, BackTile, BackRight, Width);
|
||||
if (pct is not null && p > 0f)
|
||||
DrawHBar(ctx, resolve, FrontLeft, FrontTile, FrontRight, Width * p);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -158,6 +189,28 @@ public sealed class UiMeter : UiElement
|
|||
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>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).
|
||||
|
|
|
|||
35
tests/AcDream.App.Tests/UI/UiMeterVerticalTests.cs
Normal file
35
tests/AcDream.App.Tests/UI/UiMeterVerticalTests.cs
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
using AcDream.App.UI;
|
||||
using Xunit;
|
||||
|
||||
namespace AcDream.App.Tests.UI;
|
||||
|
||||
public class UiMeterVerticalTests
|
||||
{
|
||||
[Fact]
|
||||
public void Vertical_fill_from_bottom_occupies_lower_fraction()
|
||||
{
|
||||
// 11x58 bar, 50% → fill rect is the bottom 29px.
|
||||
var (x, y, w, h) = UiMeter.ComputeVFillRect(0.5f, 11f, 58f, fromBottom: true);
|
||||
Assert.Equal(0f, x);
|
||||
Assert.Equal(29f, y, 3); // h - h*p = 58 - 29
|
||||
Assert.Equal(11f, w);
|
||||
Assert.Equal(29f, h, 3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Vertical_fill_from_top_starts_at_zero()
|
||||
{
|
||||
var (_, y, _, h) = UiMeter.ComputeVFillRect(0.25f, 11f, 58f, fromBottom: false);
|
||||
Assert.Equal(0f, y);
|
||||
Assert.Equal(14.5f, h, 3);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(-1f, 0f)]
|
||||
[InlineData(2f, 58f)]
|
||||
public void Vertical_fill_clamps_fraction(float pct, float expectedH)
|
||||
{
|
||||
var (_, _, _, h) = UiMeter.ComputeVFillRect(pct, 11f, 58f, fromBottom: true);
|
||||
Assert.Equal(expectedH, h, 3);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue