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>
|
/// <summary>Coloured-fill right-cap RenderSurface id.</summary>
|
||||||
public uint FrontRight { get; set; }
|
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; }
|
public UiMeter() { ClickThrough = true; }
|
||||||
|
|
||||||
/// <summary>The meter draws its own 3-slice bars; the importer must not build its
|
/// <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);
|
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)
|
protected override void OnDraw(UiRenderContext ctx)
|
||||||
{
|
{
|
||||||
float? pct = Fill();
|
float? pct = Fill();
|
||||||
|
|
@ -78,14 +98,25 @@ public sealed class UiMeter : UiElement
|
||||||
|
|
||||||
if (SpriteResolve is { } resolve && (BackLeft != 0 || BackTile != 0 || FrontTile != 0))
|
if (SpriteResolve is { } resolve && (BackLeft != 0 || BackTile != 0 || FrontTile != 0))
|
||||||
{
|
{
|
||||||
// Retail meter (UIElement_Meter::DrawChildren): the BACK 3-slice is the
|
if (Vertical)
|
||||||
// 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.
|
// Track full height, fill clipped to the fraction along the fill direction.
|
||||||
// The front carries its own right-cap (shown at 100%); clipping below 100%
|
// The burden meter is a single-tile bar (BackTile/FrontTile, no caps).
|
||||||
// removes it and reveals the back track's right-cap — retail's scissor-fill.
|
DrawVBar(ctx, resolve, BackTile, Height, fromBottom: FillFromBottom, isFill: false);
|
||||||
DrawHBar(ctx, resolve, BackLeft, BackTile, BackRight, Width);
|
if (pct is not null && p > 0f)
|
||||||
if (pct is not null && p > 0f)
|
DrawVBar(ctx, resolve, FrontTile, Height * p, fromBottom: FillFromBottom, isFill: true);
|
||||||
DrawHBar(ctx, resolve, FrontLeft, FrontTile, FrontRight, Width * p);
|
}
|
||||||
|
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
|
else
|
||||||
{
|
{
|
||||||
|
|
@ -158,6 +189,28 @@ public sealed class UiMeter : UiElement
|
||||||
DrawPiece(ctx, rt, w - capR, capR, rw, 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>Draw a slice over local [<paramref name="pieceX"/>,
|
/// <summary>Draw a slice over local [<paramref name="pieceX"/>,
|
||||||
/// pieceX+<paramref name="pieceW"/>], with the texture repeating every
|
/// pieceX+<paramref name="pieceW"/>], with the texture repeating every
|
||||||
/// <paramref name="nativeW"/> px (UV-repeat — the UI texture is GL_REPEAT-wrapped).
|
/// <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