Files
vectorcraft/crates/effects/src/marks.rs
T
arhammusheer 5beac4f122 #1244: smaller objects: Node 352 -> 152 bytes, NodeKind 200 -> 80, and a cheaper undo estimate
Rarely set fields move into one optional boxed NodeExtras, and large NodeKind payloads (images,
live shapes, blends, envelopes) are boxed. Every object, and every undo step that copies it,
shrinks: a 50k-path move keeps ~24.5 MB per step instead of ~40 MB. Files stay byte-identical
(Node keeps its flat JSON through a borrowed Serialize and a from-wire Deserialize, no flatten),
and loading is slightly faster. Undo and redo reuse their step's size estimate instead of
walking the document again (8.3 -> 5.6 ms for a 50k-path move).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VmMmc1J8XeNLxhRihWv9cc

# Conflicts:
#	crates/engine/src/cmd/graph.rs
#	crates/svg/src/export.rs
2026-10-12 11:02:32 +07:00

71 lines
3.3 KiB
Rust

//! Effect → Crop Marks: live trim marks in the Registration colour around the object, which follow
//! it as it moves or changes size. The renderer, the exporters ([`crate::bake_document`]) and
//! Expand Appearance draw the object as [`crop_marks_art`].
use std::sync::Arc;
use vectorcraft_doc::Node;
use vectorcraft_doc::marks::{MarkStyle, TrimMarks};
/// The effect id.
pub const CROP_MARKS: &str = "cropMarks";
/// Does `n` carry a visible Crop Marks effect?
pub fn has_crop_marks(n: &Node) -> bool {
n.appearance.effects.iter().any(|e| e.visible && e.id == CROP_MARKS)
}
/// The art of an object with Crop Marks: a group (with the object's id, name and transparency) of
/// the object itself, without the effect, and the marks around its visual bounds (the last Crop
/// Marks effect's style). `None` without a visible Crop Marks effect.
pub fn crop_marks_art(n: &Node) -> Option<Node> {
let fx = n.appearance.effects.iter().rev().find(|e| e.visible && e.id == CROP_MARKS)?;
let style = fx.params.get("style").and_then(|v| v.as_str()).and_then(MarkStyle::parse).unwrap_or_default();
let bounds = n.visual_bounds()?;
let marks = TrimMarks::of(style).group(bounds, &mut || n.id);
let mut object = n.clone();
object.appearance.effects.retain(|e| e.id != CROP_MARKS);
// The group takes over the object's identity, transparency, mask and link.
let mut g = Node::group(n.id, vec![]);
(g.visible, g.locked) = (n.visible, n.locked);
g.name = object.name.take();
g.opacity = std::mem::replace(&mut object.opacity, 1.0);
g.blend = std::mem::take(&mut object.blend);
g.isolate = std::mem::take(&mut object.isolate);
g.extras_mut().mask = object.extras_mut().mask.take();
g.extras_mut().attrs = object.extras_mut().attrs.take();
if let Some(ch) = g.children_mut() {
*ch = vec![Arc::new(object), Arc::new(marks)];
}
Some(g)
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
use vectorcraft_doc::{Appearance, Effect, NodeId};
use vectorcraft_geom::{Rect, shapes};
#[test]
fn the_marks_surround_the_object_and_follow_it() {
let mut n = Node::path(NodeId(7), shapes::rectangle(Rect::new(0.0, 0.0, 100.0, 50.0)), Appearance::default_art());
assert!(crop_marks_art(&n).is_none());
n.opacity = 0.5;
n.appearance.effects.push(Effect { id: CROP_MARKS.into(), params: json!({"style": "japanese"}), visible: true });
let art = crop_marks_art(&n).unwrap();
assert_eq!((art.id, art.opacity), (NodeId(7), 0.5));
let [object, marks] = art.children().unwrap().as_slice() else { panic!() };
assert!(object.appearance.effects.is_empty() && object.opacity == 1.0);
assert_eq!(marks.children().unwrap().len(), 24, "Japanese marks");
// Around the stroked bounds (0.5 outside the path), by the marks' reach.
let reach = TrimMarks::of(MarkStyle::Japanese).reach() + 0.5;
let b = art.visual_bounds().unwrap();
assert!((b.x0 + reach).abs() < 2.0 && (b.x1 - 100.0 - reach).abs() < 2.0, "{b:?}");
// Moved, the marks move along.
n.transform(vectorcraft_geom::Affine::translate((40.0, 0.0)), false);
let b2 = crop_marks_art(&n).unwrap().visual_bounds().unwrap();
assert!((b2.x0 - b.x0 - 40.0).abs() < 1e-6);
}
}