mirror of
https://github.com/penpot/penpot.git
synced 2026-04-25 11:18:36 +00:00
WIP
This commit is contained in:
parent
2047aa8cb5
commit
2a7c8dea42
@ -946,7 +946,7 @@ pub extern "C" fn set_modifiers() -> Result<()> {
|
||||
performance::begin_measure!("prepare_drag_layers");
|
||||
state
|
||||
.render_state
|
||||
.prepare_drag_layers(&ids, &state.shapes)?;
|
||||
.prepare_drag_layers(&ids, &mut state.shapes)?;
|
||||
performance::end_measure!("prepare_drag_layers");
|
||||
}
|
||||
|
||||
|
||||
@ -66,6 +66,41 @@ pub struct NodeRenderState {
|
||||
flattened: bool,
|
||||
}
|
||||
|
||||
/// Mark every shape that satisfies `predicate` as hidden and return the
|
||||
/// list of uuids actually toggled so the caller can restore their previous
|
||||
/// state with `restore_hidden`. Shapes that were already hidden are left
|
||||
/// alone and not included in the returned list.
|
||||
fn hide_shapes_with(
|
||||
tree: ShapesPoolMutRef,
|
||||
predicate: impl Fn(&Shape) -> bool,
|
||||
) -> Vec<Uuid> {
|
||||
let candidates: Vec<Uuid> = tree.all_ids();
|
||||
let mut toggled = Vec::new();
|
||||
for id in candidates {
|
||||
let Some(shape) = tree.get_mut(&id) else {
|
||||
continue;
|
||||
};
|
||||
if shape.hidden {
|
||||
continue;
|
||||
}
|
||||
if predicate(&*shape) {
|
||||
shape.hidden = true;
|
||||
toggled.push(id);
|
||||
}
|
||||
}
|
||||
toggled
|
||||
}
|
||||
|
||||
/// Reverses a previous `hide_shapes_with` by clearing the `hidden` flag on
|
||||
/// exactly the shapes that call returned.
|
||||
fn restore_hidden(tree: ShapesPoolMutRef, toggled: &[Uuid]) {
|
||||
for id in toggled {
|
||||
if let Some(shape) = tree.get_mut(id) {
|
||||
shape.hidden = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Get simplified children of a container, flattening nested flattened containers
|
||||
fn get_simplified_children<'a>(tree: ShapesPoolRef<'a>, shape: &'a Shape) -> Vec<Uuid> {
|
||||
let mut result = Vec::new();
|
||||
@ -1937,22 +1972,30 @@ impl RenderState {
|
||||
Ok((data.as_bytes().to_vec(), width, height))
|
||||
}
|
||||
|
||||
/// Rasterize each of `ids` into its own `skia::Image` and store the
|
||||
/// snapshots in `self.drag_layers`. The snapshots are taken with the tree
|
||||
/// in its pre-modifier state so that during the drag we can blit them
|
||||
/// transformed by the current modifier matrix without re-rasterizing.
|
||||
/// Rasterize the snapshots used by the SVG-style layered drag path:
|
||||
///
|
||||
/// Must be called from an interactive transform session (`set_modifiers_start`
|
||||
/// has been invoked). The WASM entry point wires the call so the frontend
|
||||
/// does not need to manage layer lifecycles explicitly.
|
||||
/// * one `DragLayer` image per selected shape,
|
||||
/// * a `backdrop` image of the viewport with the selected shapes
|
||||
/// hidden,
|
||||
/// * an `overlay` image that only contains the shapes that sit above
|
||||
/// the selection in the z-order (captured on transparent).
|
||||
///
|
||||
/// The state-saving dance mirrors `render_shape_pixels`: it touches the
|
||||
/// Export surface, the render area and the pending-node stack, all of
|
||||
/// which belong to the main workspace render and must not leak.
|
||||
/// The snapshots are taken with the tree in its pre-modifier state so
|
||||
/// that during the drag we can blit them transformed by the current
|
||||
/// modifier matrix without re-rasterizing.
|
||||
///
|
||||
/// Must be called from an interactive transform session
|
||||
/// (`set_modifiers_start` has been invoked). The WASM entry point wires
|
||||
/// the call so the frontend does not need to manage layer lifecycles
|
||||
/// explicitly.
|
||||
///
|
||||
/// The tree is borrowed mutably because capturing the backdrop / overlay
|
||||
/// requires toggling `Shape::hidden` on a subset of shapes. The flags
|
||||
/// are restored before this function returns.
|
||||
pub fn prepare_drag_layers(
|
||||
&mut self,
|
||||
ids: &[Uuid],
|
||||
tree: ShapesPoolRef,
|
||||
tree: ShapesPoolMutRef,
|
||||
) -> Result<()> {
|
||||
self.drag_layers.clear();
|
||||
if ids.is_empty() {
|
||||
@ -1973,13 +2016,100 @@ impl RenderState {
|
||||
let saved_ignore_nested_blurs = self.ignore_nested_blurs;
|
||||
let saved_preview_mode = self.preview_mode;
|
||||
|
||||
// Disable focus mode while rasterizing individual shapes so that a
|
||||
// pre-existing focus filter from the workspace cannot hide the shape
|
||||
// we are trying to snapshot.
|
||||
// Disable focus mode while rasterizing so that a pre-existing focus
|
||||
// filter from the workspace cannot hide the shapes we are trying to
|
||||
// snapshot.
|
||||
self.focus_mode.clear();
|
||||
|
||||
let target_surface = SurfaceId::Export;
|
||||
// Per-shape isolated snapshots. Use an immutable reborrow so the
|
||||
// tree is only accessed through `get` for this block; bulk hidden
|
||||
// toggles come later and require the mutable borrow back.
|
||||
{
|
||||
let tree_ref: ShapesPoolRef = tree;
|
||||
self.capture_drag_layer_images(ids, tree_ref, scale)?;
|
||||
}
|
||||
|
||||
// Backdrop + overlay are viewport-sized snapshots. They share the
|
||||
// same destination rect — the viewbox area in document space — and
|
||||
// are sampled 1:1 at `scale`.
|
||||
let viewport_rect = self.viewbox.area;
|
||||
let selected_set: HashSet<Uuid> = ids.iter().copied().collect();
|
||||
|
||||
// --- Backdrop: whole scene with the selected shapes hidden --------
|
||||
let hidden_backdrop = hide_shapes_with(tree, |shape| selected_set.contains(&shape.id));
|
||||
{
|
||||
let tree_ref: ShapesPoolRef = tree;
|
||||
let backdrop = self.capture_viewport_snapshot(
|
||||
tree_ref,
|
||||
viewport_rect,
|
||||
scale,
|
||||
skia::Color::TRANSPARENT,
|
||||
)?;
|
||||
self.drag_layers.backdrop = Some(backdrop);
|
||||
}
|
||||
restore_hidden(tree, &hidden_backdrop);
|
||||
|
||||
// --- Overlay: only the shapes above the selection ----------------
|
||||
// The frontend always sees at least the selection itself above the
|
||||
// backdrop, so we only bother capturing an overlay when there is
|
||||
// something *else* stacked on top.
|
||||
let above_ids = tree.collect_above_of(&selected_set);
|
||||
if !above_ids.is_empty() {
|
||||
let hidden_overlay =
|
||||
hide_shapes_with(tree, |shape| !above_ids.contains(&shape.id));
|
||||
{
|
||||
let tree_ref: ShapesPoolRef = tree;
|
||||
let overlay = self.capture_viewport_snapshot(
|
||||
tree_ref,
|
||||
viewport_rect,
|
||||
scale,
|
||||
skia::Color::TRANSPARENT,
|
||||
)?;
|
||||
self.drag_layers.overlay = Some(overlay);
|
||||
}
|
||||
restore_hidden(tree, &hidden_overlay);
|
||||
}
|
||||
|
||||
self.drag_layers.viewport_rect = viewport_rect;
|
||||
|
||||
// Restore the workspace render state exactly like `render_shape_pixels`
|
||||
// does. Without this the next workspace render could observe stale
|
||||
// render_area / focus / pending_nodes left over from our snapshots.
|
||||
self.focus_mode = saved_focus_mode;
|
||||
self.export_context = saved_export_context;
|
||||
self.render_area = saved_render_area;
|
||||
self.render_area_with_margins = saved_render_area_with_margins;
|
||||
self.current_tile = saved_current_tile;
|
||||
self.pending_nodes = saved_pending_nodes;
|
||||
self.nested_fills = saved_nested_fills;
|
||||
self.nested_blurs = saved_nested_blurs;
|
||||
self.nested_shadows = saved_nested_shadows;
|
||||
self.ignore_nested_blurs = saved_ignore_nested_blurs;
|
||||
self.preview_mode = saved_preview_mode;
|
||||
|
||||
let workspace_scale = self.get_scale();
|
||||
if let Some(tile) = self.current_tile {
|
||||
self.update_render_context(tile);
|
||||
} else if !self.render_area.is_empty() {
|
||||
self.surfaces
|
||||
.update_render_context(self.render_area, workspace_scale);
|
||||
}
|
||||
|
||||
self.drag_layers.active = true;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Rasterize every id in `ids` into its own `DragLayer` image, sized
|
||||
/// tightly around the shape's extrect. Extracted from
|
||||
/// `prepare_drag_layers` so the borrow of `tree` stays immutable for
|
||||
/// the duration of the loop.
|
||||
fn capture_drag_layer_images(
|
||||
&mut self,
|
||||
ids: &[Uuid],
|
||||
tree: ShapesPoolRef,
|
||||
scale: f32,
|
||||
) -> Result<()> {
|
||||
let target_surface = SurfaceId::Export;
|
||||
for id in ids {
|
||||
let Some(shape) = tree.get(id) else {
|
||||
continue;
|
||||
@ -2027,32 +2157,68 @@ impl RenderState {
|
||||
source_doc_rect,
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Restore the workspace render state exactly like `render_shape_pixels`
|
||||
// does. Without this the next workspace render could observe stale
|
||||
// render_area / focus / pending_nodes left over from our snapshots.
|
||||
self.focus_mode = saved_focus_mode;
|
||||
self.export_context = saved_export_context;
|
||||
self.render_area = saved_render_area;
|
||||
self.render_area_with_margins = saved_render_area_with_margins;
|
||||
self.current_tile = saved_current_tile;
|
||||
self.pending_nodes = saved_pending_nodes;
|
||||
self.nested_fills = saved_nested_fills;
|
||||
self.nested_blurs = saved_nested_blurs;
|
||||
self.nested_shadows = saved_nested_shadows;
|
||||
self.ignore_nested_blurs = saved_ignore_nested_blurs;
|
||||
self.preview_mode = saved_preview_mode;
|
||||
|
||||
let workspace_scale = self.get_scale();
|
||||
if let Some(tile) = self.current_tile {
|
||||
self.update_render_context(tile);
|
||||
} else if !self.render_area.is_empty() {
|
||||
self.surfaces
|
||||
.update_render_context(self.render_area, workspace_scale);
|
||||
/// Render the workspace viewport to the Export surface and return its
|
||||
/// snapshot. Assumes `tree` already has the intended `hidden` flags set
|
||||
/// by the caller; this method only drives the render pipeline and does
|
||||
/// not mutate shape state.
|
||||
fn capture_viewport_snapshot(
|
||||
&mut self,
|
||||
tree: ShapesPoolRef,
|
||||
viewport_rect: Rect,
|
||||
scale: f32,
|
||||
clear_color: skia::Color,
|
||||
) -> Result<skia::Image> {
|
||||
if viewport_rect.is_empty() {
|
||||
return Err(Error::CriticalError(
|
||||
"Cannot capture viewport snapshot on empty rect".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
self.drag_layers.active = true;
|
||||
Ok(())
|
||||
let target_surface = SurfaceId::Export;
|
||||
let mut extrect = viewport_rect;
|
||||
|
||||
self.export_context = Some((extrect, scale));
|
||||
let margins = self.surfaces.margins();
|
||||
extrect.offset((margins.width as f32 / scale, margins.height as f32 / scale));
|
||||
|
||||
self.surfaces.resize_export_surface(scale, extrect);
|
||||
self.render_area = extrect;
|
||||
self.render_area_with_margins = extrect;
|
||||
self.surfaces.update_render_context(extrect, scale);
|
||||
|
||||
self.surfaces.canvas(target_surface).clear(clear_color);
|
||||
|
||||
// Seed the traversal with every direct child of the invisible root
|
||||
// shape. Mirrors what `render_shape_tree_partial` does for the
|
||||
// workspace render (it pushes root children, not the root itself).
|
||||
self.pending_nodes.clear();
|
||||
let Some(root) = tree.get(&Uuid::nil()) else {
|
||||
return Err(Error::CriticalError(
|
||||
"Root shape not found while capturing viewport snapshot".to_string(),
|
||||
));
|
||||
};
|
||||
let root_children = root.children_ids(false);
|
||||
self.pending_nodes.extend(root_children.into_iter().map(|id| {
|
||||
NodeRenderState {
|
||||
id,
|
||||
visited_children: false,
|
||||
clip_bounds: None,
|
||||
visited_mask: false,
|
||||
mask: false,
|
||||
flattened: false,
|
||||
}
|
||||
}));
|
||||
self.render_shape_tree_partial_uncached(tree, 0, false, true)?;
|
||||
|
||||
self.export_context = None;
|
||||
|
||||
self.surfaces
|
||||
.flush_and_submit(&mut self.gpu_state, target_surface);
|
||||
|
||||
Ok(self.surfaces.snapshot(target_surface))
|
||||
}
|
||||
|
||||
/// Release any snapshots created by `prepare_drag_layers`.
|
||||
@ -2061,48 +2227,41 @@ impl RenderState {
|
||||
}
|
||||
|
||||
/// SVG-style layered render path used while the interactive transform is
|
||||
/// active. Instead of walking the shape tree, we:
|
||||
/// active. Three pre-captured images are composited on the Target each
|
||||
/// frame:
|
||||
///
|
||||
/// 1. Paint the persistent atlas as a static backdrop.
|
||||
/// 2. For every snapshot in `drag_layers`, concat the shape's modifier
|
||||
/// matrix on the Target canvas and blit the image into its original
|
||||
/// document-space rect. Because the canvas is already in document
|
||||
/// space (zoom + pan applied), this produces the correct screen-space
|
||||
/// position with a single draw call per layer and no rasterization.
|
||||
/// 3. Flush the Target.
|
||||
/// 1. `backdrop` — the scene without the selected shapes. Replaces
|
||||
/// the "atlas + ghost killer" trick: no ghost silhouette, and the
|
||||
/// shapes that were originally below the selection are preserved.
|
||||
/// 2. `layers` — one image per selected shape, drawn with its current
|
||||
/// modifier matrix concatenated on the canvas. Because
|
||||
/// `source_doc_rect` is in document space, a single
|
||||
/// `draw_image_rect` per layer is enough.
|
||||
/// 3. `overlay` — the shapes that sit above the selection, captured
|
||||
/// on a transparent background. Drawn last so they keep appearing
|
||||
/// in front of the dragged shape, matching the original z-order.
|
||||
///
|
||||
/// This is the same trick the browser uses when it composes a dragged
|
||||
/// SVG node: the raster never changes during the gesture, only the
|
||||
/// transform applied to it.
|
||||
/// This is the same trick the browser uses when it composites a dragged
|
||||
/// SVG node: the rasters never change during the gesture, only the
|
||||
/// transform applied to them.
|
||||
pub fn render_drag_layered(&mut self, tree: ShapesPoolRef) -> Result<()> {
|
||||
let _start = performance::begin_timed_log!("render_drag_layered");
|
||||
performance::begin_measure!("render_drag_layered");
|
||||
|
||||
self.reset_canvas();
|
||||
|
||||
// Step 1: stable backdrop from the persistent atlas. If the atlas
|
||||
// has not been populated yet we fall through and the target stays
|
||||
// filled with the background color, which is visually acceptable
|
||||
// for the very first drag frame and will be corrected by the
|
||||
// normal render that runs on `set_modifiers_end`.
|
||||
if self.surfaces.has_atlas() {
|
||||
self.surfaces.draw_atlas_to_target(
|
||||
self.viewbox,
|
||||
self.options.dpr(),
|
||||
self.background_color,
|
||||
);
|
||||
}
|
||||
|
||||
// Step 2: compose the drag layers on top.
|
||||
let dpr = self.options.dpr();
|
||||
let zoom = self.viewbox.zoom * dpr;
|
||||
let pan = (self.viewbox.pan_x, self.viewbox.pan_y);
|
||||
let sampling = self.sampling_options;
|
||||
let background_color = self.background_color;
|
||||
let viewport_rect = self.drag_layers.viewport_rect;
|
||||
|
||||
// Collect the data we need up-front: extracting both an immutable
|
||||
// borrow of `self.drag_layers` and a mutable borrow of
|
||||
// `self.surfaces` at the same time would fight the borrow checker.
|
||||
let backdrop_image = self.drag_layers.backdrop.clone();
|
||||
let overlay_image = self.drag_layers.overlay.clone();
|
||||
let draws: Vec<(skia::Image, Rect, Option<Matrix>)> = self
|
||||
.drag_layers
|
||||
.iter()
|
||||
@ -2116,32 +2275,39 @@ impl RenderState {
|
||||
canvas.save();
|
||||
canvas.reset_matrix();
|
||||
// Configure document space on the target canvas: zoom + pan.
|
||||
// After this, drawing at `source_doc_rect` lands where the shape
|
||||
// originally was; the modifier matrix (concat below) moves it.
|
||||
// After this, drawing at `viewport_rect` / `source_doc_rect` lands
|
||||
// where the original pixels were; the modifier matrix (concat
|
||||
// below) moves the selection on top of that.
|
||||
canvas.scale((zoom, zoom));
|
||||
canvas.translate(pan);
|
||||
|
||||
// Erase the "ghost" of the selected shapes from the atlas backdrop.
|
||||
// The atlas still has them at their pre-drag position; if we just
|
||||
// composed the modifier-transformed layers on top, the user would
|
||||
// see both the original silhouette (from the atlas) and the moved
|
||||
// copy (from the layer). Paint the original extrects with the
|
||||
// background color so the area reads as empty canvas.
|
||||
//
|
||||
// NOTE: this trades visual correctness for simplicity. If a shape
|
||||
// lies on top of other shapes, the covered area will briefly show
|
||||
// background instead of the underlying shapes during the gesture.
|
||||
// A follow-up can replace this with an atlas re-render of those
|
||||
// tiles with the selected shapes marked hidden.
|
||||
let mut clear_paint = skia::Paint::default();
|
||||
clear_paint.set_color(background_color);
|
||||
clear_paint.set_style(skia::PaintStyle::Fill);
|
||||
clear_paint.set_anti_alias(false);
|
||||
for (_, dst, _) in &draws {
|
||||
canvas.draw_rect(*dst, &clear_paint);
|
||||
// Background fill: if the backdrop snapshot fails or the scene was
|
||||
// empty, the viewport still reads as the workspace color instead
|
||||
// of a flash of the previous frame.
|
||||
let mut bg_paint = skia::Paint::default();
|
||||
bg_paint.set_color(background_color);
|
||||
bg_paint.set_style(skia::PaintStyle::Fill);
|
||||
bg_paint.set_anti_alias(false);
|
||||
if !viewport_rect.is_empty() {
|
||||
canvas.draw_rect(viewport_rect, &bg_paint);
|
||||
}
|
||||
|
||||
let paint = skia::Paint::default();
|
||||
|
||||
// Step 1: backdrop.
|
||||
if let Some(image) = backdrop_image {
|
||||
if !viewport_rect.is_empty() {
|
||||
canvas.draw_image_rect_with_sampling_options(
|
||||
&image,
|
||||
None,
|
||||
viewport_rect,
|
||||
sampling,
|
||||
&paint,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Step 2: transformed selection layers.
|
||||
for (image, dst, modifier) in draws {
|
||||
canvas.save();
|
||||
if let Some(m) = modifier {
|
||||
@ -2157,6 +2323,19 @@ impl RenderState {
|
||||
canvas.restore();
|
||||
}
|
||||
|
||||
// Step 3: overlay (shapes that were above the selection).
|
||||
if let Some(image) = overlay_image {
|
||||
if !viewport_rect.is_empty() {
|
||||
canvas.draw_image_rect_with_sampling_options(
|
||||
&image,
|
||||
None,
|
||||
viewport_rect,
|
||||
sampling,
|
||||
&paint,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
canvas.restore();
|
||||
|
||||
// Keep parity with the normal render loop: UI overlay is drawn
|
||||
|
||||
@ -33,11 +33,36 @@ pub struct DragLayer {
|
||||
/// "composited preview" during interactive transforms.
|
||||
///
|
||||
/// When `active` is true, the render loop short-circuits the full tile
|
||||
/// rendering pipeline and instead uses the cached images inside `layers`
|
||||
/// to present a frame: this matches what a browser does with SVG shapes
|
||||
/// during a drag (raster once, transform the raster).
|
||||
/// rendering pipeline and instead uses three pre-captured images to compose
|
||||
/// every frame:
|
||||
///
|
||||
/// * `backdrop` — the workspace rendered with the selected shapes hidden.
|
||||
/// Sits behind everything and already has the correct pixels for the
|
||||
/// "hole" left by the dragged shapes, so we don't need to erase any
|
||||
/// ghost silhouette.
|
||||
/// * `layers` — per-shape snapshots of the selected shapes in isolation.
|
||||
/// Each one is drawn with its current modifier matrix on top of the
|
||||
/// backdrop.
|
||||
/// * `overlay` — the shapes that sit above the (topmost) selected shape
|
||||
/// in the z-order, captured on a transparent background. Drawing it
|
||||
/// last restores the original stacking: elements that were in front of
|
||||
/// the dragged shape keep being in front during the gesture.
|
||||
///
|
||||
/// All three images are in document space and share the viewbox rect, so
|
||||
/// compositing is `canvas.draw_image_rect(_, viewbox.area)` after setting
|
||||
/// the canvas to document coordinates.
|
||||
pub struct DragLayers {
|
||||
pub layers: Vec<DragLayer>,
|
||||
/// Scene snapshot without the selected shapes. Drawn first each frame.
|
||||
pub backdrop: Option<Image>,
|
||||
/// Snapshot of the shapes that are above the selection on a transparent
|
||||
/// background. Drawn last to preserve z-order. `None` when there is
|
||||
/// nothing above the selection, in which case the overlay step is
|
||||
/// skipped.
|
||||
pub overlay: Option<Image>,
|
||||
/// Document-space rectangle both `backdrop` and `overlay` cover. Matches
|
||||
/// the viewbox area at the moment `prepare` was called.
|
||||
pub viewport_rect: Rect,
|
||||
/// Toggled on by `prepare` once snapshots are captured, and off again by
|
||||
/// `clear`. The render loop checks this flag before every frame to pick
|
||||
/// between the layered fast path and the normal tile pipeline.
|
||||
@ -48,12 +73,18 @@ impl DragLayers {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
layers: Vec::new(),
|
||||
backdrop: None,
|
||||
overlay: None,
|
||||
viewport_rect: Rect::new_empty(),
|
||||
active: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clear(&mut self) {
|
||||
self.layers.clear();
|
||||
self.backdrop = None;
|
||||
self.overlay = None;
|
||||
self.viewport_rect = Rect::new_empty();
|
||||
self.active = false;
|
||||
}
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
use std::collections::HashMap;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::iter;
|
||||
|
||||
use crate::performance;
|
||||
@ -211,6 +211,62 @@ impl ShapesPoolImpl {
|
||||
self.modified_shape_cache.clear()
|
||||
}
|
||||
|
||||
/// Returns every uuid registered in the pool, in arbitrary order.
|
||||
///
|
||||
/// Used by the drag-layer pipeline to toggle `Shape::hidden` in bulk
|
||||
/// when rendering the backdrop / overlay snapshots.
|
||||
pub fn all_ids(&self) -> Vec<Uuid> {
|
||||
self.uuid_to_idx.keys().copied().collect()
|
||||
}
|
||||
|
||||
/// Collect every shape that sits visually above any of `selected` in
|
||||
/// the tree-render order. Currently we approximate "above" as
|
||||
/// "siblings that come after a selected shape in its parent's child
|
||||
/// list, plus all of their descendants". This matches the common case
|
||||
/// (dragging a shape inside a frame) and, because the overlay is just
|
||||
/// an extra compositing pass, anything we miss simply degrades back to
|
||||
/// the current visual — it never corrupts the atlas.
|
||||
pub fn collect_above_of(&self, selected: &HashSet<Uuid>) -> HashSet<Uuid> {
|
||||
let mut above: HashSet<Uuid> = HashSet::new();
|
||||
|
||||
for sel_id in selected {
|
||||
let Some(shape) = self.get(sel_id) else {
|
||||
continue;
|
||||
};
|
||||
let Some(parent_id) = shape.parent_id else {
|
||||
continue;
|
||||
};
|
||||
let Some(parent) = self.get(&parent_id) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
// `children` stores siblings in paint order (first = behind,
|
||||
// last = in front), so anything after `sel_id` is above it.
|
||||
let Some(pos) = parent.children.iter().position(|c| c == sel_id) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
for sibling_id in parent.children.iter().skip(pos + 1) {
|
||||
if selected.contains(sibling_id) {
|
||||
// Another selection also participates in the drag. Its
|
||||
// own layer will be composed on top, so treat it as
|
||||
// "part of the selected set" rather than overlay.
|
||||
continue;
|
||||
}
|
||||
above.insert(*sibling_id);
|
||||
if let Some(sibling) = self.get(sibling_id) {
|
||||
for descendant in sibling.all_children_iter(self, false, false) {
|
||||
if !selected.contains(&descendant) {
|
||||
above.insert(descendant);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
above
|
||||
}
|
||||
|
||||
/// Returns the raw modifier matrix currently applied to `id`, if any.
|
||||
///
|
||||
/// The `get` method above already returns a shape with modifiers baked in,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user