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();
|
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 {
|
pub fn get_cache_size(viewbox: &Viewbox, interest: i32) -> skia::ISize {
|
||||||
// First we retrieve the extended area of the viewport that we could render.
|
// First we retrieve the extended area of the viewport that we could render.
|
||||||
let TileRect(isx, isy, iex, iey) =
|
let TileRect(isx, isy, iex, iey) =
|
||||||
@ -1287,9 +1313,14 @@ impl Surfaces {
|
|||||||
.tiles
|
.tiles
|
||||||
.add(tile_viewbox, tile, scale, view_doc, &mut tile_doc_rects);
|
.add(tile_viewbox, tile, scale, view_doc, &mut tile_doc_rects);
|
||||||
self.atlas.tile_doc_rects = tile_doc_rects;
|
self.atlas.tile_doc_rects = tile_doc_rects;
|
||||||
let dst = tile_ref.rect;
|
|
||||||
let mut current = self.current.clone();
|
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 {
|
if !skip_cache_surface {
|
||||||
// Optional legacy Cache surface fill (debug). Pan/zoom preview
|
// Optional legacy Cache surface fill (debug). Pan/zoom preview
|
||||||
@ -1709,20 +1740,6 @@ impl TileTextureCache {
|
|||||||
tiles::tile_atlas_compose_scale(self.slot_size)
|
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) {
|
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;
|
let capacity = ((texture_width / slot_size) * (texture_height / slot_size)) as usize;
|
||||||
self.slot_size = slot_size;
|
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);
|
self.textures[index].set_ltrb(src.left, src.top, src.right, src.bottom);
|
||||||
|
|
||||||
index += 1;
|
index += 1;
|
||||||
@ -1884,7 +1901,7 @@ impl TileTextureCache {
|
|||||||
continue;
|
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 scos = doc_rect.width() * s / src.width();
|
||||||
let tx = ((doc_rect.left + viewbox.pan.x) * s).round();
|
let tx = ((doc_rect.left + viewbox.pan.x) * s).round();
|
||||||
let ty = ((doc_rect.top + viewbox.pan.y) * s).round();
|
let ty = ((doc_rect.top + viewbox.pan.y) * s).round();
|
||||||
@ -1914,7 +1931,7 @@ impl TileTextureCache {
|
|||||||
continue;
|
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 tx = ((doc_rect.left + viewbox.pan.x) * s).round();
|
||||||
let ty = ((doc_rect.top + viewbox.pan.y) * s).round();
|
let ty = ((doc_rect.top + viewbox.pan.y) * s).round();
|
||||||
let scos = doc_rect.width() * s / src.width();
|
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)
|
(atlas_px / side).clamp(MIN_SLOT, TILE_SIZE as i32)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Inset (texels) applied when sampling a packed atlas slot with Linear
|
/// 1px pad around packed-slot content so Linear compose does not bleed into
|
||||||
/// filtering, so upsample kernels do not bleed into the neighboring cell.
|
/// 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;
|
pub const TILE_ATLAS_SAMPLE_INSET: f32 = 1.0;
|
||||||
|
|
||||||
/// Source size inside a packed slot after the Linear-filter inset.
|
/// Rect inside `slot` that holds the tile drawable (inset when packed).
|
||||||
pub fn tile_atlas_compose_src_size(slot_size: i32) -> f32 {
|
pub fn tile_atlas_content_rect(slot: skia::Rect, slot_size: i32) -> skia::Rect {
|
||||||
if slot_size < TILE_SIZE as i32 {
|
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 {
|
} 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 {
|
pub fn tile_atlas_compose_scale(slot_size: i32) -> f32 {
|
||||||
TILE_SIZE / tile_atlas_compose_src_size(slot_size)
|
TILE_SIZE / tile_atlas_compose_src_size(slot_size)
|
||||||
}
|
}
|
||||||
@ -519,6 +529,15 @@ mod tests {
|
|||||||
assert!(src < slot as f32);
|
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]
|
#[test]
|
||||||
fn edit_dirty_rect_includes_pre_rotate_extent_outside_current_index() {
|
fn edit_dirty_rect_includes_pre_rotate_extent_outside_current_index() {
|
||||||
// Indexed tiles are interest-clipped; old AABB still covers wings.
|
// Indexed tiles are interest-clipped; old AABB still covers wings.
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user