Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fdb7d7a183 | ||
|
|
99671cabd8 |
No files matched your search
@@ -52,6 +52,13 @@ impl RasterFx {
|
||||
pub fn is_below(&self) -> bool {
|
||||
matches!(self, RasterFx::DropShadow { .. } | RasterFx::OuterGlow { .. })
|
||||
}
|
||||
/// The blend mode it paints with (shadows and glows).
|
||||
pub fn mode(&self) -> Option<BlendMode> {
|
||||
match self {
|
||||
RasterFx::DropShadow { mode, .. } | RasterFx::OuterGlow { mode, .. } | RasterFx::InnerGlow { mode, .. } => Some(*mode),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
/// How far this effect reaches beyond `bounds`, what it paints around (Radial Blur reaches
|
||||
/// further around larger objects).
|
||||
pub fn outset(&self, bounds: Rect) -> f64 {
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use serde_json::{Map, Value, json};
|
||||
use vectorcraft_color::Paint;
|
||||
use vectorcraft_color::{BlendMode, Paint};
|
||||
use vectorcraft_doc::rastersettings::MAX_ADD_AROUND;
|
||||
use vectorcraft_doc::{
|
||||
Appearance, AppearanceItem, ColorMode, Document, Effect, ImageBlob, ImageObject, Node, NodeId, NodeKind, RasterColorModel, StrokeLayer,
|
||||
@@ -217,18 +217,55 @@ pub(crate) fn raster_fx(n: &Node) -> Vec<RasterFx> {
|
||||
/// Remove the raster effects of `n` (its own and its fills' and strokes') and, with `members`, of
|
||||
/// everything inside it.
|
||||
pub(crate) fn strip_raster(n: &mut Node, members: bool) {
|
||||
let keep = |e: &Effect| !effects::is_raster(&e.id);
|
||||
retain_effects(n, members, &|e| !effects::is_raster(&e.id));
|
||||
}
|
||||
|
||||
/// Keep the effects of `n` (its own and its fills' and strokes') that `keep` accepts and, with
|
||||
/// `members`, of everything inside it.
|
||||
fn retain_effects(n: &mut Node, members: bool, keep: &dyn Fn(&Effect) -> bool) {
|
||||
n.appearance.effects.retain(keep);
|
||||
for it in &mut n.appearance.items {
|
||||
it.effects_mut().retain(keep);
|
||||
}
|
||||
if members {
|
||||
for c in n.children_mut().into_iter().flatten() {
|
||||
strip_raster(Arc::make_mut(c), true);
|
||||
retain_effects(Arc::make_mut(c), true, keep);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The blend mode raster effect `e` paints with (`None`: not a raster effect, or one without).
|
||||
fn effect_mode(e: &Effect) -> Option<BlendMode> {
|
||||
effects::raster_effects(std::slice::from_ref(e)).first().and_then(RasterFx::mode)
|
||||
}
|
||||
|
||||
/// The images of the shadows and outer glows of `n`, one per blend mode they use (in the order
|
||||
/// the modes first come), each composited with its mode: rendered alone, over nothing, they can't
|
||||
/// blend with the art below the object, so the image does.
|
||||
fn below_images(d: &mut Document, n: &Node) -> Option<Vec<Node>> {
|
||||
let mut modes: Vec<BlendMode> = vec![];
|
||||
for mode in raster_fx(n).iter().filter_map(RasterFx::mode) {
|
||||
if !modes.contains(&mode) {
|
||||
modes.push(mode);
|
||||
}
|
||||
}
|
||||
if modes.len() < 2 {
|
||||
let mut image = raster_image(d, n, true, false)?;
|
||||
image.blend = modes.first().copied().unwrap_or_default();
|
||||
return Some(vec![image]);
|
||||
}
|
||||
modes
|
||||
.into_iter()
|
||||
.map(|mode| {
|
||||
let mut only = n.clone();
|
||||
retain_effects(&mut only, false, &|e| effect_mode(e).is_none_or(|m| m == mode));
|
||||
let mut image = raster_image(d, &only, true, false)?;
|
||||
image.blend = mode;
|
||||
Some(image)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// The raster effects of `n` (its own, its fills' and strokes') as an embedded image of `d`
|
||||
/// rendered at the document's raster effects resolution, without `n`'s transparency (that stays
|
||||
/// on the object). With `below` (they all paint below it: shadows and outer glows) the image holds
|
||||
@@ -405,15 +442,18 @@ fn walk(out: &mut Document, n: &Node, which: Which) -> Option<Node> {
|
||||
return Some(walk(out, &g, which).unwrap_or(g));
|
||||
}
|
||||
// An image that can't be made leaves the object as it is (the writer reports its effects).
|
||||
let image = raster_image(out, n, below, false)?;
|
||||
let mut v = n.clone();
|
||||
strip_raster(&mut v, false);
|
||||
let mut m = if below {
|
||||
let images = below_images(out, n)?;
|
||||
let mut m = walk(out, &v, which).unwrap_or(v);
|
||||
put_below(out, &mut m, image);
|
||||
// Each goes in first: the last one put is the lowest.
|
||||
for image in images.into_iter().rev() {
|
||||
put_below(out, &mut m, image);
|
||||
}
|
||||
m
|
||||
} else {
|
||||
replace_with_image(v, image)
|
||||
replace_with_image(v, raster_image(out, n, false, false)?)
|
||||
};
|
||||
walk_mask(out, &mut m, which);
|
||||
Some(m)
|
||||
|
||||
@@ -150,3 +150,35 @@ fn live_objects_and_single_fills_and_strokes_keep_their_raster_effects() {
|
||||
assert_eq!(pieces, 2, "item {item}: one piece per fill and stroke");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shadows_and_glows_keep_their_blend_modes() {
|
||||
// #827: a red Screen glow over grey can't darken it, as Normal would.
|
||||
let glow = Effect {
|
||||
id: "stylize.outerGlow".into(),
|
||||
params: json!({"mode": "screen", "color": "#ff0000", "opacity": 100.0, "blur": 20.0}),
|
||||
visible: true,
|
||||
};
|
||||
let mut d = Document::new(200.0, 200.0);
|
||||
let back = rect(&mut d, Rect::new(0.0, 0.0, 200.0, 200.0), Color::rgb(0.5, 0.5, 0.5));
|
||||
add(&mut d, back);
|
||||
let dot = rect(&mut d, Rect::new(70.0, 70.0, 130.0, 130.0), Color::rgb(0.12, 0.12, 0.12));
|
||||
add(&mut d, with(dot, glow.clone()));
|
||||
assert_written("screen glow", &d);
|
||||
let r = crate::cmd::rasterfx::export_pdf_with_report(&d, &Default::default()).unwrap();
|
||||
let back = render(&vectorcraft_pdf::import(&r.bytes).unwrap());
|
||||
for x in [133, 138, 145] {
|
||||
let i = (100 * back.width as usize + x) * 4;
|
||||
let (red, green, blue) = (back.pixels[i], back.pixels[i + 1], back.pixels[i + 2]);
|
||||
assert!(red > green && green >= 124 && blue >= 124, "x {x}: {:?}", (red, green, blue));
|
||||
}
|
||||
|
||||
// A Multiply shadow and the Screen glow: an image each, composited with its mode.
|
||||
let mut d = Document::new(200.0, 200.0);
|
||||
let dot = rect(&mut d, Rect::new(70.0, 70.0, 130.0, 130.0), Color::rgb(0.12, 0.12, 0.12));
|
||||
add(&mut d, with(with(dot, shadow()), glow));
|
||||
let flat = crate::flatten_raster_effects(&d).unwrap();
|
||||
let modes: Vec<_> =
|
||||
flat.layers[0].children().unwrap()[0].children().unwrap().iter().filter(|c| matches!(c.kind, NodeKind::Image(_))).map(|c| c.blend).collect();
|
||||
assert_eq!(modes, [vectorcraft_color::BlendMode::Multiply, vectorcraft_color::BlendMode::Screen]);
|
||||
}
|
||||
@@ -71,6 +71,8 @@ pub struct SelectionTool {
|
||||
targets: Option<Targets>,
|
||||
moving: Option<MoveSnap>,
|
||||
guide: GuideEdit,
|
||||
/// Smart Guides: the centre or anchor under the pointer, and its label ([`Self::hover_point`]).
|
||||
hover: Option<(Point, &'static str)>,
|
||||
}
|
||||
|
||||
/// Snapping for the selection moved as a whole, its targets gathered when the move begins: View →
|
||||
@@ -163,10 +165,36 @@ fn box_hit(cx: &ToolContext, b: &OrientedBox, p: Point) -> Option<BoxHit> {
|
||||
in_rotate_zone(b.rect, lp, tol, cx.tol(18.0)).map(|_| BoxHit::Rotate)
|
||||
}
|
||||
|
||||
/// How near (screen points) the pointer must come to an object's centre or anchor for Smart Guides
|
||||
/// to label it while hovering.
|
||||
const HOVER_REACH: f64 = 6.0;
|
||||
|
||||
impl SelectionTool {
|
||||
fn drag_threshold(cx: &ToolContext) -> f64 {
|
||||
cx.tol(3.0)
|
||||
}
|
||||
|
||||
/// With Smart Guides on, the anchor or the centre of an object near `p` (the one under the
|
||||
/// pointer, else a selected one) and its label, "anchor" or "center", as Illustrator shows
|
||||
/// them while hovering (#812). A press there grabs the object by that point.
|
||||
fn hover_point(cx: &ToolContext, p: Point) -> Option<(Point, &'static str)> {
|
||||
if !cx.smart_guides {
|
||||
return None;
|
||||
}
|
||||
let reach = cx.tol(HOVER_REACH);
|
||||
let under = hit_test(cx.doc, p, cx.hit_options()).map(|h| h.top_object(cx.isolation));
|
||||
let nearest = |points: &mut dyn Iterator<Item = Point>| {
|
||||
points.filter(|q| q.distance(p) <= reach).min_by(|a, b| a.distance(p).total_cmp(&b.distance(p)))
|
||||
};
|
||||
under.into_iter().chain(cx.selection.objects.iter().copied()).find_map(|id| {
|
||||
let n = cx.doc.node(id)?;
|
||||
let anchor = n.path_data().and_then(|pd| nearest(&mut pd.subpaths.iter().flat_map(|sp| sp.anchors.iter().map(|a| a.p))));
|
||||
anchor.map(|q| (q, "anchor")).or_else(|| {
|
||||
let c = cx.doc.bounds_of(&[id], cx.preview_bounds)?.center();
|
||||
(c.distance(p) <= reach).then_some((c, "center"))
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Tool for SelectionTool {
|
||||
@@ -188,6 +216,11 @@ impl Tool for SelectionTool {
|
||||
}
|
||||
let p = ev.pos;
|
||||
let m = ev.mods;
|
||||
// A press near the hovered centre or anchor grabs the object by it.
|
||||
let grab = self.hover.take().map(|(q, _)| q).filter(|q| q.distance(p) <= cx.tol(HOVER_REACH)).unwrap_or(p);
|
||||
if matches!(ev.kind, PointerKind::Move) && matches!(self.state, State::Idle) {
|
||||
self.hover = Self::hover_point(cx, p);
|
||||
}
|
||||
match (ev.kind, self.state.clone()) {
|
||||
(PointerKind::DoubleClick, _) => {
|
||||
self.state = State::Idle;
|
||||
@@ -278,7 +311,7 @@ impl Tool for SelectionTool {
|
||||
} else if cx.selection.objects.len() > 1 && !m.cmd && !m.alt {
|
||||
key = Some(top);
|
||||
}
|
||||
self.state = State::Moving { start: p, began: false, deselect, key };
|
||||
self.state = State::Moving { start: grab, began: false, deselect, key };
|
||||
out
|
||||
}
|
||||
None => {
|
||||
@@ -397,6 +430,12 @@ impl Tool for SelectionTool {
|
||||
}
|
||||
o.extend(self.guides.iter().cloned());
|
||||
o.extend(self.guide.overlays(cx));
|
||||
if let (Some((q, label)), State::Idle) = (self.hover, &self.state) {
|
||||
let style = crate::guides::GuideStyle::of(cx);
|
||||
if style.labels {
|
||||
o.push(Overlay::Label { p: q, text: label.into(), color: style.color });
|
||||
}
|
||||
}
|
||||
if let Some((p, t)) = &self.measure {
|
||||
o.push(Overlay::Measure { p: *p, text: t.clone() });
|
||||
}
|
||||
@@ -483,6 +522,39 @@ mod tests {
|
||||
PointerEvent::new(kind, x, y)
|
||||
}
|
||||
|
||||
/// #812: with Smart Guides on, hovering near an object's centre or an anchor labels it, and a
|
||||
/// press there grabs the object by that point.
|
||||
#[test]
|
||||
fn hovering_labels_the_centre_and_anchors_and_a_press_grabs_by_them() {
|
||||
let (d, _) = doc_with_rect();
|
||||
let s = Selection::default();
|
||||
let p = paint();
|
||||
let cx = cx(&d, &s, &p);
|
||||
let label = |t: &SelectionTool| {
|
||||
t.overlays(&cx).iter().find_map(|o| match o {
|
||||
Overlay::Label { p, text, .. } => Some((*p, text.clone())),
|
||||
_ => None,
|
||||
})
|
||||
};
|
||||
let mut t = SelectionTool::default();
|
||||
t.pointer(&cx, &ev(PointerKind::Move, 152.0, 151.0));
|
||||
assert_eq!(label(&t), Some((Point::new(150.0, 150.0), "center".to_string())));
|
||||
t.pointer(&cx, &ev(PointerKind::Move, 102.0, 101.0));
|
||||
assert_eq!(label(&t), Some((Point::new(100.0, 100.0), "anchor".to_string())));
|
||||
t.pointer(&cx, &ev(PointerKind::Move, 130.0, 170.0));
|
||||
assert_eq!(label(&t), None, "away from both");
|
||||
// Grabbed by its centre, the object's centre follows the pointer.
|
||||
t.pointer(&cx, &ev(PointerKind::Move, 152.0, 151.0));
|
||||
t.pointer(&cx, &ev(PointerKind::Down, 152.0, 151.0));
|
||||
let a = t.pointer(&cx, &ev(PointerKind::Drag, 250.0, 250.0));
|
||||
assert!(a.iter().any(|a| matches!(a, Action::Preview(_, v) if v["matrix"][4] == 100.0 && v["matrix"][5] == 100.0)), "{a:?}");
|
||||
// With Smart Guides off, nothing is labelled.
|
||||
let off = ToolContext { smart_guides: false, ..cx };
|
||||
let mut t = SelectionTool::default();
|
||||
t.pointer(&off, &ev(PointerKind::Move, 152.0, 151.0));
|
||||
assert!(!t.overlays(&off).iter().any(|o| matches!(o, Overlay::Label { .. })));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn click_selects_object() {
|
||||
let (d, id) = doc_with_rect();
|
||||
|
||||
Reference in new issue
Block a user