mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-14 19:26:56 -04:00
Fix #833: isolation mode locks and dims the art around the group
While a group or layer was isolated, clicks, hovers and drags still reached the art outside it, and that art drew as usual. Now only the isolated container's contents hit: the art around it is locked and clicks pass through it. On the canvas it draws at half opacity, above the group as well as below, each run of it as one dimmed layer. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
61d372bb7f
commit
b426859b15
5 files changed
+122
-5
No files matched your search
+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