Skip to content

Commit b62714a

Browse files
zachelnetDeepSeek V4
andcommitted
fix(ui): center sprite overlay rotation, tighten tests
- BlockSprite now rotates around center (transformOrigin: center) matching the renderer's overlay_sprite_with_rotation centering. Fixes visual offset between UI overlay and rendered output for rotated text blocks. - 90°/180° overlay tests now assert non-transparent pixels exist. - 90° expand test uses exact assert_eq (fast path, no resampling). Co-authored-by: DeepSeek V4 <deepseek@v4.ai>
1 parent a66f011 commit b62714a

2 files changed

Lines changed: 17 additions & 12 deletions

File tree

crates/koharu-app/src/renderer.rs

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1597,11 +1597,10 @@ mod tests {
15971597
#[test]
15981598
fn overlay_sprite_90deg_rotates() {
15991599
let mut canvas = RgbaImage::new(600, 600);
1600-
// A large sprite in the center should stay visible after any rotation.
16011600
let sprite = make_test_sprite(100, 100);
16021601
let transform = make_transform(250.0, 250.0, 90.0);
1603-
// The function must not panic.
16041602
overlay_sprite_with_rotation(&mut canvas, &sprite, &transform, false);
1603+
assert!(canvas.pixels().any(|p| p.0[3] != 0));
16051604
}
16061605

16071606
#[test]
@@ -1610,7 +1609,7 @@ mod tests {
16101609
let sprite = make_test_sprite(100, 100);
16111610
let transform = make_transform(250.0, 250.0, 180.0);
16121611
overlay_sprite_with_rotation(&mut canvas, &sprite, &transform, false);
1613-
// Must not panic.
1612+
assert!(canvas.pixels().any(|p| p.0[3] != 0));
16141613
}
16151614

16161615
#[test]
@@ -1630,15 +1629,17 @@ mod tests {
16301629
let src = make_test_sprite(40, 20);
16311630
let (rotated, _min_x, _min_y) =
16321631
rotate_sprite_expand_top_left(&src, std::f32::consts::FRAC_PI_2);
1633-
// A 90° rotation swaps width and height, plus bilinear rounding = roughly 20×40.
1634-
assert!(
1635-
rotated.width() >= 20 && rotated.width() <= 22,
1636-
"expected ~20-22 wide, got {}",
1632+
// 90° rotation swaps width and height exactly (fast path, no resampling).
1633+
assert_eq!(
1634+
rotated.width(),
1635+
20,
1636+
"expected 20 wide, got {}",
16371637
rotated.width()
16381638
);
1639-
assert!(
1640-
rotated.height() >= 39 && rotated.height() <= 41,
1641-
"expected ~39-41 tall, got {}",
1639+
assert_eq!(
1640+
rotated.height(),
1641+
40,
1642+
"expected 40 tall, got {}",
16421643
rotated.height()
16431644
);
16441645
}

ui/components/canvas/TextBlockLayer.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,10 @@ function BlockSprite({ node, scale }: { node: TextNodeEntry; scale: number }) {
314314
const x = (spriteT?.x ?? node.transform.x) * scale
315315
const y = (spriteT?.y ?? node.transform.y) * scale
316316
const rotation = spriteT?.rotationDeg ?? node.transform.rotationDeg ?? 0
317+
// Renderer centers the rotated sprite; match that model by rotating
318+
// around the sprite's center rather than top-left.
319+
const cx = x + node.transform.width * scale * 0.5
320+
const cy = y + node.transform.height * scale * 0.5
317321
return (
318322
<img
319323
alt=''
@@ -323,8 +327,8 @@ function BlockSprite({ node, scale }: { node: TextNodeEntry; scale: number }) {
323327
style={{
324328
top: 0,
325329
left: 0,
326-
transformOrigin: 'top left',
327-
transform: `translate(${x}px, ${y}px) rotate(${rotation}deg) scale(${scale})`,
330+
transformOrigin: 'center center',
331+
transform: `translate(${cx}px, ${cy}px) rotate(${rotation}deg) scale(${scale})`,
328332
}}
329333
/>
330334
)

0 commit comments

Comments
 (0)