mirror of
https://github.com/penpot/penpot.git
synced 2026-08-29 16:18:48 +00:00
Merge remote-tracking branch 'origin/staging' into develop
This commit is contained in:
commit
7429b97f86
@ -325,13 +325,16 @@
|
|||||||
(let [size-px (cond (= size "l") icon-size-l
|
(let [size-px (cond (= size "l") icon-size-l
|
||||||
(= size "s") icon-size-s
|
(= size "s") icon-size-s
|
||||||
:else icon-size-m)
|
:else icon-size-m)
|
||||||
|
offset (if (or (= size "s") (= size "m"))
|
||||||
|
(/ (- icon-size-m size-px) 2)
|
||||||
|
0)
|
||||||
props (mf/spread-props props
|
props (mf/spread-props props
|
||||||
{:class [class (stl/css :icon)]
|
{:class [class (stl/css :icon)]
|
||||||
:width size-px
|
:width (max icon-size-m size-px)
|
||||||
:height size-px})]
|
:height (max icon-size-m size-px)})]
|
||||||
|
|
||||||
[:> :svg props
|
[:> :svg props
|
||||||
[:use {:href (dm/str "#icon-" icon-id)
|
[:use {:href (dm/str "#icon-" icon-id)
|
||||||
|
:x offset
|
||||||
|
:y offset
|
||||||
:width size-px
|
:width size-px
|
||||||
:height size-px}]]))
|
:height size-px}]]))
|
||||||
|
|||||||
@ -96,14 +96,6 @@
|
|||||||
height: deprecated.$s-32;
|
height: deprecated.$s-32;
|
||||||
width: deprecated.$s-24;
|
width: deprecated.$s-24;
|
||||||
padding: 0 deprecated.$s-4 0 deprecated.$s-8;
|
padding: 0 deprecated.$s-4 0 deprecated.$s-8;
|
||||||
|
|
||||||
svg {
|
|
||||||
@extend %button-icon-small;
|
|
||||||
|
|
||||||
color: transparent;
|
|
||||||
fill: none;
|
|
||||||
stroke: var(--icon-foreground);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-actions {
|
.page-actions {
|
||||||
|
|||||||
@ -1,61 +1,73 @@
|
|||||||
use std::sync::Mutex;
|
use crate::{error::Result, performance};
|
||||||
|
|
||||||
use crate::error::{Error, Result, CRITICAL_ERROR};
|
|
||||||
|
|
||||||
pub const LAYOUT_ALIGN: usize = 4;
|
pub const LAYOUT_ALIGN: usize = 4;
|
||||||
|
|
||||||
pub static BUFFERU8: Mutex<Option<Vec<u8>>> = Mutex::new(None);
|
// Please, read about the #[allow(static_mut_refs)]
|
||||||
pub static BUFFER_ERROR: Mutex<u8> = Mutex::new(0x00);
|
//
|
||||||
|
// If we don't put this allow, the compiler shows a warning like this:
|
||||||
|
//
|
||||||
|
// shared references to mutable statics are dangerous; it's undefined behavior
|
||||||
|
// if the static is mutated or if a mutable reference is created for it while
|
||||||
|
// the shared reference lives
|
||||||
|
//
|
||||||
|
// https://doc.rust-lang.org/edition-guide/rust-2024/static-mut-references.html
|
||||||
|
//
|
||||||
|
// But this isn't a problem in a single-threaded environment like WebAssembly
|
||||||
|
// because access/modification is always sequential, not parallel.
|
||||||
|
pub static mut BUFFERU8: Option<Vec<u8>> = None;
|
||||||
|
pub static mut BUFFER_ERROR: u8 = 0x00;
|
||||||
|
|
||||||
pub fn clear_error_code() {
|
pub fn clear_error_code() {
|
||||||
let mut guard = BUFFER_ERROR.lock().unwrap();
|
unsafe {
|
||||||
*guard = 0x00;
|
BUFFER_ERROR = 0x00;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sets the error buffer from a byte. Used by #[wasm_error] when E: Into<u8>.
|
/// Sets the error buffer from a byte. Used by #[wasm_error] when E: Into<u8>.
|
||||||
pub fn set_error_code(code: u8) {
|
pub fn set_error_code(code: u8) {
|
||||||
let mut guard = BUFFER_ERROR.lock().unwrap();
|
unsafe {
|
||||||
*guard = code;
|
BUFFER_ERROR = code;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub extern "C" fn read_error_code() -> u8 {
|
pub extern "C" fn read_error_code() -> u8 {
|
||||||
if let Ok(guard) = BUFFER_ERROR.lock() {
|
unsafe { BUFFER_ERROR }
|
||||||
*guard
|
|
||||||
} else {
|
|
||||||
CRITICAL_ERROR
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn write_bytes(mut bytes: Vec<u8>) -> *mut u8 {
|
pub fn write_bytes(mut bytes: Vec<u8>) -> *mut u8 {
|
||||||
let mut guard = BUFFERU8.lock().unwrap();
|
unsafe {
|
||||||
|
performance::begin_measure!("write_bytes");
|
||||||
|
#[allow(static_mut_refs)]
|
||||||
|
if BUFFERU8.is_some() {
|
||||||
|
panic!("Bytes already allocated");
|
||||||
|
}
|
||||||
|
|
||||||
if guard.is_some() {
|
let ptr = bytes.as_mut_ptr();
|
||||||
panic!("Bytes already allocated");
|
BUFFERU8 = Some(bytes);
|
||||||
|
performance::end_measure!("write_bytes");
|
||||||
|
ptr
|
||||||
}
|
}
|
||||||
|
|
||||||
let ptr = bytes.as_mut_ptr();
|
|
||||||
|
|
||||||
*guard = Some(bytes);
|
|
||||||
ptr
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn bytes() -> Vec<u8> {
|
pub fn bytes() -> Vec<u8> {
|
||||||
let mut guard = BUFFERU8.lock().unwrap();
|
unsafe {
|
||||||
guard.take().expect("Buffer is not initialized")
|
#[allow(static_mut_refs)]
|
||||||
|
BUFFERU8.take().expect("Buffer is not initialized")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn bytes_or_empty() -> Vec<u8> {
|
pub fn bytes_or_empty() -> Vec<u8> {
|
||||||
let mut guard = BUFFERU8.lock().unwrap();
|
unsafe {
|
||||||
guard.take().unwrap_or_default()
|
#[allow(static_mut_refs)]
|
||||||
|
BUFFERU8.take().unwrap_or_default()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn free_bytes() -> Result<()> {
|
pub fn free_bytes() -> Result<()> {
|
||||||
let mut guard = BUFFERU8
|
unsafe {
|
||||||
.lock()
|
BUFFERU8 = None;
|
||||||
.map_err(|_| Error::CriticalError("Failed to lock buffer".to_string()))?;
|
}
|
||||||
*guard = None;
|
|
||||||
std::mem::drop(guard);
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -381,7 +381,7 @@ pub(crate) struct RenderState {
|
|||||||
/// interactive backdrop exactly once per gesture (first rAF) so we don't
|
/// interactive backdrop exactly once per gesture (first rAF) so we don't
|
||||||
/// repeatedly overwrite tiles that have already been updated.
|
/// repeatedly overwrite tiles that have already been updated.
|
||||||
pub interactive_target_seeded: bool,
|
pub interactive_target_seeded: bool,
|
||||||
/// GPU crops from `Backbuffer` keyed by shape id. Filled on full-frame completion; during
|
/// GPU crops from `Backbuffer` or tile atlas keyed by shape id. Filled on full-frame completion; during
|
||||||
/// drag, entries for the moved top-level selection are ensured here
|
/// drag, entries for the moved top-level selection are ensured here
|
||||||
pub backbuffer_crop_cache: HashMap<Uuid, InteractiveDragCrop>,
|
pub backbuffer_crop_cache: HashMap<Uuid, InteractiveDragCrop>,
|
||||||
}
|
}
|
||||||
@ -389,9 +389,6 @@ pub(crate) struct RenderState {
|
|||||||
pub struct InteractiveDragCrop {
|
pub struct InteractiveDragCrop {
|
||||||
pub src_doc_bounds: Rect,
|
pub src_doc_bounds: Rect,
|
||||||
pub src_selrect: Rect,
|
pub src_selrect: Rect,
|
||||||
/// True if the captured crop bounds were fully inside the viewport at capture time.
|
|
||||||
/// Used to avoid serving partial/offscreen crops during interactive drag.
|
|
||||||
pub fits_viewport_at_capture: bool,
|
|
||||||
/// Viewbox origin (doc-space) at capture time.
|
/// Viewbox origin (doc-space) at capture time.
|
||||||
pub capture_vb_left: f32,
|
pub capture_vb_left: f32,
|
||||||
pub capture_vb_top: f32,
|
pub capture_vb_top: f32,
|
||||||
@ -401,6 +398,49 @@ pub struct InteractiveDragCrop {
|
|||||||
pub image: skia::Image,
|
pub image: skia::Image,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Chooses a window inside the full workspace-pixel crop `[0, out_w) × [0, out_h)` with each side
|
||||||
|
/// at most `max_side_px` (**without scaling**): centered on the projection of
|
||||||
|
/// `viewport_doc ∩ src_doc_bounds`, or on the full crop if that intersection is empty.
|
||||||
|
/// `max_side_px` should match [`Surfaces::max_texture_dimension_px`] (same budget as the atlas).
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
fn drag_crop_snapshot_window_px(
|
||||||
|
max_side_px: i32,
|
||||||
|
out_w: i32,
|
||||||
|
out_h: i32,
|
||||||
|
viewport_doc: Rect,
|
||||||
|
vb_left: f32,
|
||||||
|
vb_top: f32,
|
||||||
|
scale: f32,
|
||||||
|
src_left_px: i32,
|
||||||
|
src_top_px: i32,
|
||||||
|
src_doc_bounds: Rect,
|
||||||
|
) -> (i32, i32, i32, i32) {
|
||||||
|
let cap = max_side_px.max(1);
|
||||||
|
if out_w <= cap && out_h <= cap {
|
||||||
|
return (0, 0, out_w, out_h);
|
||||||
|
}
|
||||||
|
let win_w = out_w.min(cap);
|
||||||
|
let win_h = out_h.min(cap);
|
||||||
|
|
||||||
|
let mut vis = viewport_doc;
|
||||||
|
let has_vis = vis.intersect(src_doc_bounds);
|
||||||
|
let (cx, cy) = if !has_vis || vis.is_empty() {
|
||||||
|
(out_w as f32 * 0.5, out_h as f32 * 0.5)
|
||||||
|
} else {
|
||||||
|
let lx0 = (vis.left - vb_left) * scale - src_left_px as f32;
|
||||||
|
let ly0 = (vis.top - vb_top) * scale - src_top_px as f32;
|
||||||
|
let lx1 = (vis.right - vb_left) * scale - src_left_px as f32;
|
||||||
|
let ly1 = (vis.bottom - vb_top) * scale - src_top_px as f32;
|
||||||
|
((lx0 + lx1) * 0.5, (ly0 + ly1) * 0.5)
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut ox = (cx - win_w as f32 * 0.5).round() as i32;
|
||||||
|
let mut oy = (cy - win_h as f32 * 0.5).round() as i32;
|
||||||
|
ox = ox.clamp(0, out_w - win_w);
|
||||||
|
oy = oy.clamp(0, out_h - win_h);
|
||||||
|
(ox, oy, win_w, win_h)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn get_cache_size(viewbox: Viewbox, scale: f32, interest: i32) -> skia::ISize {
|
pub fn get_cache_size(viewbox: Viewbox, scale: f32, 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) =
|
||||||
@ -458,10 +498,7 @@ impl RenderState {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
let Some(crop) = self.backbuffer_crop_cache.get(&node_id) else {
|
if !self.backbuffer_crop_cache.contains_key(&node_id) {
|
||||||
return false;
|
|
||||||
};
|
|
||||||
if !crop.fits_viewport_at_capture {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1666,10 +1703,12 @@ impl RenderState {
|
|||||||
self.backbuffer_crop_cache.clear();
|
self.backbuffer_crop_cache.clear();
|
||||||
|
|
||||||
// Collect candidate shapes that are "recortable" and visible in the current viewport.
|
// Collect candidate shapes that are "recortable" and visible in the current viewport.
|
||||||
|
|
||||||
// This is intentionally conservative; we only cache shapes that do not overlap with
|
// This is intentionally conservative; we only cache shapes that do not overlap with
|
||||||
// ANY other candidate to guarantee the pixels under their bounds belong exclusively
|
// ANY other candidate to guarantee the pixels under their bounds belong exclusively
|
||||||
// to that shape in Backbuffer.
|
// to that shape in Backbuffer.
|
||||||
let viewport = self.viewbox.area;
|
let viewport = self.viewbox.area;
|
||||||
|
let scale = self.get_scale();
|
||||||
let mut candidates: Vec<(Uuid, Rect, Rect)> = Vec::new(); // (id, doc_bounds, selrect)
|
let mut candidates: Vec<(Uuid, Rect, Rect)> = Vec::new(); // (id, doc_bounds, selrect)
|
||||||
|
|
||||||
let root_ids: Vec<Uuid> = match tree.get(&Uuid::nil()) {
|
let root_ids: Vec<Uuid> = match tree.get(&Uuid::nil()) {
|
||||||
@ -1700,23 +1739,57 @@ impl RenderState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Filter out any candidate that overlaps with any other candidate.
|
// Filter out any candidate that overlaps with any other candidate.
|
||||||
let mut non_overlapping: Vec<(Uuid, Rect, Rect)> = Vec::new();
|
// Sort by left edge so the inner loop can break early once no further
|
||||||
'outer: for (i, (id, bounds, selrect)) in candidates.iter().enumerate() {
|
// x-overlap is possible, reducing comparisons from O(N²) to O(N log N)
|
||||||
for (j, (_id2, bounds2, _sel2)) in candidates.iter().enumerate() {
|
// in typical layouts where shapes are spread out.
|
||||||
if i == j {
|
candidates.sort_unstable_by(|a, b| {
|
||||||
continue;
|
a.1.left
|
||||||
|
.partial_cmp(&b.1.left)
|
||||||
|
.unwrap_or(std::cmp::Ordering::Equal)
|
||||||
|
});
|
||||||
|
let n = candidates.len();
|
||||||
|
let mut is_overlapping = vec![false; n];
|
||||||
|
for i in 0..n {
|
||||||
|
for j in (i + 1)..n {
|
||||||
|
if candidates[j].1.left >= candidates[i].1.right {
|
||||||
|
break; // sorted: no further x-overlap possible for i
|
||||||
}
|
}
|
||||||
if bounds.intersects(*bounds2) {
|
if is_overlapping[i] && is_overlapping[j] {
|
||||||
continue 'outer;
|
continue; // both already excluded, skip check
|
||||||
|
}
|
||||||
|
if candidates[i].1.intersects(candidates[j].1) {
|
||||||
|
is_overlapping[i] = true;
|
||||||
|
is_overlapping[j] = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
non_overlapping.push((*id, *bounds, *selrect));
|
|
||||||
}
|
}
|
||||||
|
let non_overlapping: Vec<(Uuid, Rect, Rect)> = candidates
|
||||||
|
.iter()
|
||||||
|
.zip(is_overlapping.iter())
|
||||||
|
.filter_map(|((id, bounds, selrect), ov)| {
|
||||||
|
if !ov {
|
||||||
|
Some((*id, *bounds, *selrect))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
// Snapshot from Backbuffer for each accepted shape.
|
|
||||||
let scale = self.get_scale();
|
|
||||||
let vb_left = self.viewbox.area.left;
|
let vb_left = self.viewbox.area.left;
|
||||||
let vb_top = self.viewbox.area.top;
|
let vb_top = self.viewbox.area.top;
|
||||||
|
let (bb_w, bb_h) = self.surfaces.surface_size(SurfaceId::Backbuffer);
|
||||||
|
let max_snap_px = self.surfaces.max_texture_dimension_px();
|
||||||
|
|
||||||
|
// Snapshot the atlas once for the whole pass so that all shapes sharing
|
||||||
|
// the tile/atlas fallback path reuse the same GPU image rather than each
|
||||||
|
// triggering a separate `image_snapshot` flush.
|
||||||
|
let atlas_snap = self.surfaces.atlas_snapshot_for_drag_crop();
|
||||||
|
|
||||||
|
// Scratch surface reused across all shapes that need the tile/atlas
|
||||||
|
// fallback — avoids one WebGL texture allocation per shape.
|
||||||
|
// Created lazily on first use and grown if a later shape needs more space.
|
||||||
|
let mut scratch_surface: Option<skia::Surface> = None;
|
||||||
|
|
||||||
for (id, doc_bounds, selrect) in non_overlapping {
|
for (id, doc_bounds, selrect) in non_overlapping {
|
||||||
let left = ((doc_bounds.left - vb_left) * scale).floor() as i32;
|
let left = ((doc_bounds.left - vb_left) * scale).floor() as i32;
|
||||||
let top = ((doc_bounds.top - vb_top) * scale).floor() as i32;
|
let top = ((doc_bounds.top - vb_top) * scale).floor() as i32;
|
||||||
@ -1726,12 +1799,6 @@ impl RenderState {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let src_irect = skia::IRect::new(left, top, right, bottom);
|
let src_irect = skia::IRect::new(left, top, right, bottom);
|
||||||
let Some(image) = self
|
|
||||||
.surfaces
|
|
||||||
.snapshot_rect(SurfaceId::Backbuffer, src_irect)
|
|
||||||
else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
|
|
||||||
let src_doc_bounds = Rect::new(
|
let src_doc_bounds = Rect::new(
|
||||||
src_irect.left as f32 / scale + vb_left,
|
src_irect.left as f32 / scale + vb_left,
|
||||||
@ -1739,30 +1806,92 @@ impl RenderState {
|
|||||||
src_irect.right as f32 / scale + vb_left,
|
src_irect.right as f32 / scale + vb_left,
|
||||||
src_irect.bottom as f32 / scale + vb_top,
|
src_irect.bottom as f32 / scale + vb_top,
|
||||||
);
|
);
|
||||||
let fits_viewport_at_capture = doc_bounds.left >= viewport.left
|
|
||||||
&& doc_bounds.top >= viewport.top
|
|
||||||
&& doc_bounds.right <= viewport.right
|
|
||||||
&& doc_bounds.bottom <= viewport.bottom;
|
|
||||||
|
|
||||||
// When the shape extends beyond the viewport to the left or top,
|
let full_w = src_irect.width();
|
||||||
// `left`/`top` are negative. Skia clamps `makeImageSnapshot` to the
|
let full_h = src_irect.height();
|
||||||
// surface bounds, so the returned image actually starts at pixel 0 —
|
let (win_ox, win_oy, win_w, win_h) = drag_crop_snapshot_window_px(
|
||||||
// not at the negative coordinate. Store the clamped value so that
|
max_snap_px,
|
||||||
// `doc_left`/`doc_top` computed during the drag reflects the true
|
full_w,
|
||||||
// image origin in the backbuffer.
|
full_h,
|
||||||
let capture_src_left = left.max(0);
|
viewport,
|
||||||
let capture_src_top = top.max(0);
|
vb_left,
|
||||||
|
vb_top,
|
||||||
|
scale,
|
||||||
|
src_irect.left,
|
||||||
|
src_irect.top,
|
||||||
|
src_doc_bounds,
|
||||||
|
);
|
||||||
|
let window_irect = skia::IRect::new(
|
||||||
|
src_irect.left + win_ox,
|
||||||
|
src_irect.top + win_oy,
|
||||||
|
src_irect.left + win_ox + win_w,
|
||||||
|
src_irect.top + win_oy + win_h,
|
||||||
|
);
|
||||||
|
|
||||||
|
let src_doc_window = Rect::new(
|
||||||
|
window_irect.left as f32 / scale + vb_left,
|
||||||
|
window_irect.top as f32 / scale + vb_top,
|
||||||
|
window_irect.right as f32 / scale + vb_left,
|
||||||
|
window_irect.bottom as f32 / scale + vb_top,
|
||||||
|
);
|
||||||
|
|
||||||
|
let in_backbuffer = window_irect.left >= 0
|
||||||
|
&& window_irect.top >= 0
|
||||||
|
&& window_irect.right <= bb_w
|
||||||
|
&& window_irect.bottom <= bb_h;
|
||||||
|
|
||||||
|
let backbuffer_snap = if in_backbuffer {
|
||||||
|
self.surfaces
|
||||||
|
.snapshot_rect(SurfaceId::Backbuffer, window_irect)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
let image = if let Some(img) = backbuffer_snap {
|
||||||
|
img
|
||||||
|
} else {
|
||||||
|
// Ensure the scratch surface is large enough for this window.
|
||||||
|
// Grow (reallocate) only when necessary so that the common case
|
||||||
|
// of similarly-sized shapes pays zero extra allocation cost.
|
||||||
|
let needs_alloc = scratch_surface
|
||||||
|
.as_ref()
|
||||||
|
.is_none_or(|s| s.width() < win_w || s.height() < win_h);
|
||||||
|
if needs_alloc {
|
||||||
|
scratch_surface = get_gpu_state()
|
||||||
|
.create_surface_with_isize(
|
||||||
|
"drag_crop_scratch".to_string(),
|
||||||
|
skia::ISize::new(win_w, win_h),
|
||||||
|
)
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
let Some(scratch) = scratch_surface.as_mut() else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let Some(img) = self.surfaces.try_snapshot_doc_rect_from_tiles_and_atlas(
|
||||||
|
scratch,
|
||||||
|
atlas_snap.as_ref(),
|
||||||
|
src_doc_window,
|
||||||
|
window_irect,
|
||||||
|
win_w,
|
||||||
|
win_h,
|
||||||
|
vb_left,
|
||||||
|
vb_top,
|
||||||
|
scale,
|
||||||
|
) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
img
|
||||||
|
};
|
||||||
|
|
||||||
self.backbuffer_crop_cache.insert(
|
self.backbuffer_crop_cache.insert(
|
||||||
id,
|
id,
|
||||||
InteractiveDragCrop {
|
InteractiveDragCrop {
|
||||||
src_doc_bounds,
|
src_doc_bounds: src_doc_window,
|
||||||
src_selrect: selrect,
|
src_selrect: selrect,
|
||||||
fits_viewport_at_capture,
|
|
||||||
capture_vb_left: vb_left,
|
capture_vb_left: vb_left,
|
||||||
capture_vb_top: vb_top,
|
capture_vb_top: vb_top,
|
||||||
capture_src_left,
|
capture_src_left: window_irect.left,
|
||||||
capture_src_top,
|
capture_src_top: window_irect.top,
|
||||||
image,
|
image,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@ -3054,7 +3183,10 @@ impl RenderState {
|
|||||||
|
|
||||||
let x = (doc_left + translation.0) * scale;
|
let x = (doc_left + translation.0) * scale;
|
||||||
let y = (doc_top + translation.1) * scale;
|
let y = (doc_top + translation.1) * scale;
|
||||||
canvas.draw_image(crop_image, (x, y), Some(&skia::Paint::default()));
|
let bw = crop_image.width() as f32;
|
||||||
|
let bh = crop_image.height() as f32;
|
||||||
|
let dst = skia::Rect::from_xywh(x, y, bw, bh);
|
||||||
|
canvas.draw_image_rect(crop_image, None, dst, &skia::Paint::default());
|
||||||
|
|
||||||
canvas.restore();
|
canvas.restore();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,7 +5,10 @@ use crate::{get_gpu_state, performance};
|
|||||||
|
|
||||||
use skia_safe::{self as skia, IRect, Paint, RRect};
|
use skia_safe::{self as skia, IRect, Paint, RRect};
|
||||||
|
|
||||||
use super::{gpu_state::GpuState, tiles::Tile, tiles::TileViewbox, tiles::TILE_SIZE};
|
use super::{
|
||||||
|
gpu_state::GpuState,
|
||||||
|
tiles::{self, Tile, TileViewbox, TILE_SIZE},
|
||||||
|
};
|
||||||
|
|
||||||
use base64::{engine::general_purpose, Engine as _};
|
use base64::{engine::general_purpose, Engine as _};
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
@ -183,6 +186,11 @@ impl Surfaces {
|
|||||||
self.max_atlas_texture_size = max_px.clamp(TILE_SIZE as i32, MAX_ATLAS_TEXTURE_SIZE);
|
self.max_atlas_texture_size = max_px.clamp(TILE_SIZE as i32, MAX_ATLAS_TEXTURE_SIZE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn max_texture_dimension_px(&self) -> i32 {
|
||||||
|
self.max_atlas_texture_size
|
||||||
|
}
|
||||||
|
|
||||||
/// Sets the document-space bounds used to clamp atlas updates.
|
/// Sets the document-space bounds used to clamp atlas updates.
|
||||||
/// Pass `None` to disable clamping.
|
/// Pass `None` to disable clamping.
|
||||||
pub fn set_atlas_doc_bounds(&mut self, bounds: Option<skia::Rect>) {
|
pub fn set_atlas_doc_bounds(&mut self, bounds: Option<skia::Rect>) {
|
||||||
@ -1053,6 +1061,118 @@ impl Surfaces {
|
|||||||
self.tiles.has(tile)
|
self.tiles.has(tile)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns a snapshot of the atlas together with its scale and origin, so the
|
||||||
|
/// caller can take it **once** per `rebuild_backbuffer_crop_cache` and share it
|
||||||
|
/// across all shapes that need the tile/atlas fallback path — avoiding an
|
||||||
|
/// `image_snapshot` (and potential GPU flush) per shape.
|
||||||
|
pub fn atlas_snapshot_for_drag_crop(&mut self) -> Option<(skia::Image, f32, skia::Point)> {
|
||||||
|
if !self.has_atlas() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some((
|
||||||
|
self.atlas.image_snapshot(),
|
||||||
|
self.atlas_scale.max(0.01),
|
||||||
|
self.atlas_origin,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builds a 1:1 workspace-pixel snapshot for `src_doc_bounds` / `src_irect` into
|
||||||
|
/// `scratch`, then returns the sub-region `[0, out_w) × [0, out_h)` as an image.
|
||||||
|
///
|
||||||
|
/// `scratch` must be at least `out_w × out_h` pixels — the caller is responsible
|
||||||
|
/// for allocating (and **reusing across shapes**) a surface large enough to hold
|
||||||
|
/// the largest window needed in one `rebuild_backbuffer_crop_cache` pass.
|
||||||
|
///
|
||||||
|
/// `atlas_snap` is a pre-snapshotted view of the persistent atlas produced by
|
||||||
|
/// [`Surfaces::atlas_snapshot_for_drag_crop`]; pass `None` when no atlas exists.
|
||||||
|
///
|
||||||
|
/// For each tile cell intersecting `src_doc_bounds`: draws from
|
||||||
|
/// [`TileTextureCache`] when present; otherwise samples the atlas.
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
pub fn try_snapshot_doc_rect_from_tiles_and_atlas(
|
||||||
|
&mut self,
|
||||||
|
scratch: &mut skia::Surface,
|
||||||
|
atlas_snap: Option<&(skia::Image, f32, skia::Point)>,
|
||||||
|
src_doc_bounds: skia::Rect,
|
||||||
|
src_irect: IRect,
|
||||||
|
out_w: i32,
|
||||||
|
out_h: i32,
|
||||||
|
vb_left: f32,
|
||||||
|
vb_top: f32,
|
||||||
|
scale: f32,
|
||||||
|
) -> Option<skia::Image> {
|
||||||
|
if out_w <= 0 || out_h <= 0 || src_doc_bounds.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let canvas = scratch.canvas();
|
||||||
|
canvas.clear(skia::Color::TRANSPARENT);
|
||||||
|
|
||||||
|
let tile_size = tiles::get_tile_size(scale);
|
||||||
|
let tr = tiles::get_tiles_for_rect(src_doc_bounds, tile_size);
|
||||||
|
let ix0 = src_irect.left as f32;
|
||||||
|
let iy0 = src_irect.top as f32;
|
||||||
|
let paint = skia::Paint::default();
|
||||||
|
|
||||||
|
for ty in tr.y1()..=tr.y2() {
|
||||||
|
for tx in tr.x1()..=tr.x2() {
|
||||||
|
let tile = Tile(tx, ty);
|
||||||
|
let tile_doc = tiles::get_tile_rect(tile, scale);
|
||||||
|
let mut clip_doc = tile_doc;
|
||||||
|
if !clip_doc.intersect(src_doc_bounds) || clip_doc.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let dst = skia::Rect::from_ltrb(
|
||||||
|
(clip_doc.left - vb_left) * scale - ix0,
|
||||||
|
(clip_doc.top - vb_top) * scale - iy0,
|
||||||
|
(clip_doc.right - vb_left) * scale - ix0,
|
||||||
|
(clip_doc.bottom - vb_top) * scale - iy0,
|
||||||
|
);
|
||||||
|
|
||||||
|
if let Some(tile_image) = self.tiles.get(tile) {
|
||||||
|
let iw = tile_image.width() as f32;
|
||||||
|
let ih = tile_image.height() as f32;
|
||||||
|
let td_w = tile_doc.width().max(1e-6);
|
||||||
|
let td_h = tile_doc.height().max(1e-6);
|
||||||
|
|
||||||
|
let src = skia::Rect::from_ltrb(
|
||||||
|
((clip_doc.left - tile_doc.left) / td_w) * iw,
|
||||||
|
((clip_doc.top - tile_doc.top) / td_h) * ih,
|
||||||
|
((clip_doc.right - tile_doc.left) / td_w) * iw,
|
||||||
|
((clip_doc.bottom - tile_doc.top) / td_h) * ih,
|
||||||
|
);
|
||||||
|
|
||||||
|
canvas.draw_image_rect(
|
||||||
|
tile_image,
|
||||||
|
Some((&src, skia::canvas::SrcRectConstraint::Fast)),
|
||||||
|
dst,
|
||||||
|
&paint,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
let snap = atlas_snap?;
|
||||||
|
let (atlas, a_scale, atlas_origin) = (&snap.0, snap.1, snap.2);
|
||||||
|
let sx = (clip_doc.left - atlas_origin.x) * a_scale;
|
||||||
|
let sy = (clip_doc.top - atlas_origin.y) * a_scale;
|
||||||
|
let sw = clip_doc.width() * a_scale;
|
||||||
|
let sh = clip_doc.height() * a_scale;
|
||||||
|
if sw <= 0.0 || sh <= 0.0 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let src = skia::Rect::from_xywh(sx, sy, sw, sh);
|
||||||
|
canvas.draw_image_rect(
|
||||||
|
atlas,
|
||||||
|
Some((&src, skia::canvas::SrcRectConstraint::Fast)),
|
||||||
|
dst,
|
||||||
|
&paint,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
scratch.image_snapshot_with_bounds(IRect::new(0, 0, out_w, out_h))
|
||||||
|
}
|
||||||
|
|
||||||
pub fn remove_cached_tile_surface(&mut self, tile: Tile) {
|
pub fn remove_cached_tile_surface(&mut self, tile: Tile) {
|
||||||
let gpu_state = get_gpu_state();
|
let gpu_state = get_gpu_state();
|
||||||
// Mark tile as invalid
|
// Mark tile as invalid
|
||||||
|
|||||||
@ -9,15 +9,22 @@ use macros::wasm_error;
|
|||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
#[wasm_error]
|
#[wasm_error]
|
||||||
pub extern "C" fn alloc_bytes(len: usize) -> Result<*mut u8> {
|
pub extern "C" fn alloc_bytes(len: usize) -> Result<*mut u8> {
|
||||||
let mut guard = BUFFERU8
|
|
||||||
.lock()
|
|
||||||
.map_err(|_| Error::CriticalError("Failed to lock buffer".to_string()))?;
|
|
||||||
|
|
||||||
if guard.is_some() {
|
|
||||||
return Err(Error::CriticalError("Bytes already allocated".to_string()));
|
|
||||||
}
|
|
||||||
|
|
||||||
unsafe {
|
unsafe {
|
||||||
|
// If we don't put this allow, the compiler shows a warning like this:
|
||||||
|
//
|
||||||
|
// shared references to mutable statics are dangerous; it's undefined behavior
|
||||||
|
// if the static is mutated or if a mutable reference is created for it while
|
||||||
|
// the shared reference lives
|
||||||
|
//
|
||||||
|
// https://doc.rust-lang.org/edition-guide/rust-2024/static-mut-references.html
|
||||||
|
//
|
||||||
|
// But this isn't a problem in a single-threaded environment like WebAssembly
|
||||||
|
// because access/modification is always sequential, not parallel.
|
||||||
|
#[allow(static_mut_refs)]
|
||||||
|
if BUFFERU8.is_some() {
|
||||||
|
return Err(Error::CriticalError("Bytes already allocated".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
let layout = Layout::from_size_align_unchecked(len, LAYOUT_ALIGN);
|
let layout = Layout::from_size_align_unchecked(len, LAYOUT_ALIGN);
|
||||||
let ptr = alloc(layout);
|
let ptr = alloc(layout);
|
||||||
if ptr.is_null() {
|
if ptr.is_null() {
|
||||||
@ -25,7 +32,7 @@ pub extern "C" fn alloc_bytes(len: usize) -> Result<*mut u8> {
|
|||||||
}
|
}
|
||||||
// TODO: Maybe this could be removed.
|
// TODO: Maybe this could be removed.
|
||||||
ptr::write_bytes(ptr, 0, len);
|
ptr::write_bytes(ptr, 0, len);
|
||||||
*guard = Some(Vec::from_raw_parts(ptr, len, len));
|
BUFFERU8 = Some(Vec::from_raw_parts(ptr, len, len));
|
||||||
Ok(ptr)
|
Ok(ptr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user