Improve nine-slice atlas sampling

This commit is contained in:
gem
2026-06-09 14:49:17 +08:00
parent 4ce5fe1ae7
commit 220bb0aba1
3 changed files with 65 additions and 6 deletions

View File

@@ -3,7 +3,7 @@
## Unreleased
- Added TexturePacker frame, manual source-region, and nine-slice image rendering fields for image-capable nodes.
- Fixed nine-slice image seams by overlapping destination slices during runtime rendering.
- Fixed nine-slice image seams by overlapping destination slices and using inset source sampling during runtime rendering.
- Fixed Runtime alpha inheritance so parent fade commands apply to the full child subtree.
- Added Runtime text shadow fields for text-capable nodes.
- Fixed Runtime node color alpha composition so `#AARRGGBB` alpha now multiplies with node/runtime alpha instead of being overwritten.

View File

@@ -44,6 +44,7 @@ List<({Rect source, Rect destination})> runtimeNineSliceRects({
double sliceRight = 0,
double sliceBottom = 0,
double destinationOverlap = 0,
double sourceInset = 0,
}) {
if (source.width <= 0 ||
source.height <= 0 ||
@@ -88,7 +89,7 @@ List<({Rect source, Rect destination})> runtimeNineSliceRects({
final parts = <({Rect source, Rect destination})>[];
for (var y = 0; y < 3; y++) {
for (var x = 0; x < 3; x++) {
final sourcePart = Rect.fromLTRB(
final rawSourcePart = Rect.fromLTRB(
sourceXs[x],
sourceYs[y],
sourceXs[x + 1],
@@ -100,12 +101,17 @@ List<({Rect source, Rect destination})> runtimeNineSliceRects({
destXs[x + 1],
destYs[y + 1],
);
if (sourcePart.width <= 0 ||
sourcePart.height <= 0 ||
if (rawSourcePart.width <= 0 ||
rawSourcePart.height <= 0 ||
rawDestPart.width <= 0 ||
rawDestPart.height <= 0) {
continue;
}
final sourcePart = _insetNineSliceSourceRect(
rawSourcePart,
bounds: source,
inset: sourceInset,
);
final destPart = _overlapNineSliceDestinationRect(
rawDestPart,
x: x,
@@ -119,6 +125,22 @@ List<({Rect source, Rect destination})> runtimeNineSliceRects({
return parts;
}
Rect _insetNineSliceSourceRect(
Rect rect, {
required Rect bounds,
required double inset,
}) {
if (inset <= 0 || rect.width <= inset * 2 || rect.height <= inset * 2) {
return rect;
}
return Rect.fromLTRB(
math.min(rect.right, math.max(bounds.left, rect.left + inset)),
math.min(rect.bottom, math.max(bounds.top, rect.top + inset)),
math.max(rect.left, math.min(bounds.right, rect.right - inset)),
math.max(rect.top, math.min(bounds.bottom, rect.bottom - inset)),
);
}
Rect _overlapNineSliceDestinationRect(
Rect rect, {
required int x,
@@ -575,7 +597,13 @@ class RuntimeComponent extends PositionComponent
..color = composeRuntimeColorAlpha(Colors.white, renderAlpha);
final source = _imageSourceRect(image, _currentImageFrame(_node));
if (_usesNineSlice(source, rect)) {
_drawNineSliceImage(canvas, image, source, rect, imagePaint);
_drawNineSliceImage(
canvas,
image,
source,
rect,
imagePaint..filterQuality = FilterQuality.none,
);
} else {
canvas.drawImageRect(image, source, rect, imagePaint);
}
@@ -630,7 +658,8 @@ class RuntimeComponent extends PositionComponent
final parts = runtimeNineSliceRects(
source: source,
destination: destination,
destinationOverlap: 0.5,
destinationOverlap: 1,
sourceInset: 0.5,
sliceLeft: _node.sliceLeft ?? 0,
sliceTop: _node.sliceTop ?? 0,
sliceRight: _node.sliceRight ?? 0,

View File

@@ -284,6 +284,36 @@ void main() {
expect(parts[4].source, const Rect.fromLTRB(10, 10, 20, 20));
});
test('insets nine-slice source rects to avoid atlas edge sampling', () {
final parts = runtimeNineSliceRects(
source: const Rect.fromLTWH(10, 20, 30, 30),
destination: const Rect.fromLTWH(0, 0, 90, 90),
sliceLeft: 10,
sliceTop: 10,
sliceRight: 10,
sliceBottom: 10,
sourceInset: 0.5,
);
expect(parts.first.source, const Rect.fromLTRB(10.5, 20.5, 19.5, 29.5));
expect(parts[4].source, const Rect.fromLTRB(20.5, 30.5, 29.5, 39.5));
expect(parts.last.source, const Rect.fromLTRB(30.5, 40.5, 39.5, 49.5));
});
test('keeps tiny nine-slice source rects when inset would collapse them', () {
final parts = runtimeNineSliceRects(
source: const Rect.fromLTWH(0, 0, 3, 3),
destination: const Rect.fromLTWH(0, 0, 30, 30),
sliceLeft: 1,
sliceTop: 1,
sliceRight: 1,
sliceBottom: 1,
sourceInset: 0.5,
);
expect(parts[4].source, const Rect.fromLTRB(1, 1, 2, 2));
});
test('updates text alpha style without rebuilding text component', () {
final component = RuntimeComponent(
node: const RuntimeNode(