mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-28 19:57:05 -06:00
Merge pull request #878 from storytold/fix/833-isolation-locks-outside
Fix #833: isolation mode locks and dims the art around the group
This commit is contained in:
+20
-5
@@ -86,6 +86,19 @@ pub fn objects_at(doc: &Document, p: Point, opt: HitOptions, scope: Option<NodeI
|
||||
|
||||
/// [`hit_test`] passing over the nodes `skip` names (and their contents).
|
||||
fn hit_test_skipping(doc: &Document, p: Point, opt: HitOptions, skip: &dyn Fn(NodeId) -> bool) -> Option<Hit> {
|
||||
let finish = |mut h: Hit| {
|
||||
h.contents_of = h.ancestry.iter().rev().copied().find(|a| doc.node(*a).is_some_and(edits_contents));
|
||||
h.layers = h.ancestry.iter().take_while(|a| doc.node(**a).is_some_and(Node::is_layer)).count();
|
||||
h
|
||||
};
|
||||
// Isolation mode: only what is inside the isolated container hits. The art around it is
|
||||
// locked, and clicks pass through it (#833).
|
||||
if let Some(scope) = opt.scope
|
||||
&& let Some(container) = doc.node(scope).filter(|n| n.is_container())
|
||||
&& let Some(mut chain) = doc.ancestry(scope)
|
||||
{
|
||||
return hit_children(container, p, opt, skip, &mut chain).map(finish);
|
||||
}
|
||||
let mut chain = Vec::new();
|
||||
for layer in doc.layers.iter().rev() {
|
||||
if !layer.visible || layer.locked {
|
||||
@@ -100,10 +113,8 @@ fn hit_test_skipping(doc: &Document, p: Point, opt: HitOptions, skip: &dyn Fn(No
|
||||
}
|
||||
chain.push(layer.id);
|
||||
let opt = if matches!(layer.kind, NodeKind::Layer { preview: false, .. }) { HitOptions { outline: true, ..opt } } else { opt };
|
||||
if let Some(mut h) = hit_children(layer, p, opt, skip, &mut chain) {
|
||||
h.contents_of = h.ancestry.iter().rev().copied().find(|a| doc.node(*a).is_some_and(edits_contents));
|
||||
h.layers = h.ancestry.iter().take_while(|a| doc.node(**a).is_some_and(Node::is_layer)).count();
|
||||
return Some(h);
|
||||
if let Some(h) = hit_children(layer, p, opt, skip, &mut chain) {
|
||||
return Some(finish(h));
|
||||
}
|
||||
chain.pop();
|
||||
}
|
||||
@@ -560,7 +571,11 @@ world",
|
||||
d.insert(Some(l), 9, far).unwrap();
|
||||
let at = |x: f64, scope| objects_at(&d, Point::new(x, 25.0), HitOptions::default(), scope);
|
||||
assert_eq!(at(30.0, None), vec![g, a_id]);
|
||||
assert_eq!(at(30.0, Some(g))[..2], [c_id, b_id], "isolated: the group's own objects");
|
||||
assert_eq!(at(30.0, Some(g)), [c_id, b_id], "isolated: the group's own objects");
|
||||
// Isolated, the art around the group is locked: clicks pass through it (#833).
|
||||
assert!(at(5.0, Some(g)).is_empty());
|
||||
assert!(at(130.0, Some(g)).is_empty());
|
||||
assert!(hit_test(&d, Point::new(5.0, 25.0), HitOptions { scope: Some(g), ..HitOptions::default() }).is_none());
|
||||
assert_eq!(at(5.0, None), vec![a_id]);
|
||||
assert!(at(90.0, None).is_empty());
|
||||
}
|
||||
|
||||
@@ -107,8 +107,14 @@ pub struct RenderOptions {
|
||||
/// Images are sampled smoothly when scaled or rotated; off, each pixel takes its nearest image
|
||||
/// pixel (Pixel Preview with File Handling › Display Bitmaps as Anti-aliased Images off).
|
||||
pub smooth_images: bool,
|
||||
/// Screen view in isolation mode: the isolated group or layer. Everything around it draws
|
||||
/// dimmed ([`ISOLATION_DIM`]).
|
||||
pub isolated: Option<NodeId>,
|
||||
}
|
||||
|
||||
/// The opacity of the art around an isolated group or layer.
|
||||
pub const ISOLATION_DIM: f32 = 0.5;
|
||||
|
||||
/// How edges are rasterized (raster export option).
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub enum AntiAlias {
|
||||
@@ -174,6 +180,7 @@ impl Default for RenderOptions {
|
||||
progressive_placed: false,
|
||||
trace_views: false,
|
||||
smooth_images: true,
|
||||
isolated: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -355,6 +362,11 @@ pub struct Renderer {
|
||||
/// Inline graphics being drawn inside inline graphics (a symbol whose art holds text showing
|
||||
/// it): drawing stops at [`MAX_INLINE_DEPTH`].
|
||||
inline_depth: u32,
|
||||
/// [`RenderOptions::isolated`]'s layers and groups, from the top layer down to it, for the
|
||||
/// frame (`stamp`) and document they were found in.
|
||||
isolation: Option<(u64, usize, NodeId, Vec<NodeId>)>,
|
||||
/// Drawing inside the isolated container or a dimmed object: nothing further dims.
|
||||
isolation_settled: bool,
|
||||
}
|
||||
|
||||
/// How deep inline graphics nest (text in a symbol shown inline in text…) before they draw nothing.
|
||||
@@ -441,6 +453,8 @@ impl Renderer {
|
||||
adjusted: Default::default(),
|
||||
dim_images: None,
|
||||
inline_depth: 0,
|
||||
isolation: None,
|
||||
isolation_settled: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -876,6 +890,46 @@ impl Renderer {
|
||||
|
||||
/// [`Self::draw_node`] after culling and Layer Options.
|
||||
fn draw_layer_node(&mut self, ctx: &mut RenderContext, f: &Frame, n: &Node) {
|
||||
// Isolation mode: the art around the isolated container draws dimmed (#833).
|
||||
if let Some((isolated, path)) = self.isolation_path(f) {
|
||||
// A container on the way down to it draws as usual: its members decide.
|
||||
if path.contains(&n.id) && n.id != isolated {
|
||||
return self.draw_layer_node_now(ctx, f, n);
|
||||
}
|
||||
self.isolation_settled = true;
|
||||
if n.id == isolated {
|
||||
self.draw_layer_node_now(ctx, f, n);
|
||||
} else {
|
||||
self.dimmed(ctx, f, &mut |r, c, fr| r.draw_layer_node_now(c, fr, n));
|
||||
}
|
||||
self.isolation_settled = false;
|
||||
return;
|
||||
}
|
||||
self.draw_layer_node_now(ctx, f, n);
|
||||
}
|
||||
|
||||
/// In isolation mode, while drawing outside the isolated container and the art dimmed around
|
||||
/// it: the isolated container and its layers and groups, from the top layer down to it.
|
||||
fn isolation_path(&mut self, f: &Frame) -> Option<(NodeId, Vec<NodeId>)> {
|
||||
let isolated = f.opts.isolated.filter(|_| !self.isolation_settled)?;
|
||||
let doc = std::ptr::from_ref(f.doc) as usize;
|
||||
if !self.isolation.as_ref().is_some_and(|(stamp, d, id, _)| (*stamp, *d, *id) == (self.stamp, doc, isolated)) {
|
||||
self.isolation = Some((self.stamp, doc, isolated, f.doc.ancestry(isolated).unwrap_or_default()));
|
||||
}
|
||||
let path = self.isolation.as_ref().map(|(.., path)| path.clone()).filter(|p| !p.is_empty())?;
|
||||
Some((isolated, path))
|
||||
}
|
||||
|
||||
/// `draw` at [`ISOLATION_DIM`], with nothing inside dimmed again.
|
||||
fn dimmed(&mut self, ctx: &mut RenderContext, f: &Frame, draw: &mut group::Content) {
|
||||
let settled = std::mem::replace(&mut self.isolation_settled, true);
|
||||
let comp = Composite { opacity: ISOLATION_DIM, ..Default::default() };
|
||||
self.group(ctx, f, comp, draw);
|
||||
self.isolation_settled = settled;
|
||||
}
|
||||
|
||||
/// [`Self::draw_layer_node`] past isolation mode's dimming.
|
||||
fn draw_layer_node_now(&mut self, ctx: &mut RenderContext, f: &Frame, n: &Node) {
|
||||
if fx::has_object_fx(n) {
|
||||
return self.draw_object_fx(ctx, f, &Arc::new(n.clone()), false);
|
||||
}
|
||||
@@ -998,6 +1052,22 @@ impl Renderer {
|
||||
fn draw_children(&mut self, ctx: &mut RenderContext, f: &Frame, children: &[Arc<Node>], knockout: bool) {
|
||||
if knockout {
|
||||
self.draw_knockout(ctx, f, children);
|
||||
} else if let Some((_, path)) = self.isolation_path(f) {
|
||||
// Isolation mode, in a container on the way down to the isolated one: each run of
|
||||
// members around it draws dimmed as one (#833).
|
||||
let mut rest = children;
|
||||
while let Some(i) = rest.iter().position(|c| path.contains(&c.id)) {
|
||||
let (around, from) = rest.split_at(i);
|
||||
if !around.is_empty() {
|
||||
self.dimmed(ctx, f, &mut |r, c, fr| around.iter().for_each(|n| r.draw_arc(c, fr, n)));
|
||||
}
|
||||
let Some((on_path, after)) = from.split_first() else { break };
|
||||
self.draw_arc(ctx, f, on_path);
|
||||
rest = after;
|
||||
}
|
||||
if !rest.is_empty() {
|
||||
self.dimmed(ctx, f, &mut |r, c, fr| rest.iter().for_each(|n| r.draw_arc(c, fr, n)));
|
||||
}
|
||||
} else {
|
||||
for c in children {
|
||||
self.draw_arc(ctx, f, c);
|
||||
|
||||
@@ -218,3 +218,30 @@ fn copying_the_backdrop_changes_nothing_without_blending() {
|
||||
let worst = a.iter().zip(&b).map(|(x, y)| (*x as i32 - *y as i32).abs()).max().unwrap();
|
||||
assert!(worst <= 1, "differs by {worst}/255");
|
||||
}
|
||||
|
||||
/// Isolation mode (#833): the art around the isolated group draws at half opacity, above it as
|
||||
/// well as below; the group's own art as usual.
|
||||
#[test]
|
||||
fn isolation_mode_dims_the_art_around_the_isolated_group() {
|
||||
let mut d = Document::new(100.0, 100.0);
|
||||
let l = d.layers[0].id;
|
||||
let below = rect(&mut d, Rect::new(0.0, 0.0, 40.0, 40.0), [0.0, 0.0, 0.0]);
|
||||
let inside = rect(&mut d, Rect::new(60.0, 60.0, 100.0, 100.0), [0.0, 0.0, 0.0]);
|
||||
let above = rect(&mut d, Rect::new(60.0, 0.0, 100.0, 40.0), [0.0, 0.0, 0.0]);
|
||||
let g = group(&mut d, vec![inside], 1.0, Blend::Normal, false);
|
||||
let gid = g.id;
|
||||
for n in [below, g, above] {
|
||||
d.insert(Some(l), usize::MAX, n).unwrap();
|
||||
}
|
||||
let alpha = |img: &Rendered, x: usize, y: usize| img.pixels[(y * 100 + x) * 4 + 3];
|
||||
let plain = render(&d);
|
||||
let isolated = |id| render_with(&mut Renderer::new(), &d, &RenderOptions { isolated: Some(id), ..Default::default() });
|
||||
let dimmed = isolated(gid);
|
||||
assert_eq!(alpha(&dimmed, 80, 80), 255, "the isolated group's art");
|
||||
for (x, y) in [(20, 20), (80, 20)] {
|
||||
assert_eq!(alpha(&plain, x, y), 255);
|
||||
assert!((120..=135).contains(&alpha(&dimmed, x, y)), "({x}, {y}): {}", alpha(&dimmed, x, y));
|
||||
}
|
||||
// An object that isn't in the document dims nothing.
|
||||
assert_eq!(isolated(NodeId(u64::MAX)).pixels, plain.pixels);
|
||||
}
|
||||
|
||||
@@ -249,6 +249,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
placed: vectorcraft_render::placed_document::generation(),
|
||||
pixel,
|
||||
smooth_images,
|
||||
isolated: st.isolation.map(|id| id.0),
|
||||
};
|
||||
// Placed documents' bitmaps are being made: draw again when they are ready.
|
||||
if vectorcraft_render::placed_document::busy() {
|
||||
@@ -298,6 +299,8 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
progressive_placed: true,
|
||||
trace_views: true,
|
||||
smooth_images,
|
||||
// Isolation mode: the art around the isolated group or layer is dimmed (#833).
|
||||
isolated: key.isolated.map(vectorcraft_doc::NodeId),
|
||||
..opts
|
||||
};
|
||||
// Light documents render synchronously (no lag vs overlays); heavy ones go to the worker.
|
||||
|
||||
@@ -293,6 +293,8 @@ pub struct CacheKey {
|
||||
pub pixel: Option<[i64; 4]>,
|
||||
/// Images sampled smoothly ([`vectorcraft_render::RenderOptions::smooth_images`]).
|
||||
pub smooth_images: bool,
|
||||
/// Isolation mode's group or layer ([`vectorcraft_render::RenderOptions::isolated`]).
|
||||
pub isolated: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
|
||||
Reference in New Issue
Block a user