mirror of
https://github.com/penpot/penpot.git
synced 2026-07-31 02:16:13 +00:00
🚧 Reserve atlas-sample scaffolding for layout-drag fast path
This commit is contained in:
parent
a5c4b83611
commit
c168f39d5c
@ -1049,7 +1049,7 @@ pub extern "C" fn set_modifiers() -> Result<()> {
|
||||
let ts = performance::get_time();
|
||||
state
|
||||
.render_state
|
||||
.drag_sprite_try_capture(&ids, &state.shapes, ts)?;
|
||||
.drag_sprite_try_capture(&ids, &modifiers, &state.shapes, ts)?;
|
||||
}
|
||||
state.set_modifiers(modifiers);
|
||||
// Throttle: skip per-pointer-move tile invalidation. The render
|
||||
|
||||
@ -17,7 +17,7 @@
|
||||
//! render rebuild the affected tiles with the shape at its committed
|
||||
//! position.
|
||||
|
||||
use std::collections::HashSet;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
use skia_safe::{self as skia, Rect};
|
||||
|
||||
@ -71,6 +71,80 @@ pub struct DragSprite {
|
||||
pub above_image: Option<skia::Image>,
|
||||
}
|
||||
|
||||
/// Find the unique "primary" shape in the modifier set: the one whose
|
||||
/// ancestors are NOT also in the set. For a layout-container drag
|
||||
/// `propagate-modifiers` expands the user's single-shape modifier into
|
||||
/// transforms-per-descendant; this picks the topmost one back out so
|
||||
/// the sprite path can capture the parent (whose subtree contains all
|
||||
/// the propagated children).
|
||||
///
|
||||
/// Returns `None` when the set has no unique root (multi-shape drag of
|
||||
/// unrelated shapes), in which case the fast path is disabled.
|
||||
///
|
||||
/// Currently dormant — pairs with the layout-drag atlas-sample
|
||||
/// shortcut that hasn't landed cleanly yet.
|
||||
#[allow(dead_code)]
|
||||
fn find_primary_shape(ids: &[Uuid], tree: ShapesPoolRef) -> Option<Uuid> {
|
||||
let id_set: HashSet<Uuid> = ids.iter().copied().collect();
|
||||
let mut roots: Vec<Uuid> = Vec::new();
|
||||
for &id in ids {
|
||||
// Walk ancestors; if any is in the set, `id` isn't a root.
|
||||
let mut cur = tree.get(&id).and_then(|s| s.parent_id);
|
||||
let mut has_ancestor_in_set = false;
|
||||
while let Some(pid) = cur {
|
||||
if id_set.contains(&pid) {
|
||||
has_ancestor_in_set = true;
|
||||
break;
|
||||
}
|
||||
cur = tree.get(&pid).and_then(|s| s.parent_id);
|
||||
}
|
||||
if !has_ancestor_in_set {
|
||||
roots.push(id);
|
||||
if roots.len() > 1 {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
if roots.len() == 1 {
|
||||
Some(roots[0])
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// True iff every matrix in `modifiers` is a pure translation AND they
|
||||
/// all share the same `(tx, ty)` (within `eps`). This is the safety
|
||||
/// invariant for the layout-container drag path: a layout that's just
|
||||
/// being dragged emits the same translation for the parent and every
|
||||
/// auto-laid-out descendant. If any descendant differs, the layout has
|
||||
/// reflowed (e.g. a flex item resized) and the captured sprite would
|
||||
/// no longer represent the live state.
|
||||
///
|
||||
/// Currently dormant — pairs with `find_primary_shape`.
|
||||
#[allow(dead_code)]
|
||||
fn all_modifiers_uniform_translation<'a, I>(modifiers: I) -> bool
|
||||
where
|
||||
I: IntoIterator<Item = &'a skia::Matrix>,
|
||||
{
|
||||
let eps = 1e-3_f32;
|
||||
let mut first: Option<(f32, f32)> = None;
|
||||
for m in modifiers {
|
||||
if !is_translation_only(m) {
|
||||
return false;
|
||||
}
|
||||
let (tx, ty) = (m.translate_x(), m.translate_y());
|
||||
match first {
|
||||
None => first = Some((tx, ty)),
|
||||
Some((fx, fy)) => {
|
||||
if (tx - fx).abs() > eps || (ty - fy).abs() > eps {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
/// Returns true iff the matrix is a pure translation (within a small
|
||||
/// epsilon to absorb floating-point noise from modifier composition).
|
||||
/// Drag-sprite's per-rAF blit assumes the captured pixels are still
|
||||
@ -122,6 +196,7 @@ impl RenderState {
|
||||
pub fn drag_sprite_try_capture(
|
||||
&mut self,
|
||||
ids: &[Uuid],
|
||||
_modifiers: &HashMap<Uuid, skia::Matrix>,
|
||||
tree: ShapesPoolRef,
|
||||
timestamp: i32,
|
||||
) -> Result<()> {
|
||||
@ -129,7 +204,11 @@ impl RenderState {
|
||||
return Ok(());
|
||||
}
|
||||
if ids.len() != 1 {
|
||||
// Multi-shape drag: out of Phase 3 scope.
|
||||
// Multi-shape drag (incl. layout containers whose
|
||||
// `propagate-modifiers` expanded into per-descendant
|
||||
// transforms): no fast path — slow tile walker runs for the
|
||||
// gesture. Layout-aware drag-sprite optimization is a known
|
||||
// open problem.
|
||||
self.drag_sprite_state = DragSpriteState::Disabled;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@ -403,6 +403,108 @@ impl Surfaces {
|
||||
self.atlas_size.width > 0 && self.atlas_size.height > 0
|
||||
}
|
||||
|
||||
/// True iff the atlas (a) exists, (b) has the same scale as the
|
||||
/// workspace render scale (no resampling on the sprite), and (c)
|
||||
/// fully contains `doc_rect`. Strict because the layout-drag
|
||||
/// atlas-sample path is gated on this and any miss should bail to
|
||||
/// the slow path with no fallback — partial coverage / scale
|
||||
/// mismatch caused too many regressions.
|
||||
///
|
||||
/// Currently dormant — pairs with the layout-drag atlas-sample
|
||||
/// shortcut that hasn't landed cleanly yet.
|
||||
#[allow(dead_code)]
|
||||
pub fn atlas_covers_doc_rect_strict(
|
||||
&self,
|
||||
doc_rect: skia::Rect,
|
||||
workspace_scale: f32,
|
||||
) -> bool {
|
||||
if !self.has_atlas() {
|
||||
return false;
|
||||
}
|
||||
if (self.atlas_scale - workspace_scale).abs() > 1e-3 {
|
||||
return false;
|
||||
}
|
||||
let atlas_scale = self.atlas_scale.max(0.01);
|
||||
let left = self.atlas_origin.x;
|
||||
let top = self.atlas_origin.y;
|
||||
let right = left + (self.atlas_size.width as f32) / atlas_scale;
|
||||
let bottom = top + (self.atlas_size.height as f32) / atlas_scale;
|
||||
let eps = 1e-3_f32;
|
||||
doc_rect.left >= left - eps
|
||||
&& doc_rect.top >= top - eps
|
||||
&& doc_rect.right <= right + eps
|
||||
&& doc_rect.bottom <= bottom + eps
|
||||
}
|
||||
|
||||
/// Doc-space rect covered by the atlas, or `None` when the atlas
|
||||
/// is empty.
|
||||
///
|
||||
/// Currently dormant — pairs with the layout-drag atlas-sample
|
||||
/// shortcut that hasn't landed cleanly yet.
|
||||
#[allow(dead_code)]
|
||||
pub fn atlas_doc_extents(&self) -> Option<skia::Rect> {
|
||||
if !self.has_atlas() {
|
||||
return None;
|
||||
}
|
||||
let atlas_scale = self.atlas_scale.max(0.01);
|
||||
let left = self.atlas_origin.x;
|
||||
let top = self.atlas_origin.y;
|
||||
let right = left + (self.atlas_size.width as f32) / atlas_scale;
|
||||
let bottom = top + (self.atlas_size.height as f32) / atlas_scale;
|
||||
Some(skia::Rect::from_ltrb(left, top, right, bottom))
|
||||
}
|
||||
|
||||
/// True iff the atlas exists at a usable scale for sampling at the
|
||||
/// given `workspace_scale`. We accept some downscale (atlas may be
|
||||
/// reduced when document bounds exceed the texture cap) since a
|
||||
/// slightly resampled drag preview is still much better than the
|
||||
/// alternative — re-rendering the dragged subtree, which freezes
|
||||
/// the UI on layouts with many components.
|
||||
///
|
||||
/// Currently dormant — pairs with the layout-drag atlas-sample
|
||||
/// shortcut that hasn't landed cleanly yet.
|
||||
#[allow(dead_code)]
|
||||
pub fn atlas_supports_sample(&self, workspace_scale: f32) -> bool {
|
||||
if !self.has_atlas() {
|
||||
return false;
|
||||
}
|
||||
// Allow up to 2× upscale (atlas_scale ≥ 0.5 × workspace_scale).
|
||||
// Below that the sprite gets visibly soft; better to fall back.
|
||||
self.atlas_scale + 1e-4 >= workspace_scale * 0.5
|
||||
}
|
||||
|
||||
/// Snapshot the atlas region covering `doc_rect` as a standalone
|
||||
/// `skia::Image`. Used by the drag-sprite atlas-sample shortcut
|
||||
/// (multi-id / layout drag path) to obtain a sprite without
|
||||
/// re-rendering the dragged subtree. Returns `None` when the atlas
|
||||
/// is empty or the region snapshot fails. Caller should pre-validate
|
||||
/// coverage with `atlas_covers_doc_rect_strict`.
|
||||
///
|
||||
/// Currently dormant — pairs with the layout-drag atlas-sample
|
||||
/// shortcut that hasn't landed cleanly yet.
|
||||
#[allow(dead_code)]
|
||||
pub fn snapshot_atlas_doc_rect(&mut self, doc_rect: skia::Rect) -> Option<skia::Image> {
|
||||
if !self.has_atlas() {
|
||||
return None;
|
||||
}
|
||||
let atlas_scale = self.atlas_scale.max(0.01);
|
||||
let x = ((doc_rect.left - self.atlas_origin.x) * atlas_scale).floor() as i32;
|
||||
let y = ((doc_rect.top - self.atlas_origin.y) * atlas_scale).floor() as i32;
|
||||
let w = ((doc_rect.width() * atlas_scale).ceil() as i32).max(1);
|
||||
let h = ((doc_rect.height() * atlas_scale).ceil() as i32).max(1);
|
||||
// Clamp to atlas extents so the snapshot stays in-bounds even
|
||||
// if the caller's `atlas_covers_doc_rect_at_scale` rounded down
|
||||
// by a sub-pixel.
|
||||
let aw = self.atlas_size.width;
|
||||
let ah = self.atlas_size.height;
|
||||
let x = x.clamp(0, aw.saturating_sub(1));
|
||||
let y = y.clamp(0, ah.saturating_sub(1));
|
||||
let w = w.min(aw - x).max(1);
|
||||
let h = h.min(ah - y).max(1);
|
||||
let irect = skia::IRect::from_xywh(x, y, w, h);
|
||||
self.atlas.image_snapshot_with_bounds(irect)
|
||||
}
|
||||
|
||||
/// Draw the persistent atlas onto the target using the current viewbox transform.
|
||||
/// Intended for fast pan/zoom-out previews (avoids per-tile composition).
|
||||
/// Clears Target to `background` first so atlas-uncovered regions don't
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user