fix retained glyph bottom clipping

This commit is contained in:
gamer147
2026-07-30 20:29:39 -04:00
parent d9f24c69ef
commit 49722c55fe
9 changed files with 185 additions and 13 deletions

View File

@@ -193,6 +193,40 @@ public class GlyphMaskCompositorTests
Assert.Equal(255, Pixel(destination, 2, 7).A);
}
[Fact]
public void RetainedPresentationCropIncludesInkBelowTheNativeCell()
{
var mask = new GlyphMask(
width: 1, height: 4, stride: 1,
originX: 0, originY: 3,
cellAdvanceX: 4, cellAdvanceY: 0,
cellWidth: 4, cellHeight: 5,
coverage: [16, 16, 16, 16]);
var destination = Image(16, 16);
var engine = new RetainedGlyphLayoutEngine(
new CountingRasterizer(_ => mask));
GlyphTextLayoutResult result = engine.Render(
destination,
new GlyphTextLayoutOptions(
CursorX: 2, CursorY: 1, LineOriginX: 2,
RightBound: 16, BottomBound: 16, WrapHorizontally: true,
Style(renderMode: 3) with
{
EffectOffsetX = 1,
EffectOffsetY = 1,
}),
[NativeRequest(0x82a0, 0x3042)]);
Assert.Equal(
new AdvRetainedGlyphRecord(0, 2, 1, 6, 6),
Assert.Single(result.Records));
Assert.Equal(
new AdvRetainedGlyphPresentationRect(2, 1, 6, 8),
Assert.Single(result.PresentationRects));
Assert.NotEqual(0, Pixel(destination, 2, 7).A);
}
[Fact]
public void ClosingPunctuationStaysOnOverflowingPrecedingLine()
{

View File

@@ -79,6 +79,29 @@ public class RetainedAdvTextLayoutPresentationTests
gfx.SnapshotVisibleObjects().Select(item => (item.DstX, item.DstY)));
}
[Fact]
public void PublicationUsesInkSafeCropWithoutChangingNativeRecord()
{
var presentation = new RetainedAdvTextLayoutPresentation(
new AdvTextLayoutPresentationBinding(
1, 21, 100, 4, -1, 10, 20));
var record = new AdvRetainedGlyphRecord(0, 3, 4, 7, 10);
presentation.Append(
[record],
[new AdvRetainedGlyphPresentationRect(3, 3, 7, 12)],
layoutOriginX: 30,
layoutOriginY: 40);
var gfx = new GfxState();
gfx.CreateSurface(21);
presentation.PublishThrough(gfx, 1);
RenderObject item = Assert.Single(gfx.SnapshotVisibleObjects());
Assert.Equal((3, 3, 4, 9, 33, 43),
(item.SrcX, item.SrcY, item.W, item.H, item.DstX, item.DstY));
Assert.Equal(record, Assert.Single(presentation.Glyphs).Record);
}
[Fact]
public void SavedFrameStyleReconstructionRebuildsTheSameBindings()
{

View File

@@ -34,8 +34,23 @@ public readonly record struct AdvTextLayoutPresentationBinding(
int ResetCursorX,
int ResetCursorY);
/// <summary>
/// Transient publication crop. Native records retain measured cell edges, but a backend mask/effect can
/// carry ink outside that cell. Keeping the expanded crop separate preserves the native layout ABI.
/// </summary>
public readonly record struct AdvRetainedGlyphPresentationRect(
int Left,
int Top,
int Right,
int Bottom)
{
public int Width => Right - Left;
public int Height => Bottom - Top;
}
public readonly record struct AdvRetainedGlyphPlacement(
AdvRetainedGlyphRecord Record,
AdvRetainedGlyphPresentationRect PresentationRect,
int LayoutOriginX,
int LayoutOriginY);

View File

@@ -38,7 +38,30 @@ public sealed class RetainedAdvTextLayoutPresentation
int first = _glyphs.Count;
foreach (AdvRetainedGlyphRecord record in records)
_glyphs.Add(new AdvRetainedGlyphPlacement(
record, layoutOriginX, layoutOriginY));
record,
new AdvRetainedGlyphPresentationRect(
record.Left, record.Top, record.Right, record.Bottom),
layoutOriginX,
layoutOriginY));
return first;
}
public int Append(
IReadOnlyList<AdvRetainedGlyphRecord> records,
IReadOnlyList<AdvRetainedGlyphPresentationRect> presentationRects,
int layoutOriginX,
int layoutOriginY)
{
ArgumentNullException.ThrowIfNull(records);
ArgumentNullException.ThrowIfNull(presentationRects);
if (presentationRects.Count != records.Count)
throw new ArgumentException(
"Every retained glyph record requires one presentation rectangle.",
nameof(presentationRects));
int first = _glyphs.Count;
for (int index = 0; index < records.Count; index++)
_glyphs.Add(new AdvRetainedGlyphPlacement(
records[index], presentationRects[index], layoutOriginX, layoutOriginY));
return first;
}
@@ -71,16 +94,16 @@ public sealed class RetainedAdvTextLayoutPresentation
private void Bind(GfxState gfx, int index)
{
AdvRetainedGlyphPlacement placement = _glyphs[index];
AdvRetainedGlyphRecord record = placement.Record;
if (record.Width <= 0 || record.Height <= 0) return;
AdvRetainedGlyphPresentationRect rect = placement.PresentationRect;
if (rect.Width <= 0 || rect.Height <= 0) return;
gfx.BindDraw(
checked(Binding.FirstObjectHandle + index),
Binding.SourceSurfaceSlot,
record.Left,
record.Top,
record.Width,
record.Height,
checked(placement.LayoutOriginX + record.Left),
checked(placement.LayoutOriginY + record.Top));
rect.Left,
rect.Top,
rect.Width,
rect.Height,
checked(placement.LayoutOriginX + rect.Left),
checked(placement.LayoutOriginY + rect.Top));
}
}

View File

@@ -14,6 +14,7 @@ public readonly record struct GlyphTextLayoutOptions(
public sealed record GlyphTextLayoutResult(
IReadOnlyList<AdvRetainedGlyphRecord> Records,
IReadOnlyList<AdvRetainedGlyphPresentationRect> PresentationRects,
int CursorX,
int CursorY,
int ConsumedGlyphs,
@@ -40,6 +41,8 @@ public sealed class RetainedGlyphLayoutEngine
ArgumentNullException.ThrowIfNull(destination);
ArgumentNullException.ThrowIfNull(glyphs);
var records = new List<AdvRetainedGlyphRecord>(glyphs.Count);
var presentationRects =
new List<AdvRetainedGlyphPresentationRect>(glyphs.Count);
int cursorX = options.CursorX;
int cursorY = options.CursorY;
int wrappedLines = 0;
@@ -81,11 +84,59 @@ public sealed class RetainedGlyphLayoutEngine
AgeGlyphMaskCompositor.DrawGlyph(
destination, mask, cursorX, cursorY, request.PixelHeight, options.Style);
records.Add(new AdvRetainedGlyphRecord(0, cursorX, cursorY, right, bottom));
presentationRects.Add(PresentationRect(
destination.Height,
mask,
cursorX,
cursorY,
right,
bottom,
request.PixelHeight,
options.Style));
cursorX = checked(cursorX + mask.CellAdvanceX);
cursorY = checked(cursorY + mask.CellAdvanceY);
}
return new GlyphTextLayoutResult(
records, cursorX, cursorY, records.Count, wrappedLines, observed, stopped);
records, presentationRects, cursorX, cursorY, records.Count,
wrappedLines, observed, stopped);
}
private static AdvRetainedGlyphPresentationRect PresentationRect(
int surfaceHeight,
GlyphMask mask,
int cellLeft,
int cellTop,
int cellRight,
int cellBottom,
int topToBaseline,
AdvTextStyle style)
{
int minimumEffectY = 0;
int maximumEffectY = 0;
if (style.RenderMode == 1)
{
minimumEffectY = Math.Min(0, style.EffectOffsetY);
maximumEffectY = Math.Max(0, style.EffectOffsetY);
}
else if (style.RenderMode == 3)
{
foreach ((int _, int y) in AgeGlyphMaskCompositor.GetMode3OutlineOffsets(
style.EffectOffsetX, style.EffectOffsetY))
{
minimumEffectY = Math.Min(minimumEffectY, y);
maximumEffectY = Math.Max(maximumEffectY, y);
}
}
int inkTop = checked(
cellTop + topToBaseline - mask.OriginY + minimumEffectY);
int inkBottom = checked(
cellTop + topToBaseline - mask.OriginY + mask.Height + maximumEffectY);
return new AdvRetainedGlyphPresentationRect(
cellLeft,
Math.Clamp(Math.Min(cellTop, inkTop), 0, surfaceHeight),
cellRight,
Math.Clamp(Math.Max(cellBottom, inkBottom), 0, surfaceHeight));
}
}