mirror of
https://github.com/penpot/penpot.git
synced 2026-09-18 18:06:14 +00:00
🐛 Preserve tile coverage when packing atlas slots (#11749)
Packed atlas compose inset Linear samples after blitting the full drawable into the slot, which dropped edge texels and opened multi-pixel gaps on text that crossed tile seams (#11696), especially under HiDPI packing. Write the drawable into the inset content rect and clamp-pad the 1px frame so Linear compose keeps coverage without bleeding into the next cell. Closes #11696
This commit is contained in:
parent
fcae641c7b
commit
20547f658e
@ -52,6 +52,32 @@ fn draw_surface_src_rect_to_dst(
|
||||
to_canvas.restore();
|
||||
}
|
||||
|
||||
/// Copy a rendered tile into an atlas slot. Packed slots keep a 1px border so
|
||||
/// Linear filtering on compose does not sample the neighboring cell.
|
||||
fn blit_drawable_into_tile_atlas_slot(
|
||||
current: &mut skia::Surface,
|
||||
atlas_canvas: &skia::Canvas,
|
||||
src: skia::Rect,
|
||||
slot: skia::Rect,
|
||||
slot_size: i32,
|
||||
) {
|
||||
let nearest = skia::SamplingOptions::new(skia::FilterMode::Nearest, skia::MipmapMode::None);
|
||||
let content = tiles::tile_atlas_content_rect(slot, slot_size);
|
||||
if content == slot {
|
||||
draw_surface_src_rect_to_dst(current, atlas_canvas, src, slot, nearest);
|
||||
return;
|
||||
}
|
||||
|
||||
let linear = skia::SamplingOptions::new(skia::FilterMode::Linear, skia::MipmapMode::None);
|
||||
draw_surface_src_rect_to_dst(current, atlas_canvas, src, content, linear);
|
||||
|
||||
atlas_canvas.save();
|
||||
atlas_canvas.clip_rect(slot, None, false);
|
||||
atlas_canvas.clip_rect(content, Some(skia::ClipOp::Difference), false);
|
||||
draw_surface_src_rect_to_dst(current, atlas_canvas, src, slot, nearest);
|
||||
atlas_canvas.restore();
|
||||
}
|
||||
|
||||
pub fn get_cache_size(viewbox: &Viewbox, interest: i32) -> skia::ISize {
|
||||
// First we retrieve the extended area of the viewport that we could render.
|
||||
let TileRect(isx, isy, iex, iey) =
|
||||
@ -1287,9 +1313,14 @@ impl Surfaces {
|
||||
.tiles
|
||||
.add(tile_viewbox, tile, scale, view_doc, &mut tile_doc_rects);
|
||||
self.atlas.tile_doc_rects = tile_doc_rects;
|
||||
let dst = tile_ref.rect;
|
||||
let mut current = self.current.clone();
|
||||
draw_surface_src_rect_to_dst(&mut current, self.tile_atlas.canvas(), src, dst, sampling);
|
||||
blit_drawable_into_tile_atlas_slot(
|
||||
&mut current,
|
||||
self.tile_atlas.canvas(),
|
||||
src,
|
||||
tile_ref.rect,
|
||||
self.tiles.slot_size(),
|
||||
);
|
||||
|
||||
if !skip_cache_surface {
|
||||
// Optional legacy Cache surface fill (debug). Pan/zoom preview
|
||||
@ -1709,20 +1740,6 @@ impl TileTextureCache {
|
||||
tiles::tile_atlas_compose_scale(self.slot_size)
|
||||
}
|
||||
|
||||
fn compose_src_rect(&self, rect: Rect) -> Rect {
|
||||
if self.slot_size < TILE_SIZE {
|
||||
let inset = tiles::TILE_ATLAS_SAMPLE_INSET;
|
||||
Rect::new(
|
||||
rect.left + inset,
|
||||
rect.top + inset,
|
||||
rect.right - inset,
|
||||
rect.bottom - inset,
|
||||
)
|
||||
} else {
|
||||
rect
|
||||
}
|
||||
}
|
||||
|
||||
pub fn repack(&mut self, texture_width: i32, texture_height: i32, slot_size: i32) {
|
||||
let capacity = ((texture_width / slot_size) * (texture_height / slot_size)) as usize;
|
||||
self.slot_size = slot_size;
|
||||
@ -1844,7 +1861,7 @@ impl TileTextureCache {
|
||||
),
|
||||
);
|
||||
|
||||
let src = self.compose_src_rect(tile_ref.rect);
|
||||
let src = tiles::tile_atlas_content_rect(tile_ref.rect, self.slot_size);
|
||||
self.textures[index].set_ltrb(src.left, src.top, src.right, src.bottom);
|
||||
|
||||
index += 1;
|
||||
@ -1884,7 +1901,7 @@ impl TileTextureCache {
|
||||
continue;
|
||||
}
|
||||
|
||||
let src = self.compose_src_rect(tile_ref.rect);
|
||||
let src = tiles::tile_atlas_content_rect(tile_ref.rect, self.slot_size);
|
||||
let scos = doc_rect.width() * s / src.width();
|
||||
let tx = ((doc_rect.left + viewbox.pan.x) * s).round();
|
||||
let ty = ((doc_rect.top + viewbox.pan.y) * s).round();
|
||||
@ -1914,7 +1931,7 @@ impl TileTextureCache {
|
||||
continue;
|
||||
}
|
||||
|
||||
let src = self.compose_src_rect(tile_ref.rect);
|
||||
let src = tiles::tile_atlas_content_rect(tile_ref.rect, self.slot_size);
|
||||
let tx = ((doc_rect.left + viewbox.pan.x) * s).round();
|
||||
let ty = ((doc_rect.top + viewbox.pan.y) * s).round();
|
||||
let scos = doc_rect.width() * s / src.width();
|
||||
|
||||
@ -269,20 +269,30 @@ pub fn tile_atlas_slot_size(needed_slots: usize, atlas_px: i32) -> i32 {
|
||||
(atlas_px / side).clamp(MIN_SLOT, TILE_SIZE as i32)
|
||||
}
|
||||
|
||||
/// Inset (texels) applied when sampling a packed atlas slot with Linear
|
||||
/// filtering, so upsample kernels do not bleed into the neighboring cell.
|
||||
/// 1px pad around packed-slot content so Linear compose does not bleed into
|
||||
/// the next cell. The drawable is written into the inner rect (full spatial
|
||||
/// coverage); sampling that inner rect avoids the edge-drop gaps of #11696.
|
||||
pub const TILE_ATLAS_SAMPLE_INSET: f32 = 1.0;
|
||||
|
||||
/// Source size inside a packed slot after the Linear-filter inset.
|
||||
pub fn tile_atlas_compose_src_size(slot_size: i32) -> f32 {
|
||||
/// Rect inside `slot` that holds the tile drawable (inset when packed).
|
||||
pub fn tile_atlas_content_rect(slot: skia::Rect, slot_size: i32) -> skia::Rect {
|
||||
if slot_size < TILE_SIZE as i32 {
|
||||
(slot_size as f32 - 2.0 * TILE_ATLAS_SAMPLE_INSET).max(1.0)
|
||||
let i = TILE_ATLAS_SAMPLE_INSET;
|
||||
skia::Rect::new(slot.left + i, slot.top + i, slot.right - i, slot.bottom - i)
|
||||
} else {
|
||||
slot_size as f32
|
||||
slot
|
||||
}
|
||||
}
|
||||
|
||||
/// `draw_atlas` scale so the destination sprite stays `TILE_SIZE` after inset.
|
||||
pub fn tile_atlas_compose_src_size(slot_size: i32) -> f32 {
|
||||
tile_atlas_content_rect(
|
||||
skia::Rect::from_wh(slot_size as f32, slot_size as f32),
|
||||
slot_size,
|
||||
)
|
||||
.width()
|
||||
.max(1.0)
|
||||
}
|
||||
|
||||
pub fn tile_atlas_compose_scale(slot_size: i32) -> f32 {
|
||||
TILE_SIZE / tile_atlas_compose_src_size(slot_size)
|
||||
}
|
||||
@ -519,6 +529,15 @@ mod tests {
|
||||
assert!(src < slot as f32);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn atlas_content_rect_matches_compose_src_when_packed() {
|
||||
let slot = skia::Rect::from_xywh(10.0, 20.0, 315.0, 315.0);
|
||||
let inner = tile_atlas_content_rect(slot, 315);
|
||||
assert_eq!(inner, skia::Rect::new(11.0, 21.0, 324.0, 334.0));
|
||||
assert_eq!(tile_atlas_content_rect(slot, 512), slot);
|
||||
assert!((inner.width() - tile_atlas_compose_src_size(315)).abs() < 1e-4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn edit_dirty_rect_includes_pre_rotate_extent_outside_current_index() {
|
||||
// Indexed tiles are interest-clipped; old AABB still covers wings.
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user