Skip to content

Commit 6df0e9b

Browse files
authored
bug: Fix 9-slice textures with asymmetric borders. (#13921)
# Objective Fix a 9-slice asymmetric border issue that [QueenOfSquiggles](https://blobfox.coffee/@queenofsquiggles/112639035165575222) found. Here's the behavior before: <img width="340" alt="the-bug" src="https://github.com/bevyengine/bevy/assets/54390/81ff1847-b2ea-4578-9fd0-af6ee96c5438"> ## Solution Here's the behavior with the fix. <img width="327" alt="the-fix" src="https://github.com/bevyengine/bevy/assets/54390/33a4e3f0-b6a8-448e-9654-1197218ea11d"> ## Testing I used QueenOfSquiggles [repo](https://github.com/QueenOfSquiggles/my-bevy-learning-project) to exercise the code. I manually went through a number of variations of the border and caught a few other issues after the first pass. I added some code to create random borders and though they often looked funny there weren't any gaps like before. ### Unit Tests I did add some tests to `slicer.rs` mostly as an exploratory programming exercise. So they currently act as a limited, incomplete, "golden-file"-ish approach. Perhaps they're not worth keeping. In order to write the tests, I did add a `PartialEq` derive for `TextureSlice`. I only tested these changes on macOS. --- ## Changelog Make 9-slice textures work with asymmetric borders.
1 parent eddb006 commit 6df0e9b

File tree

2 files changed

+180
-16
lines changed

2 files changed

+180
-16
lines changed

crates/bevy_sprite/src/texture_slice/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ pub(crate) use computed_slices::{
1111
};
1212

1313
/// Single texture slice, representing a texture rect to draw in a given area
14-
#[derive(Debug, Clone)]
14+
#[derive(Debug, Clone, PartialEq)]
1515
pub struct TextureSlice {
1616
/// texture area to draw
1717
pub texture_rect: Rect,

crates/bevy_sprite/src/texture_slice/slicer.rs

Lines changed: 179 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ pub enum SliceScaleMode {
4444
}
4545

4646
impl TextureSlicer {
47-
/// Computes the 4 corner slices
47+
/// Computes the 4 corner slices: top left, top right, bottom left, bottom right.
4848
#[must_use]
4949
fn corner_slices(&self, base_rect: Rect, render_size: Vec2) -> [TextureSlice; 4] {
5050
let coef = render_size / base_rect.size();
@@ -116,7 +116,7 @@ impl TextureSlicer {
116116
render_size: Vec2,
117117
) -> [TextureSlice; 2] {
118118
[
119-
// left
119+
// Left
120120
TextureSlice {
121121
texture_rect: Rect {
122122
min: base_rect.min + vec2(0.0, self.border.top),
@@ -127,24 +127,30 @@ impl TextureSlicer {
127127
},
128128
draw_size: vec2(
129129
bl_corner.draw_size.x,
130-
render_size.y - bl_corner.draw_size.y - tl_corner.draw_size.y,
130+
render_size.y - (bl_corner.draw_size.y + tl_corner.draw_size.y),
131131
),
132-
offset: vec2(-render_size.x + bl_corner.draw_size.x, 0.0) / 2.0,
132+
offset: vec2(
133+
-render_size.x + bl_corner.draw_size.x,
134+
bl_corner.draw_size.y - tl_corner.draw_size.y,
135+
) / 2.0,
133136
},
134-
// right
137+
// Right
135138
TextureSlice {
136139
texture_rect: Rect {
137140
min: vec2(
138141
base_rect.max.x - self.border.right,
139-
base_rect.min.y + self.border.bottom,
142+
base_rect.min.y + self.border.top,
140143
),
141-
max: vec2(base_rect.max.x, base_rect.max.y - self.border.top),
144+
max: vec2(base_rect.max.x, base_rect.max.y - self.border.bottom),
142145
},
143146
draw_size: vec2(
144147
br_corner.draw_size.x,
145148
render_size.y - (br_corner.draw_size.y + tr_corner.draw_size.y),
146149
),
147-
offset: vec2(render_size.x - br_corner.draw_size.x, 0.0) / 2.0,
150+
offset: vec2(
151+
render_size.x - br_corner.draw_size.x,
152+
br_corner.draw_size.y - tr_corner.draw_size.y,
153+
) / 2.0,
148154
},
149155
]
150156
}
@@ -171,7 +177,10 @@ impl TextureSlicer {
171177
render_size.x - (bl_corner.draw_size.x + br_corner.draw_size.x),
172178
bl_corner.draw_size.y,
173179
),
174-
offset: vec2(0.0, bl_corner.offset.y),
180+
offset: vec2(
181+
(bl_corner.draw_size.x - br_corner.draw_size.x) / 2.0,
182+
bl_corner.offset.y,
183+
),
175184
},
176185
// Top
177186
TextureSlice {
@@ -186,7 +195,10 @@ impl TextureSlicer {
186195
render_size.x - (tl_corner.draw_size.x + tr_corner.draw_size.x),
187196
tl_corner.draw_size.y,
188197
),
189-
offset: vec2(0.0, tl_corner.offset.y),
198+
offset: vec2(
199+
(tl_corner.draw_size.x - tr_corner.draw_size.x) / 2.0,
200+
tl_corner.offset.y,
201+
),
190202
},
191203
]
192204
}
@@ -220,22 +232,29 @@ impl TextureSlicer {
220232
}];
221233
}
222234
let mut slices = Vec::with_capacity(9);
223-
// Corners
235+
// Corners are in this order: [TL, TR, BL, BR]
224236
let corners = self.corner_slices(rect, render_size);
225-
// Sides
237+
// Vertical Sides: [B, T]
226238
let vertical_sides = self.vertical_side_slices(&corners, rect, render_size);
239+
// Horizontal Sides: [L, R]
227240
let horizontal_sides = self.horizontal_side_slices(&corners, rect, render_size);
228241
// Center
229242
let center = TextureSlice {
230243
texture_rect: Rect {
231-
min: rect.min + vec2(self.border.left, self.border.bottom),
232-
max: vec2(rect.max.x - self.border.right, rect.max.y - self.border.top),
244+
min: rect.min + vec2(self.border.left, self.border.top),
245+
max: vec2(
246+
rect.max.x - self.border.right,
247+
rect.max.y - self.border.bottom,
248+
),
233249
},
234250
draw_size: vec2(
235251
render_size.x - (corners[2].draw_size.x + corners[3].draw_size.x),
236252
render_size.y - (corners[2].draw_size.y + corners[0].draw_size.y),
237253
),
238-
offset: Vec2::ZERO,
254+
offset: Vec2::new(
255+
(corners[0].draw_size.x - corners[3].draw_size.x) / 2.0,
256+
(corners[2].draw_size.y - corners[0].draw_size.y) / 2.0,
257+
),
239258
};
240259

241260
slices.extend(corners);
@@ -279,3 +298,148 @@ impl Default for TextureSlicer {
279298
}
280299
}
281300
}
301+
302+
#[cfg(test)]
303+
mod test {
304+
use super::*;
305+
#[test]
306+
fn test_horizontal_sizes_uniform() {
307+
let slicer = TextureSlicer {
308+
border: BorderRect {
309+
left: 10.,
310+
right: 10.,
311+
top: 10.,
312+
bottom: 10.,
313+
},
314+
center_scale_mode: SliceScaleMode::Stretch,
315+
sides_scale_mode: SliceScaleMode::Stretch,
316+
max_corner_scale: 1.0,
317+
};
318+
let base_rect = Rect {
319+
min: Vec2::ZERO,
320+
max: Vec2::splat(50.),
321+
};
322+
let render_rect = Vec2::splat(100.);
323+
let slices = slicer.corner_slices(base_rect, render_rect);
324+
assert_eq!(
325+
slices[0],
326+
TextureSlice {
327+
texture_rect: Rect {
328+
min: Vec2::ZERO,
329+
max: Vec2::splat(10.0)
330+
},
331+
draw_size: Vec2::new(10.0, 10.0),
332+
offset: Vec2::new(-45.0, 45.0),
333+
}
334+
);
335+
}
336+
337+
#[test]
338+
fn test_horizontal_sizes_non_uniform_bigger() {
339+
let slicer = TextureSlicer {
340+
border: BorderRect {
341+
left: 20.,
342+
right: 10.,
343+
top: 10.,
344+
bottom: 10.,
345+
},
346+
center_scale_mode: SliceScaleMode::Stretch,
347+
sides_scale_mode: SliceScaleMode::Stretch,
348+
max_corner_scale: 1.0,
349+
};
350+
let base_rect = Rect {
351+
min: Vec2::ZERO,
352+
max: Vec2::splat(50.),
353+
};
354+
let render_rect = Vec2::splat(100.);
355+
let slices = slicer.corner_slices(base_rect, render_rect);
356+
assert_eq!(
357+
slices[0],
358+
TextureSlice {
359+
texture_rect: Rect {
360+
min: Vec2::ZERO,
361+
max: Vec2::new(20.0, 10.0)
362+
},
363+
draw_size: Vec2::new(20.0, 10.0),
364+
offset: Vec2::new(-40.0, 45.0),
365+
}
366+
);
367+
}
368+
369+
#[test]
370+
fn test_horizontal_sizes_non_uniform_smaller() {
371+
let slicer = TextureSlicer {
372+
border: BorderRect {
373+
left: 5.,
374+
right: 10.,
375+
top: 10.,
376+
bottom: 10.,
377+
},
378+
center_scale_mode: SliceScaleMode::Stretch,
379+
sides_scale_mode: SliceScaleMode::Stretch,
380+
max_corner_scale: 1.0,
381+
};
382+
let rect = Rect {
383+
min: Vec2::ZERO,
384+
max: Vec2::splat(50.),
385+
};
386+
let render_size = Vec2::splat(100.);
387+
let corners = slicer.corner_slices(rect, render_size);
388+
389+
let vertical_sides = slicer.vertical_side_slices(&corners, rect, render_size);
390+
assert_eq!(
391+
corners[0],
392+
TextureSlice {
393+
texture_rect: Rect {
394+
min: Vec2::ZERO,
395+
max: Vec2::new(5.0, 10.0)
396+
},
397+
draw_size: Vec2::new(5.0, 10.0),
398+
offset: Vec2::new(-47.5, 45.0),
399+
}
400+
);
401+
assert_eq!(
402+
vertical_sides[1], /* top */
403+
TextureSlice {
404+
texture_rect: Rect {
405+
min: Vec2::new(5.0, 0.0),
406+
max: Vec2::new(40.0, 10.0)
407+
},
408+
draw_size: Vec2::new(85.0, 10.0),
409+
offset: Vec2::new(-2.5, 45.0),
410+
}
411+
);
412+
}
413+
414+
#[test]
415+
fn test_horizontal_sizes_non_uniform_zero() {
416+
let slicer = TextureSlicer {
417+
border: BorderRect {
418+
left: 0.,
419+
right: 10.,
420+
top: 10.,
421+
bottom: 10.,
422+
},
423+
center_scale_mode: SliceScaleMode::Stretch,
424+
sides_scale_mode: SliceScaleMode::Stretch,
425+
max_corner_scale: 1.0,
426+
};
427+
let base_rect = Rect {
428+
min: Vec2::ZERO,
429+
max: Vec2::splat(50.),
430+
};
431+
let render_rect = Vec2::splat(100.);
432+
let slices = slicer.corner_slices(base_rect, render_rect);
433+
assert_eq!(
434+
slices[0],
435+
TextureSlice {
436+
texture_rect: Rect {
437+
min: Vec2::ZERO,
438+
max: Vec2::new(0.0, 10.0)
439+
},
440+
draw_size: Vec2::new(0.0, 10.0),
441+
offset: Vec2::new(-50.0, 45.0),
442+
}
443+
);
444+
}
445+
}

0 commit comments

Comments
 (0)