#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
This commit is contained in:
parent
c4214be9d7
commit
5beac4f122
|
|
@ -220,7 +220,7 @@ fn deep(n: usize) -> Result<(), String> {
|
|||
let xf = Affine::translate((f64::from(i % 2) * 800.0, f64::from(i / 2) * 600.0)) * Affine::scale(800.0 / 6000.0);
|
||||
let image = vectorcraft_doc::ImageObject { key, width: 6000, height: 4000, xf, link: None, placement: Default::default() };
|
||||
let id = d.alloc_id();
|
||||
d.insert(l, usize::MAX, Node::new(id, vectorcraft_doc::NodeKind::Image(image))).map_err(|e| e.to_string())?;
|
||||
d.insert(l, usize::MAX, Node::new(id, vectorcraft_doc::NodeKind::Image(Box::new(image)))).map_err(|e| e.to_string())?;
|
||||
}
|
||||
note("4 embedded 24 MP photos");
|
||||
let mut renderers = [Renderer::new(), Renderer::new(), Renderer::new()];
|
||||
|
|
|
|||
|
|
@ -349,7 +349,7 @@ impl<'a> Scene<'a> {
|
|||
if n.blend != vectorcraft_color::BlendMode::Normal {
|
||||
self.warn("blending modes are left out");
|
||||
}
|
||||
if n.mask.as_ref().is_some_and(|m| !m.disabled) {
|
||||
if n.extras().mask.as_ref().is_some_and(|m| !m.disabled) {
|
||||
self.warn("opacity masks are left out");
|
||||
}
|
||||
let raster = |fx: &[vectorcraft_doc::Effect]| fx.iter().any(|e| e.visible && vectorcraft_effects::is_raster(&e.id));
|
||||
|
|
|
|||
|
|
@ -355,7 +355,7 @@ fn images_link_png_or_jpeg_files() {
|
|||
link: None,
|
||||
placement: Default::default(),
|
||||
};
|
||||
vec![Node::new(d.alloc_id(), NodeKind::Image(im))]
|
||||
vec![Node::new(d.alloc_id(), NodeKind::Image(Box::new(im)))]
|
||||
});
|
||||
d.images.insert("raster-1".into(), ImageBlob { mime: "image/png".into(), bytes: Arc::new(png), proxy: None });
|
||||
let out = export(&d, &opts(&d)).unwrap();
|
||||
|
|
|
|||
|
|
@ -562,7 +562,7 @@ impl Lerp {
|
|||
}
|
||||
}
|
||||
}
|
||||
n.mask = None;
|
||||
n.extras_mut().mask = None;
|
||||
n
|
||||
}
|
||||
Shape::Path { pair, rules, members } => {
|
||||
|
|
@ -590,7 +590,7 @@ impl Lerp {
|
|||
(NodeKind::PlacedDocument(o), NodeKind::PlacedDocument(x), NodeKind::PlacedDocument(y)) => o.xf = lerp_affine(x.xf, y.xf, t),
|
||||
_ => {}
|
||||
}
|
||||
n.mask = None;
|
||||
n.extras_mut().mask = None;
|
||||
n
|
||||
}
|
||||
Shape::Other => {
|
||||
|
|
@ -614,7 +614,7 @@ impl Lerp {
|
|||
for c in n.children_mut().into_iter().flatten() {
|
||||
zero_ids(Arc::make_mut(c));
|
||||
}
|
||||
n.mask = None;
|
||||
n.extras_mut().mask = None;
|
||||
n
|
||||
}
|
||||
};
|
||||
|
|
@ -994,7 +994,7 @@ fn paints_opaquely(n: &Node) -> bool {
|
|||
};
|
||||
n.opacity >= 1.0
|
||||
&& n.blend == vectorcraft_color::BlendMode::Normal
|
||||
&& n.mask.is_none()
|
||||
&& n.extras().mask.is_none()
|
||||
&& n.appearance.effects.is_empty()
|
||||
&& n.appearance
|
||||
.items
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ impl<'a> LiveCorners<'a> {
|
|||
/// The Live Corners of a path object.
|
||||
pub fn of(node: &'a Node) -> Option<Self> {
|
||||
let NodeKind::Path { path, live, .. } = &node.kind else { return None };
|
||||
Self::new(path, live.as_ref())
|
||||
Self::new(path, live.as_deref())
|
||||
}
|
||||
|
||||
/// The corner at anchor `index` of `base`.
|
||||
|
|
@ -127,7 +127,7 @@ impl<'a> LiveCorners<'a> {
|
|||
/// A path that isn't a live shape keeps its uncut outline as a [`LiveShape::Path`], which is a
|
||||
/// plain path again once no corner is cut. Ellipses and lines have no corners: they stay as they
|
||||
/// are.
|
||||
pub fn set_corners(path: &mut PathData, live: &mut Option<LiveShape>, corners: &BTreeSet<usize>, radius: Option<f64>, kind: Option<CornerKind>) {
|
||||
pub fn set_corners(path: &mut PathData, live: &mut Option<Box<LiveShape>>, corners: &BTreeSet<usize>, radius: Option<f64>, kind: Option<CornerKind>) {
|
||||
fn edit(radii: &mut [f64], kinds: &mut [CornerKind], corners: &BTreeSet<usize>, radius: Option<f64>, kind: Option<CornerKind>) {
|
||||
for k in corners {
|
||||
if let (Some(r), Some(slot)) = (radius, radii.get_mut(*k)) {
|
||||
|
|
@ -158,17 +158,17 @@ pub fn set_corners(path: &mut PathData, live: &mut Option<LiveShape>, corners: &
|
|||
kinds.clear();
|
||||
}
|
||||
}
|
||||
let l = live.get_or_insert_with(|| LiveShape::Path { base: path.clone(), radii: vec![], kinds: vec![] });
|
||||
let l = live.get_or_insert_with(|| Box::new(LiveShape::Path { base: path.clone(), radii: vec![], kinds: vec![] }));
|
||||
// Radii are document lengths and corners circular, also on a rectangle from a file that kept
|
||||
// an uneven scale in its transform (#442).
|
||||
l.fold_scale();
|
||||
match l {
|
||||
match &mut **l {
|
||||
LiveShape::Rectangle { radii, kinds, .. } => edit(radii, kinds, corners, radius, kind),
|
||||
LiveShape::Polygon { sides, radii, kinds, .. } => edit_vec(*sides as usize, radii, kinds, corners, radius, kind),
|
||||
LiveShape::Path { base, radii, kinds } => edit_vec(base.anchor_count(), radii, kinds, corners, radius, kind),
|
||||
LiveShape::Ellipse { .. } | LiveShape::Line { .. } => return,
|
||||
}
|
||||
match live {
|
||||
match live.as_deref_mut() {
|
||||
Some(LiveShape::Path { base, radii, kinds }) if radii.is_empty() && kinds.is_empty() => {
|
||||
*path = std::mem::take(base);
|
||||
*live = None;
|
||||
|
|
@ -220,15 +220,15 @@ mod tests {
|
|||
let star = shapes::star(Point::new(100.0, 100.0), 50.0, 25.0, 5, 0.0);
|
||||
let (mut path, mut live) = (star.clone(), None);
|
||||
set_corners(&mut path, &mut live, &set(&[0, 2]), Some(5.0), None);
|
||||
let Some(LiveShape::Path { base, radii, kinds }) = &live else { panic!("live corners: {live:?}") };
|
||||
let Some(LiveShape::Path { base, radii, kinds }) = live.as_deref() else { panic!("live corners: {live:?}") };
|
||||
assert_eq!((base, radii.len(), kinds.len()), (&star, 10, 0));
|
||||
assert_eq!(path.anchor_count(), 12, "two corners cut");
|
||||
let c = LiveCorners::new(&path, live.as_ref()).unwrap();
|
||||
let c = LiveCorners::new(&path, live.as_deref()).unwrap();
|
||||
assert_eq!(c.style(&set(&[0, 2])), (Some(5.0), Some(CornerKind::Round)));
|
||||
assert_eq!(c.corners_of(&anchors(&[0, 11])), set(&[0]), "the first corner's cut ends the path");
|
||||
// Kinds alone keep it live; squaring every corner makes it the plain star again.
|
||||
set_corners(&mut path, &mut live, &set(&[0, 2]), Some(0.0), Some(CornerKind::Chamfer));
|
||||
assert!(matches!(&live, Some(LiveShape::Path { radii, .. }) if radii.is_empty()));
|
||||
assert!(matches!(live.as_deref(), Some(LiveShape::Path { radii, .. }) if radii.is_empty()));
|
||||
set_corners(&mut path, &mut live, &set(&[0, 2]), None, Some(CornerKind::Round));
|
||||
assert_eq!((path, live), (star, None));
|
||||
}
|
||||
|
|
@ -237,16 +237,16 @@ mod tests {
|
|||
#[test]
|
||||
fn polygon_corners_follow_their_sides() {
|
||||
let mut l = LiveShape::Polygon { radius: 50.0, sides: 6, xf: Affine::translate((100.0, 100.0)), radii: vec![], kinds: vec![] };
|
||||
let (mut path, mut live) = (l.to_path(), Some(l.clone()));
|
||||
let (mut path, mut live) = (l.to_path(), Some(Box::new(l.clone())));
|
||||
set_corners(&mut path, &mut live, &set(&[0, 1, 2, 3, 4, 5]), Some(8.0), None);
|
||||
assert_eq!(path.anchor_count(), 12);
|
||||
l = live.clone().unwrap();
|
||||
l = *live.clone().unwrap();
|
||||
l.set_sides(8);
|
||||
assert!(matches!(&l, LiveShape::Polygon { radii, .. } if *radii == vec![8.0; 8]));
|
||||
assert_eq!(l.to_path().anchor_count(), 16);
|
||||
// Mixed radii: the new corners are sharp.
|
||||
set_corners(&mut path, &mut live, &set(&[1]), Some(3.0), None);
|
||||
let mut l = live.unwrap();
|
||||
let mut l = *live.unwrap();
|
||||
l.set_sides(7);
|
||||
assert!(matches!(&l, LiveShape::Polygon { radii, .. } if *radii == [8.0, 3.0, 8.0, 8.0, 8.0, 8.0, 0.0]));
|
||||
// An uneven scale keeps the cuts circular: the radius scales by the mean scale.
|
||||
|
|
|
|||
|
|
@ -104,8 +104,11 @@ impl GraphDesign {
|
|||
/// graph of its own (that its commands would then edit).
|
||||
pub fn plain_art(art: &crate::Node) -> crate::Node {
|
||||
let mut n = art.clone();
|
||||
n.graph = None;
|
||||
n.series_index = None;
|
||||
{
|
||||
let mut e = n.extras_mut();
|
||||
e.graph = None;
|
||||
e.series_index = None;
|
||||
}
|
||||
if let Some(ch) = n.children_mut() {
|
||||
let old = std::mem::take(ch);
|
||||
*ch = old.iter().map(|c| std::sync::Arc::new(Self::plain_art(c))).collect();
|
||||
|
|
|
|||
|
|
@ -157,7 +157,7 @@ fn hit_children(
|
|||
return None;
|
||||
}
|
||||
for (i, c) in children.iter().enumerate().rev() {
|
||||
if parent.shaper.is_some() {
|
||||
if parent.extras().shaper.is_some() {
|
||||
let construction = children.first().is_some_and(|source| Some(source.id) == opt.scope);
|
||||
if (i == 0) != construction {
|
||||
continue;
|
||||
|
|
@ -169,7 +169,7 @@ fn hit_children(
|
|||
}
|
||||
// (An envelope's content sits where it was, not where the envelope draws it.)
|
||||
if !(edits_contents(c)
|
||||
|| c.shaper.is_some() && c.children().and_then(|children| children.first()).is_some_and(|source| Some(source.id) == opt.scope))
|
||||
|| c.extras().shaper.is_some() && c.children().and_then(|children| children.first()).is_some_and(|source| Some(source.id) == opt.scope))
|
||||
&& let Some(b) = c.reach_bounds()
|
||||
&& !b.inflate(opt.tol, opt.tol).contains(p)
|
||||
{
|
||||
|
|
@ -534,7 +534,7 @@ mod tests {
|
|||
let l = d.layers[0].id;
|
||||
let mut add = |t: TextObject, perspective: Option<Affine>| {
|
||||
let mut n = Node::new(d.alloc_id(), NodeKind::Text(Box::new(t)));
|
||||
n.perspective = perspective.map(|a| {
|
||||
n.extras_mut().perspective = perspective.map(|a| {
|
||||
Box::new(PerspectiveAttachment { projection: Some(Homography::from_affine(a).to_array()), ..PerspectiveAttachment::new("left", 0.0) })
|
||||
});
|
||||
let id = n.id;
|
||||
|
|
|
|||
|
|
@ -86,8 +86,8 @@ pub use live::{BlendOrientation, BlendSpacing, BlendSpec, EnvelopeKind, Gradient
|
|||
pub use metadata::{CopyrightStatus, DocMetadata};
|
||||
pub use node::Knockout;
|
||||
pub use node::Scaling;
|
||||
pub use node::{Extras, ExtrasMut, ImageObject, LAYER_COLORS, LayerColor, LiveShape, Node, NodeExtras, NodeId, NodeKind, OpacityMask};
|
||||
pub use node::{ImageMap, ObjectAttributes};
|
||||
pub use node::{ImageObject, LAYER_COLORS, LayerColor, LiveShape, Node, NodeId, NodeKind, OpacityMask};
|
||||
pub use orient::OrientedBox;
|
||||
pub use pattern::{Overlap, PatternDef, PatternEdit, RepeatKind, RepeatSpec, TileType};
|
||||
pub use perspective::PerspectiveAttachment;
|
||||
|
|
|
|||
|
|
@ -136,7 +136,7 @@ fn proxy_png(bytes: &[u8]) -> Option<Vec<u8>> {
|
|||
/// `n` and every descendant, opacity-mask art included.
|
||||
fn walk_all<'a>(n: &'a Node, f: &mut impl FnMut(&'a Node)) {
|
||||
f(n);
|
||||
if let Some(m) = &n.mask {
|
||||
if let Some(m) = &n.extras().mask {
|
||||
walk_all(&m.art, f);
|
||||
}
|
||||
for c in n.children().into_iter().flatten() {
|
||||
|
|
@ -295,7 +295,7 @@ impl Document {
|
|||
NodeKind::PlacedDocument(placed) => f(&mut placed.link),
|
||||
_ => {}
|
||||
}
|
||||
if let Some(mask) = &mut n.mask {
|
||||
if let Some(mask) = n.extras_if_any().and_then(|e| e.mask.as_mut()) {
|
||||
walk(&mut mask.art, f);
|
||||
}
|
||||
for child in n.children_mut().into_iter().flatten() {
|
||||
|
|
@ -331,7 +331,7 @@ impl Document {
|
|||
{
|
||||
f(im);
|
||||
}
|
||||
if let Some(m) = &mut n.mask {
|
||||
if let Some(m) = n.extras_if_any().and_then(|e| e.mask.as_mut()) {
|
||||
update(&mut m.art, pick, f);
|
||||
}
|
||||
for c in n.children_mut().into_iter().flatten() {
|
||||
|
|
@ -370,7 +370,14 @@ mod tests {
|
|||
0,
|
||||
Node::new(
|
||||
id,
|
||||
NodeKind::Image(ImageObject { key: key.into(), width: 4, height: 4, xf: Affine::IDENTITY, link, placement: Default::default() }),
|
||||
NodeKind::Image(Box::new(ImageObject {
|
||||
key: key.into(),
|
||||
width: 4,
|
||||
height: 4,
|
||||
xf: Affine::IDENTITY,
|
||||
link,
|
||||
placement: Default::default(),
|
||||
})),
|
||||
),
|
||||
)
|
||||
.unwrap()
|
||||
|
|
|
|||
|
|
@ -1222,7 +1222,7 @@ pub fn envelope_of(doc: &crate::Document, id: NodeId) -> Option<&Node> {
|
|||
/// edit (its points and handles).
|
||||
pub fn envelope_overlay(n: &Node) -> Option<(PathData, Option<GradientMesh>)> {
|
||||
let NodeKind::Envelope { kind, .. } = &n.kind else { return None };
|
||||
match kind {
|
||||
match &**kind {
|
||||
EnvelopeKind::Mesh { rows, cols, points, handles } => {
|
||||
let grid = envelope_grid(*rows, *cols, points, handles)?;
|
||||
Some((grid.lines(), Some(grid)))
|
||||
|
|
@ -1270,7 +1270,7 @@ impl<'a> EnvelopeMap<'a> {
|
|||
/// The map of envelope node `n` (`None` for other nodes).
|
||||
pub fn of(n: &'a Node) -> Option<Self> {
|
||||
match &n.kind {
|
||||
NodeKind::Envelope { content, kind, frame, .. } => Self::new(content, kind, *frame),
|
||||
NodeKind::Envelope { content, kind, frame, .. } => Self::new(content, kind, **frame),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
|
@ -1388,7 +1388,7 @@ fn carry_look(from: &Node, mut to: Node) -> Node {
|
|||
to.isolate = from.isolate;
|
||||
to.knockout = from.knockout;
|
||||
to.knockout_shape = from.knockout_shape;
|
||||
to.mask = from.mask.clone();
|
||||
to.extras_mut().mask = from.extras().mask.clone();
|
||||
to.appearance.effects = from.appearance.effects.clone();
|
||||
to
|
||||
}
|
||||
|
|
@ -1417,7 +1417,7 @@ fn map_node(n: &Node, w: &Warper) -> Node {
|
|||
{
|
||||
out.appearance.warp_linear_gradients(b, &|p| affine_near(w.f, p, w.eps));
|
||||
}
|
||||
if let Some(m) = &mut out.mask {
|
||||
if let Some(m) = out.extras_if_any().and_then(|e| e.mask.as_mut()) {
|
||||
m.art = Arc::new(map_node(&m.art, w));
|
||||
}
|
||||
match &mut out.kind {
|
||||
|
|
@ -1494,7 +1494,7 @@ fn warp_image(n: &Node, im: &ImageObject, w: &Warper) -> Node {
|
|||
if let NodeKind::Path { clipping, .. } = &mut clip.kind {
|
||||
*clipping = true;
|
||||
}
|
||||
let image = Node::new(n.id, NodeKind::Image(ImageObject { xf, ..im.clone() }));
|
||||
let image = Node::new(n.id, NodeKind::Image(Box::new(ImageObject { xf, ..im.clone() })));
|
||||
pieces.push(Arc::new(Node::new(n.id, NodeKind::Group { children: vec![Arc::new(clip), Arc::new(image)], clip: true })));
|
||||
}
|
||||
}
|
||||
|
|
@ -1523,17 +1523,18 @@ fn pattern_art(n: &Node, tiles: PatternTiles) -> Option<Node> {
|
|||
_ => None,
|
||||
};
|
||||
let plain = |items: Vec<AppearanceItem>| {
|
||||
Arc::new(Node {
|
||||
let mut p = Node {
|
||||
appearance: Appearance { items, ..Default::default() }.into(),
|
||||
opacity: 1.0,
|
||||
blend: Default::default(),
|
||||
isolate: false,
|
||||
knockout: Default::default(),
|
||||
knockout_shape: false,
|
||||
mask: None,
|
||||
name: None,
|
||||
..n.clone()
|
||||
})
|
||||
};
|
||||
p.extras_mut().mask = None;
|
||||
Arc::new(p)
|
||||
};
|
||||
let mut pieces = vec![];
|
||||
let mut rest = vec![];
|
||||
|
|
@ -1640,7 +1641,7 @@ pub fn expand_live_with(n: &Node, outline: Outliner) -> Vec<Node> {
|
|||
pub fn expand_live_hooks(n: &Node, hooks: Hooks) -> Vec<Node> {
|
||||
match &n.kind {
|
||||
NodeKind::Blend { children, spec } => blend_expand(children, spec),
|
||||
NodeKind::Envelope { content, kind, fidelity, options, frame, .. } => expand_envelope(content, kind, *fidelity, *options, *frame, hooks),
|
||||
NodeKind::Envelope { content, kind, fidelity, options, frame, .. } => expand_envelope(content, kind, *fidelity, *options, **frame, hooks),
|
||||
NodeKind::Mesh(m) => mesh_quad_nodes(m, 8),
|
||||
NodeKind::Repeat(r) => r.expand(),
|
||||
NodeKind::PlacedDocument(p) => p.art(n),
|
||||
|
|
@ -1669,7 +1670,7 @@ pub fn expanded_group_hooks(n: &Node, hooks: Hooks) -> Node {
|
|||
g.knockout = crate::Knockout::Off;
|
||||
}
|
||||
g.knockout_shape = n.knockout_shape;
|
||||
g.mask = n.mask.clone();
|
||||
g.extras_mut().mask = n.extras().mask.clone();
|
||||
g
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,9 +2,11 @@
|
|||
//!
|
||||
//! Documents share structure (`Arc<Node>` children, [`crate::Shared`] resources), so an old
|
||||
//! version kept in the undo history holds only what the edits after it replaced. The estimate
|
||||
//! walks the old version beside the newer one and visits only what isn't shared: a subtree whose
|
||||
//! `Arc` the newer document also holds costs nothing and isn't entered, so the walk is about as
|
||||
//! long as the edit was (plus a pass over each changed child list). It counts the heap the values
|
||||
//! walks two versions side by side and visits only what isn't shared: a subtree whose `Arc` both
|
||||
//! hold costs nothing and isn't entered, so the walk is about as long as the edit was (plus a pass
|
||||
//! over each changed child list). One walk gives both directions: what the older version keeps
|
||||
//! that the newer one doesn't (the undo step's cost) and the other way round (the cost of the redo
|
||||
//! step that undoing it makes), so undo and redo needn't walk again. It counts the heap the values
|
||||
//! own (struct sizes, vector capacities, strings, JSON) plus a typical allocator overhead per
|
||||
//! allocation, without following every last field: an estimate in the right ballpark, for a
|
||||
//! memory budget, not an exact figure.
|
||||
|
|
@ -15,17 +17,18 @@ use std::sync::Arc;
|
|||
|
||||
use serde_json::Value;
|
||||
use vectorcraft_color::{Paint, Swatch, SwatchGroup};
|
||||
use vectorcraft_geom::{Anchor, PathData, SubPath};
|
||||
use vectorcraft_geom::{Affine, Anchor, PathData, SubPath};
|
||||
|
||||
use crate::live::{BlendSpec, EnvelopeKind};
|
||||
use crate::text::{ParaStyle, TextObject, TextRun, TextStyleDef};
|
||||
use crate::{Appearance, AppearanceItem, Document, Effect, GraphicStyle, Node, NodeKind, SavedSelection, Shared};
|
||||
use crate::{Appearance, AppearanceItem, Document, Effect, GraphicStyle, ImageObject, Node, NodeExtras, NodeKind, SavedSelection, Shared};
|
||||
|
||||
/// Bookkeeping and rounding a typical allocator adds to each allocation.
|
||||
const ALLOC: usize = 16;
|
||||
/// An `Arc`'s two reference counts.
|
||||
const ARC: usize = 16;
|
||||
/// The kinds of node this doesn't break down (meshes, envelopes' settings, placed documents…):
|
||||
/// a typical size of what they own besides the node.
|
||||
/// The kinds of node this doesn't break down (meshes, repeats' settings, placed documents…): a
|
||||
/// typical size of what they own besides the node.
|
||||
const OTHER_KIND: usize = 256;
|
||||
|
||||
impl Document {
|
||||
|
|
@ -33,23 +36,48 @@ impl Document {
|
|||
/// keeping this version in the undo history costs while `newer` (a later version) is kept too.
|
||||
/// Visits only the parts that aren't shared (see the [module docs](self)).
|
||||
pub fn retained_bytes(&self, newer: &Document) -> usize {
|
||||
if std::ptr::eq(self, newer) {
|
||||
return 0;
|
||||
self.retained_bytes_each(newer).0
|
||||
}
|
||||
|
||||
/// [`Document::retained_bytes`] both ways in one walk: what this document keeps that `other`
|
||||
/// doesn't share, and what `other` keeps that this one doesn't.
|
||||
pub fn retained_bytes_each(&self, other: &Document) -> (usize, usize) {
|
||||
if std::ptr::eq(self, other) {
|
||||
return (0, 0);
|
||||
}
|
||||
let mut w = Walk { newer, seen: None, bytes: size_of::<Document>() + ALLOC + ARC };
|
||||
w.list(&self.layers, Some(&newer.layers));
|
||||
for (i, s) in self.symbols.iter().enumerate() {
|
||||
w.bytes += s.name.capacity();
|
||||
w.tree(&s.art, newer.symbols.get(i).map(|n| &n.art));
|
||||
let own = size_of::<Document>() + ALLOC + ARC;
|
||||
let mut w = Walk { docs: [self, other], seen: [None, None], bytes: [own, own] };
|
||||
w.list(&self.layers, &other.layers);
|
||||
for i in 0..self.symbols.len().max(other.symbols.len()) {
|
||||
match (self.symbols.get(i), other.symbols.get(i)) {
|
||||
(Some(a), Some(b)) => w.pair(&a.art, &b.art),
|
||||
(a, b) => {
|
||||
a.into_iter().for_each(|a| w.alone(0, &a.art));
|
||||
b.into_iter().for_each(|b| w.alone(1, &b.art));
|
||||
}
|
||||
}
|
||||
}
|
||||
w.bytes += vec_bytes(&self.symbols);
|
||||
for (i, p) in self.patterns.iter().enumerate() {
|
||||
w.bytes += p.name.capacity();
|
||||
w.list(&p.art, newer.patterns.get(i).map(|n| &n.art));
|
||||
for i in 0..self.patterns.len().max(other.patterns.len()) {
|
||||
match (self.patterns.get(i), other.patterns.get(i)) {
|
||||
(Some(a), Some(b)) => w.list(&a.art, &b.art),
|
||||
(a, b) => {
|
||||
a.into_iter().flat_map(|a| &a.art).for_each(|n| w.alone(0, n));
|
||||
b.into_iter().flat_map(|b| &b.art).for_each(|n| w.alone(1, n));
|
||||
}
|
||||
}
|
||||
}
|
||||
w.bytes += vec_bytes(&self.patterns);
|
||||
w.bytes += self.resources_bytes(newer);
|
||||
w.bytes
|
||||
let [mut a, mut b] = w.bytes;
|
||||
a += self.lists_bytes() + self.resources_bytes(other);
|
||||
b += other.lists_bytes() + other.resources_bytes(self);
|
||||
(a, b)
|
||||
}
|
||||
|
||||
/// The symbol and pattern lists themselves (their art is counted as nodes).
|
||||
fn lists_bytes(&self) -> usize {
|
||||
vec_bytes(&self.symbols)
|
||||
+ self.symbols.iter().map(|s| s.name.capacity()).sum::<usize>()
|
||||
+ vec_bytes(&self.patterns)
|
||||
+ self.patterns.iter().map(|p| p.name.capacity()).sum::<usize>()
|
||||
}
|
||||
|
||||
/// The document-wide lists and settings: a copy of the document copies those that aren't
|
||||
|
|
@ -92,47 +120,99 @@ impl Document {
|
|||
}
|
||||
}
|
||||
|
||||
/// One estimate in progress.
|
||||
/// One estimate in progress: two documents walked side by side.
|
||||
struct Walk<'a> {
|
||||
newer: &'a Document,
|
||||
/// Every node the newer document holds (by address), built the first time a node has no
|
||||
/// counterpart in place: then a node moved elsewhere (grouped, say) is still found shared.
|
||||
seen: Option<HashSet<usize>>,
|
||||
bytes: usize,
|
||||
docs: [&'a Document; 2],
|
||||
/// For each side, every node the other document holds (by address), built the first time a
|
||||
/// node of that side that the other may share has no counterpart in place: then a node moved
|
||||
/// elsewhere (grouped, say) is still found shared.
|
||||
seen: [Option<HashSet<usize>>; 2],
|
||||
/// For each side, what it keeps that the other doesn't.
|
||||
bytes: [usize; 2],
|
||||
}
|
||||
|
||||
impl Walk<'_> {
|
||||
/// The nodes of `list` that `newer` (the counterpart list, if any) doesn't share.
|
||||
fn list(&mut self, list: &[Arc<Node>], newer: Option<&Vec<Arc<Node>>>) {
|
||||
for (i, n) in list.iter().enumerate() {
|
||||
self.tree(n, newer.and_then(|l| l.get(i)));
|
||||
/// Two versions of a child list: the nodes in the same place on both sides (the same `Arc`, or
|
||||
/// versions of the same object) are compared in place; the others (inserted, deleted, moved)
|
||||
/// may still be shared from elsewhere.
|
||||
fn list(&mut self, a: &[Arc<Node>], b: &[Arc<Node>]) {
|
||||
let same = |x: &Arc<Node>, y: &Arc<Node>| Arc::ptr_eq(x, y) || x.id == y.id;
|
||||
// In place from the start and from the end: an insertion or deletion shifts only what is
|
||||
// between.
|
||||
let head = a.iter().zip(b).take_while(|(x, y)| same(x, y)).count();
|
||||
let (a_rest, b_rest) = (a.get(head..).unwrap_or_default(), b.get(head..).unwrap_or_default());
|
||||
let tail = a_rest.iter().rev().zip(b_rest.iter().rev()).take_while(|(x, y)| same(x, y)).count();
|
||||
for (x, y) in a.iter().zip(b).take(head).chain(a_rest.iter().rev().zip(b_rest.iter().rev()).take(tail)) {
|
||||
self.pair(x, y);
|
||||
}
|
||||
let a_mid = a_rest.get(..a_rest.len().saturating_sub(tail)).unwrap_or_default();
|
||||
let b_mid = b_rest.get(..b_rest.len().saturating_sub(tail)).unwrap_or_default();
|
||||
for i in 0..a_mid.len().max(b_mid.len()) {
|
||||
match (a_mid.get(i), b_mid.get(i)) {
|
||||
(Some(x), Some(y)) if same(x, y) => self.pair(x, y),
|
||||
(x, y) => {
|
||||
x.into_iter().for_each(|x| self.alone(0, x));
|
||||
y.into_iter().for_each(|y| self.alone(1, y));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Node `n` and what under it isn't shared; `newer` is the node in its place in the newer
|
||||
/// document, if any.
|
||||
fn tree(&mut self, n: &Arc<Node>, newer: Option<&Arc<Node>>) {
|
||||
if newer.is_some_and(|m| Arc::ptr_eq(n, m)) {
|
||||
/// Node `a` and `b` in the same place on the two sides: nothing when they are the same `Arc`,
|
||||
/// both nodes and what under them isn't shared when they are versions of the same object.
|
||||
fn pair(&mut self, a: &Arc<Node>, b: &Arc<Node>) {
|
||||
if Arc::ptr_eq(a, b) {
|
||||
return;
|
||||
}
|
||||
// In its place is another version of the same object (edited, moved): compare their
|
||||
// children in place. Else it may sit elsewhere in the newer document.
|
||||
let counterpart = newer.filter(|m| m.id == n.id);
|
||||
if counterpart.is_none() && self.newer_holds(n) {
|
||||
if a.id != b.id {
|
||||
self.alone(0, a);
|
||||
self.alone(1, b);
|
||||
return;
|
||||
}
|
||||
self.bytes += node_bytes(n, counterpart.map(|m| &**m));
|
||||
if let Some(ch) = n.children() {
|
||||
self.list(ch, counterpart.and_then(|m| m.children()));
|
||||
self.bytes[0] += node_bytes(a, Some(b));
|
||||
self.bytes[1] += node_bytes(b, Some(a));
|
||||
match (a.children(), b.children()) {
|
||||
(Some(x), Some(y)) => self.list(x, y),
|
||||
(x, y) => {
|
||||
x.into_iter().flatten().for_each(|c| self.alone(0, c));
|
||||
y.into_iter().flatten().for_each(|c| self.alone(1, c));
|
||||
}
|
||||
}
|
||||
if let Some(mask) = &n.mask {
|
||||
self.tree(&mask.art, counterpart.and_then(|m| m.mask.as_ref()).map(|m| &m.art));
|
||||
match (&a.extras().mask, &b.extras().mask) {
|
||||
(Some(x), Some(y)) => self.pair(&x.art, &y.art),
|
||||
(x, y) => {
|
||||
x.iter().for_each(|m| self.alone(0, &m.art));
|
||||
y.iter().for_each(|m| self.alone(1, &m.art));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn newer_holds(&mut self, n: &Arc<Node>) -> bool {
|
||||
let newer = self.newer;
|
||||
let seen = self.seen.get_or_insert_with(|| {
|
||||
/// Node `n` of side `side` without a counterpart in place, and what under it the other side
|
||||
/// doesn't hold.
|
||||
fn alone(&mut self, side: usize, n: &Arc<Node>) {
|
||||
if self.other_holds(side, n) {
|
||||
return;
|
||||
}
|
||||
if let Some(b) = self.bytes.get_mut(side) {
|
||||
*b += node_bytes(n, None);
|
||||
}
|
||||
for c in n.children().into_iter().flatten() {
|
||||
self.alone(side, c);
|
||||
}
|
||||
if let Some(m) = &n.extras().mask {
|
||||
self.alone(side, &m.art);
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the document across from `side` holds node `n` somewhere.
|
||||
fn other_holds(&mut self, side: usize, n: &Arc<Node>) -> bool {
|
||||
// Held only where it was found: not in the other document (which would hold it too). So
|
||||
// new art inserted by the edit is told apart without looking.
|
||||
if Arc::strong_count(n) == 1 {
|
||||
return false;
|
||||
}
|
||||
let (Some(other), Some(seen)) = (self.docs.get(1 - side.min(1)), self.seen.get_mut(side)) else { return false };
|
||||
let seen = seen.get_or_insert_with(|| {
|
||||
fn add(n: &Arc<Node>, out: &mut HashSet<usize>) {
|
||||
// A node already seen was entered already (a node shared within the document).
|
||||
if !out.insert(Arc::as_ptr(n) as usize) {
|
||||
|
|
@ -141,12 +221,12 @@ impl Walk<'_> {
|
|||
for c in n.children().into_iter().flatten() {
|
||||
add(c, out);
|
||||
}
|
||||
if let Some(m) = &n.mask {
|
||||
if let Some(m) = &n.extras().mask {
|
||||
add(&m.art, out);
|
||||
}
|
||||
}
|
||||
let mut out = HashSet::new();
|
||||
let roots = newer.layers.iter().chain(newer.symbols.iter().map(|s| &s.art)).chain(newer.patterns.iter().flat_map(|p| &p.art));
|
||||
let roots = other.layers.iter().chain(other.symbols.iter().map(|s| &s.art)).chain(other.patterns.iter().flat_map(|p| &p.art));
|
||||
for n in roots {
|
||||
add(n, &mut out);
|
||||
}
|
||||
|
|
@ -157,28 +237,29 @@ impl Walk<'_> {
|
|||
}
|
||||
|
||||
/// One node's own heap: the node, and what it owns besides its children and mask art (counted
|
||||
/// as nodes of their own). Its appearance counts unless `newer`, its newer version, shares it.
|
||||
fn node_bytes(n: &Node, newer: Option<&Node>) -> usize {
|
||||
/// as nodes of their own). Its appearance counts unless `other`, its other version, shares it.
|
||||
fn node_bytes(n: &Node, other: Option<&Node>) -> usize {
|
||||
fn boxed<T>(b: &Option<Box<T>>) -> usize {
|
||||
b.as_ref().map_or(0, |_| ALLOC + size_of::<T>())
|
||||
}
|
||||
let mut b = ALLOC + ARC + size_of::<Node>();
|
||||
b += n.name.as_ref().map_or(0, String::capacity);
|
||||
if !newer.is_some_and(|m| Shared::ptr_eq(&m.appearance, &n.appearance)) {
|
||||
if !other.is_some_and(|m| Shared::ptr_eq(&m.appearance, &n.appearance)) {
|
||||
b += ALLOC + ARC + appearance_heap(&n.appearance);
|
||||
}
|
||||
b += boxed(&n.mask) + boxed(&n.graph) + boxed(&n.shaper) + boxed(&n.attrs) + boxed(&n.slice) + boxed(&n.perspective);
|
||||
b += n.trace.as_ref().map_or(0, |t| ALLOC + json_bytes(t));
|
||||
if let Some(e) = n.extras_box() {
|
||||
b += ALLOC + size_of::<NodeExtras>();
|
||||
b += boxed(&e.mask) + boxed(&e.graph) + boxed(&e.shaper) + boxed(&e.attrs) + boxed(&e.slice) + boxed(&e.perspective);
|
||||
b += e.trace.as_ref().map_or(0, |t| ALLOC + size_of::<Value>() + json_bytes(t));
|
||||
}
|
||||
b += match &n.kind {
|
||||
NodeKind::Path { path, .. } => path_bytes(path),
|
||||
NodeKind::Layer { children, .. }
|
||||
| NodeKind::Group { children, .. }
|
||||
| NodeKind::Compound { children, .. }
|
||||
| NodeKind::Blend { children, .. }
|
||||
| NodeKind::Envelope { content: children, .. } => vec_bytes(children),
|
||||
NodeKind::Path { path, live, .. } => path_bytes(path) + boxed(live),
|
||||
NodeKind::Layer { children, .. } | NodeKind::Group { children, .. } | NodeKind::Compound { children, .. } => vec_bytes(children),
|
||||
NodeKind::Blend { children, .. } => vec_bytes(children) + ALLOC + size_of::<BlendSpec>(),
|
||||
NodeKind::Envelope { content, .. } => vec_bytes(content) + 2 * ALLOC + size_of::<EnvelopeKind>() + size_of::<Affine>(),
|
||||
NodeKind::Repeat(r) => vec_bytes(&r.source) + OTHER_KIND,
|
||||
NodeKind::Text(t) => ALLOC + text_bytes(t),
|
||||
NodeKind::Image(im) => im.key.capacity(),
|
||||
NodeKind::Image(im) => ALLOC + size_of::<ImageObject>() + im.key.capacity(),
|
||||
NodeKind::SymbolInstance { symbol, .. } => symbol.capacity(),
|
||||
_ => OTHER_KIND,
|
||||
};
|
||||
|
|
@ -273,7 +354,8 @@ mod tests {
|
|||
}
|
||||
|
||||
/// Moving every rectangle: each keeps its old node and anchors (its appearance stays shared).
|
||||
/// The process grew by about 775 bytes per moved rectangle when measured (#1244).
|
||||
/// The process grew by about 775 bytes per moved rectangle when measured (#1244), about 490
|
||||
/// once nodes kept their rarely set parts out of line.
|
||||
#[test]
|
||||
fn moving_rectangles_costs_their_nodes() {
|
||||
let n = 1000;
|
||||
|
|
@ -307,6 +389,26 @@ mod tests {
|
|||
assert!(bytes < 8192, "only the old layer and its child list: {bytes}");
|
||||
}
|
||||
|
||||
/// One walk gives both ways, each what a walk the other way gives; an insertion and a deletion
|
||||
/// cost the side that has the objects.
|
||||
#[test]
|
||||
fn both_ways_at_once() {
|
||||
let before = doc_with_rects(100);
|
||||
let mut after = before.clone();
|
||||
let layer = after.layers[0].id;
|
||||
for at in [0, 50, usize::MAX] {
|
||||
let id = after.alloc_id();
|
||||
let r = shapes::rectangle(kurbo::Rect::new(0.0, 0.0, 10.0, 10.0));
|
||||
after.insert(Some(layer), at, Node::path(id, r, Appearance::default_art())).unwrap();
|
||||
}
|
||||
let (kept, added) = before.retained_bytes_each(&after);
|
||||
assert_eq!((kept, added), (before.retained_bytes(&after), after.retained_bytes(&before)));
|
||||
assert_eq!(after.retained_bytes_each(&before), (added, kept));
|
||||
// The old layer and its list are the old version's own; the new one holds three new nodes.
|
||||
assert!(kept < 4096, "{kept}");
|
||||
assert!(added > kept + 3 * 300, "{added} for three new rectangles");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_resource_counts_only_when_changed() {
|
||||
let before = doc_with_rects(1);
|
||||
|
|
|
|||
|
|
@ -427,8 +427,9 @@ pub enum NodeKind {
|
|||
path: PathData,
|
||||
#[serde(default, skip_serializing_if = "crate::skip::is_default")]
|
||||
rule: FillRule,
|
||||
/// Boxed: few paths are live shapes, and the shape is larger than the rest of the variant.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
live: Option<LiveShape>,
|
||||
live: Option<Box<LiveShape>>,
|
||||
/// This path acts as a clipping path inside a clip group.
|
||||
#[serde(default, skip_serializing_if = "crate::skip::is_default")]
|
||||
clipping: bool,
|
||||
|
|
@ -443,7 +444,8 @@ pub enum NodeKind {
|
|||
rule: FillRule,
|
||||
},
|
||||
Text(Box<TextObject>),
|
||||
Image(ImageObject),
|
||||
/// Boxed, as the other large kinds: a node is as large as its largest kind.
|
||||
Image(Box<ImageObject>),
|
||||
SymbolInstance {
|
||||
symbol: String,
|
||||
xf: Affine,
|
||||
|
|
@ -453,12 +455,12 @@ pub enum NodeKind {
|
|||
Blend {
|
||||
children: Vec<Arc<Node>>,
|
||||
#[serde(default)]
|
||||
spec: BlendSpec,
|
||||
spec: Box<BlendSpec>,
|
||||
},
|
||||
/// Live envelope distortion of `content`.
|
||||
Envelope {
|
||||
content: Vec<Arc<Node>>,
|
||||
kind: EnvelopeKind,
|
||||
kind: Box<EnvelopeKind>,
|
||||
/// Envelope Options → Fidelity (0–100).
|
||||
#[serde(default = "crate::live::default_fidelity")]
|
||||
fidelity: f64,
|
||||
|
|
@ -472,7 +474,7 @@ pub enum NodeKind {
|
|||
/// the page axes can't stand for (rotation, shear, reflection). The content maps from its
|
||||
/// bounds in this frame, and a warp bends along it.
|
||||
#[serde(default, skip_serializing_if = "crate::skip::is_default")]
|
||||
frame: Affine,
|
||||
frame: Box<Affine>,
|
||||
},
|
||||
/// Gradient mesh.
|
||||
Mesh(GradientMesh),
|
||||
|
|
@ -491,72 +493,328 @@ fn one() -> f32 {
|
|||
}
|
||||
|
||||
/// One object in the tree. Children are `Arc`s so documents share structure between undo states.
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
///
|
||||
/// What most objects have is kept inline; what few have is in [`NodeExtras`], allocated only for
|
||||
/// the objects that have any of it: every undo step of an edit copies the nodes it changed, so a
|
||||
/// small node is a cheap step. Files keep the flat field list either way: a node is written field
|
||||
/// by field from where each lives, and read through a flat `NodeWire`.
|
||||
#[derive(Clone, Debug, PartialEq, Deserialize)]
|
||||
#[serde(from = "NodeWire")]
|
||||
pub struct Node {
|
||||
pub id: NodeId,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub name: Option<String>,
|
||||
#[serde(default = "yes", skip_serializing_if = "crate::skip::is_true")]
|
||||
pub visible: bool,
|
||||
#[serde(default, skip_serializing_if = "crate::skip::is_default")]
|
||||
pub locked: bool,
|
||||
#[serde(default = "one", skip_serializing_if = "crate::skip::is_one")]
|
||||
pub opacity: f32,
|
||||
#[serde(default, skip_serializing_if = "crate::skip::is_default")]
|
||||
pub blend: BlendMode,
|
||||
#[serde(default, skip_serializing_if = "crate::skip::is_default")]
|
||||
pub isolate: bool,
|
||||
/// Knockout Group: whether a container's children knock each other out (see [`Knockout`]).
|
||||
#[serde(default, skip_serializing_if = "crate::skip::is_default")]
|
||||
pub knockout: Knockout,
|
||||
/// Opacity & Mask Define Knockout Shape: inside a knockout group, this object's opacity and
|
||||
/// opacity mask scale how much of the objects below it in the group it knocks out.
|
||||
#[serde(default, skip_serializing_if = "crate::skip::is_default")]
|
||||
pub knockout_shape: bool,
|
||||
#[serde(default, skip_serializing_if = "crate::skip::is_default")]
|
||||
pub appearance: crate::Shared<Appearance>,
|
||||
/// Opacity mask (Transparency panel). Its art lives here, outside the layer tree.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub mask: Option<Box<OpacityMask>>,
|
||||
/// Image Trace object: `{preset, params, view?}` it was traced with (the Image Trace panel shows
|
||||
/// them; `view` is its [`crate::TraceView`] id, absent for the tracing result).
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub trace: Option<Box<serde_json::Value>>,
|
||||
/// Object → Text Wrap: area type below this object (in the same layer) flows around it.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub wrap: Option<crate::text::TextWrap>,
|
||||
/// Graph object: the group's children are generated from this spec (Object → Graph).
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub graph: Option<Box<crate::graph::GraphSpec>>,
|
||||
/// Index of the graph series this generated group belongs to. Axes, the legend and other
|
||||
/// children leave it unset, so a series named "Legend" or "Axes" is not mistaken for them.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub series_index: Option<u32>,
|
||||
/// Editable Shaper composition; the original art is retained in its source child.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub shaper: Option<Box<crate::shaper::ShaperSpec>>,
|
||||
pub kind: NodeKind,
|
||||
/// The [`crate::GraphicStyle::id`] last applied to this object. It stays linked while it keeps
|
||||
/// that style's look: editing its appearance or transparency breaks the link.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub graphic_style: Option<u32>,
|
||||
/// Attributes panel: centre point display, image map, URL and note (`None`: all defaults).
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub attrs: Option<Box<ObjectAttributes>>,
|
||||
/// Object → Slice → Make: the object is an object slice (its slice follows its bounds) with
|
||||
/// these options.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub slice: Option<Box<crate::SliceOptions>>,
|
||||
/// The angle of the object's own axes, counter-clockwise degrees (0: square to the page): its
|
||||
/// bounding box and handles stand at this angle. Transforms turn it with the object (see
|
||||
/// [`crate::orient`]); Reset Bounding Box sets it back to 0.
|
||||
#[serde(default, skip_serializing_if = "crate::skip::is_default")]
|
||||
pub bbox_angle: f64,
|
||||
/// The rarely set parts, read with [`Node::extras`] and changed with [`Node::extras_mut`]
|
||||
/// (public so that `Node { opacity: 1.0, ..node }` works).
|
||||
pub extras: Extras,
|
||||
}
|
||||
|
||||
/// The parts of a [`Node`] that few objects have: kept out of line, so that the nodes without
|
||||
/// them stay small ([`Node::extras`]).
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct NodeExtras {
|
||||
/// Opacity mask (Transparency panel). Its art lives here, outside the layer tree.
|
||||
pub mask: Option<Box<OpacityMask>>,
|
||||
/// Image Trace object: `{preset, params, view?}` it was traced with (the Image Trace panel shows
|
||||
/// them; `view` is its [`crate::TraceView`] id, absent for the tracing result).
|
||||
pub trace: Option<Box<serde_json::Value>>,
|
||||
/// Object → Text Wrap: area type below this object (in the same layer) flows around it.
|
||||
pub wrap: Option<crate::text::TextWrap>,
|
||||
/// Graph object: the group's children are generated from this spec (Object → Graph).
|
||||
pub graph: Option<Box<crate::graph::GraphSpec>>,
|
||||
/// Index of the graph series this generated group belongs to. Axes, the legend and other
|
||||
/// children leave it unset, so a series named "Legend" or "Axes" is not mistaken for them.
|
||||
pub series_index: Option<u32>,
|
||||
/// Editable Shaper composition; the original art is retained in its source child.
|
||||
pub shaper: Option<Box<crate::shaper::ShaperSpec>>,
|
||||
/// The [`crate::GraphicStyle::id`] last applied to this object. It stays linked while it keeps
|
||||
/// that style's look: editing its appearance or transparency breaks the link.
|
||||
pub graphic_style: Option<u32>,
|
||||
/// Attributes panel: centre point display, image map, URL and note (`None`: all defaults).
|
||||
pub attrs: Option<Box<ObjectAttributes>>,
|
||||
/// Object → Slice → Make: the object is an object slice (its slice follows its bounds) with
|
||||
/// these options.
|
||||
pub slice: Option<Box<crate::SliceOptions>>,
|
||||
/// Object › Perspective: the perspective grid plane the object is attached to.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub perspective: Option<Box<crate::PerspectiveAttachment>>,
|
||||
}
|
||||
|
||||
/// No extras: what [`Node::extras`] gives for a node without any.
|
||||
static NO_EXTRAS: NodeExtras = NodeExtras {
|
||||
mask: None,
|
||||
trace: None,
|
||||
wrap: None,
|
||||
graph: None,
|
||||
series_index: None,
|
||||
shaper: None,
|
||||
graphic_style: None,
|
||||
attrs: None,
|
||||
slice: None,
|
||||
perspective: None,
|
||||
};
|
||||
|
||||
/// A node's [`NodeExtras`], if it has any ([`Node::extras`]). Extras that are all unset equal
|
||||
/// none.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct Extras(Option<Box<NodeExtras>>);
|
||||
|
||||
impl Extras {
|
||||
fn get(&self) -> &NodeExtras {
|
||||
self.0.as_deref().unwrap_or(&NO_EXTRAS)
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for Extras {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
match (&self.0, &other.0) {
|
||||
(Some(a), Some(b)) => a == b,
|
||||
_ => self.get() == other.get(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Node {
|
||||
/// The rarely set parts of this node (all unset when it has none).
|
||||
#[inline]
|
||||
pub fn extras(&self) -> &NodeExtras {
|
||||
self.extras.get()
|
||||
}
|
||||
/// The rarely set parts of this node, to change: when the change is done (the guard dropped),
|
||||
/// extras that were set are allocated and extras left all unset are let go. Setting one to
|
||||
/// `None` on a node without extras allocates nothing.
|
||||
#[inline]
|
||||
pub fn extras_mut(&mut self) -> ExtrasMut<'_> {
|
||||
ExtrasMut { slot: &mut self.extras.0, local: NodeExtras::default() }
|
||||
}
|
||||
/// The rarely set parts of this node, if it has any: to change what is there in place.
|
||||
#[inline]
|
||||
pub fn extras_if_any(&mut self) -> Option<&mut NodeExtras> {
|
||||
self.extras.0.as_deref_mut()
|
||||
}
|
||||
/// The live shape of a path object, if it is one.
|
||||
pub fn live_shape(&self) -> Option<&LiveShape> {
|
||||
match &self.kind {
|
||||
NodeKind::Path { live, .. } => live.as_deref(),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
/// The live shape of a path object, if it is one, to change.
|
||||
pub fn live_shape_mut(&mut self) -> Option<&mut LiveShape> {
|
||||
match &mut self.kind {
|
||||
NodeKind::Path { live, .. } => live.as_deref_mut(),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
/// This node with opacity mask `mask` (or none).
|
||||
pub fn with_mask(mut self, mask: Option<Box<OpacityMask>>) -> Self {
|
||||
self.extras_mut().mask = mask;
|
||||
self
|
||||
}
|
||||
/// The extras, if this node has them allocated (for [`crate::mem`]'s estimate).
|
||||
pub(crate) fn extras_box(&self) -> Option<&NodeExtras> {
|
||||
self.extras.0.as_deref()
|
||||
}
|
||||
}
|
||||
|
||||
/// [`Node::extras_mut`]: the node's extras while they change.
|
||||
pub struct ExtrasMut<'a> {
|
||||
slot: &'a mut Option<Box<NodeExtras>>,
|
||||
/// Stands for the extras of a node without any, boxed into it when dropped if one was set.
|
||||
local: NodeExtras,
|
||||
}
|
||||
|
||||
impl std::ops::Deref for ExtrasMut<'_> {
|
||||
type Target = NodeExtras;
|
||||
fn deref(&self) -> &NodeExtras {
|
||||
self.slot.as_deref().unwrap_or(&self.local)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::DerefMut for ExtrasMut<'_> {
|
||||
fn deref_mut(&mut self) -> &mut NodeExtras {
|
||||
match self.slot.as_deref_mut() {
|
||||
Some(e) => e,
|
||||
None => &mut self.local,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for ExtrasMut<'_> {
|
||||
fn drop(&mut self) {
|
||||
match self.slot.as_deref() {
|
||||
Some(e) if *e == NO_EXTRAS => *self.slot = None,
|
||||
Some(_) => {}
|
||||
None if self.local != NO_EXTRAS => *self.slot = Some(Box::new(std::mem::take(&mut self.local))),
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// How a [`Node`] is written: the flat field list of the file format, in its order, borrowed from
|
||||
/// the node and its extras (nothing is copied). The node reads back through `NodeWire`.
|
||||
impl Serialize for Node {
|
||||
fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
|
||||
use crate::skip::{is_default, is_one, is_true};
|
||||
use serde::ser::SerializeStruct;
|
||||
let e = self.extras();
|
||||
// As the derive did: the fields written, then each field written or skipped in order.
|
||||
let skips = [
|
||||
false,
|
||||
self.name.is_none(),
|
||||
is_true(&self.visible),
|
||||
is_default(&self.locked),
|
||||
is_one(&self.opacity),
|
||||
is_default(&self.blend),
|
||||
is_default(&self.isolate),
|
||||
is_default(&self.knockout),
|
||||
is_default(&self.knockout_shape),
|
||||
is_default(&self.appearance),
|
||||
e.mask.is_none(),
|
||||
e.trace.is_none(),
|
||||
e.wrap.is_none(),
|
||||
e.graph.is_none(),
|
||||
e.series_index.is_none(),
|
||||
e.shaper.is_none(),
|
||||
false,
|
||||
e.graphic_style.is_none(),
|
||||
e.attrs.is_none(),
|
||||
e.slice.is_none(),
|
||||
is_default(&self.bbox_angle),
|
||||
e.perspective.is_none(),
|
||||
];
|
||||
let mut st = s.serialize_struct("Node", skips.iter().filter(|s| !**s).count())?;
|
||||
let mut skip = skips.into_iter();
|
||||
macro_rules! field {
|
||||
($name:literal, $v:expr) => {
|
||||
if skip.next().unwrap_or(false) {
|
||||
st.skip_field($name)?;
|
||||
} else {
|
||||
st.serialize_field($name, $v)?;
|
||||
}
|
||||
};
|
||||
}
|
||||
field!("id", &self.id);
|
||||
field!("name", &self.name);
|
||||
field!("visible", &self.visible);
|
||||
field!("locked", &self.locked);
|
||||
field!("opacity", &self.opacity);
|
||||
field!("blend", &self.blend);
|
||||
field!("isolate", &self.isolate);
|
||||
field!("knockout", &self.knockout);
|
||||
field!("knockout_shape", &self.knockout_shape);
|
||||
field!("appearance", &self.appearance);
|
||||
field!("mask", &e.mask);
|
||||
field!("trace", &e.trace);
|
||||
field!("wrap", &e.wrap);
|
||||
field!("graph", &e.graph);
|
||||
field!("series_index", &e.series_index);
|
||||
field!("shaper", &e.shaper);
|
||||
field!("kind", &self.kind);
|
||||
field!("graphic_style", &e.graphic_style);
|
||||
field!("attrs", &e.attrs);
|
||||
field!("slice", &e.slice);
|
||||
field!("bbox_angle", &self.bbox_angle);
|
||||
field!("perspective", &e.perspective);
|
||||
st.end()
|
||||
}
|
||||
}
|
||||
|
||||
/// How a [`Node`] is read: the flat field list of the file format. Fields move into the node
|
||||
/// (and its extras, allocated only when one is set) without a copy of what they own.
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename = "Node")]
|
||||
struct NodeWire {
|
||||
id: NodeId,
|
||||
#[serde(default)]
|
||||
name: Option<String>,
|
||||
#[serde(default = "yes")]
|
||||
visible: bool,
|
||||
#[serde(default)]
|
||||
locked: bool,
|
||||
#[serde(default = "one")]
|
||||
opacity: f32,
|
||||
#[serde(default)]
|
||||
blend: BlendMode,
|
||||
#[serde(default)]
|
||||
isolate: bool,
|
||||
#[serde(default)]
|
||||
knockout: Knockout,
|
||||
#[serde(default)]
|
||||
knockout_shape: bool,
|
||||
#[serde(default)]
|
||||
appearance: crate::Shared<Appearance>,
|
||||
#[serde(default)]
|
||||
mask: Option<Box<OpacityMask>>,
|
||||
#[serde(default)]
|
||||
trace: Option<Box<serde_json::Value>>,
|
||||
#[serde(default)]
|
||||
wrap: Option<crate::text::TextWrap>,
|
||||
#[serde(default)]
|
||||
graph: Option<Box<crate::graph::GraphSpec>>,
|
||||
#[serde(default)]
|
||||
series_index: Option<u32>,
|
||||
#[serde(default)]
|
||||
shaper: Option<Box<crate::shaper::ShaperSpec>>,
|
||||
kind: NodeKind,
|
||||
#[serde(default)]
|
||||
graphic_style: Option<u32>,
|
||||
#[serde(default)]
|
||||
attrs: Option<Box<ObjectAttributes>>,
|
||||
#[serde(default)]
|
||||
slice: Option<Box<crate::SliceOptions>>,
|
||||
#[serde(default)]
|
||||
bbox_angle: f64,
|
||||
#[serde(default)]
|
||||
perspective: Option<Box<crate::PerspectiveAttachment>>,
|
||||
}
|
||||
|
||||
impl From<NodeWire> for Node {
|
||||
fn from(w: NodeWire) -> Self {
|
||||
let extras = NodeExtras {
|
||||
mask: w.mask,
|
||||
trace: w.trace,
|
||||
wrap: w.wrap,
|
||||
graph: w.graph,
|
||||
series_index: w.series_index,
|
||||
shaper: w.shaper,
|
||||
graphic_style: w.graphic_style,
|
||||
attrs: w.attrs,
|
||||
slice: w.slice,
|
||||
perspective: w.perspective,
|
||||
};
|
||||
Node {
|
||||
id: w.id,
|
||||
name: w.name,
|
||||
visible: w.visible,
|
||||
locked: w.locked,
|
||||
opacity: w.opacity,
|
||||
blend: w.blend,
|
||||
isolate: w.isolate,
|
||||
knockout: w.knockout,
|
||||
knockout_shape: w.knockout_shape,
|
||||
appearance: w.appearance,
|
||||
kind: w.kind,
|
||||
bbox_angle: w.bbox_angle,
|
||||
extras: Extras((extras != NO_EXTRAS).then(|| Box::new(extras))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Opacity mask: the luminance of the mask art sets the object's opacity (white = opaque).
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct OpacityMask {
|
||||
|
|
@ -595,18 +853,9 @@ impl Node {
|
|||
knockout: Knockout::Neutral,
|
||||
knockout_shape: false,
|
||||
appearance: Appearance::default().into(),
|
||||
mask: None,
|
||||
trace: None,
|
||||
wrap: None,
|
||||
graph: None,
|
||||
series_index: None,
|
||||
shaper: None,
|
||||
kind,
|
||||
graphic_style: None,
|
||||
attrs: None,
|
||||
slice: None,
|
||||
bbox_angle: 0.0,
|
||||
perspective: None,
|
||||
extras: Extras::default(),
|
||||
}
|
||||
}
|
||||
pub fn path(id: NodeId, path: PathData, appearance: Appearance) -> Self {
|
||||
|
|
@ -691,7 +940,7 @@ impl Node {
|
|||
&& !self.isolate
|
||||
&& self.knockout == Knockout::Neutral
|
||||
&& !self.knockout_shape
|
||||
&& self.mask.is_none()
|
||||
&& self.extras().mask.is_none()
|
||||
}
|
||||
/// Kind name as the Layers panel / Properties panel shows it.
|
||||
pub fn kind_label(&self) -> &'static str {
|
||||
|
|
@ -772,7 +1021,7 @@ impl Node {
|
|||
}
|
||||
NodeKind::Layer { children, .. } | NodeKind::Group { children, .. } | NodeKind::Compound { children, .. } => children
|
||||
.iter()
|
||||
.skip(usize::from(self.shaper.is_some()))
|
||||
.skip(usize::from(self.extras().shaper.is_some()))
|
||||
.filter(|c| c.visible || !matches!(self.kind, NodeKind::Layer { .. }))
|
||||
.fold(None, |acc, c| vectorcraft_geom::union_opt(acc, c.geometric_bounds())),
|
||||
NodeKind::Text(t) => self.projected(t.bounds()),
|
||||
|
|
@ -782,7 +1031,7 @@ impl Node {
|
|||
let b = crate::live::nodes_bounds(children);
|
||||
vectorcraft_geom::union_opt(b, spec.spine.as_ref().and_then(|s| s.bounds()))
|
||||
}
|
||||
NodeKind::Envelope { content, kind, frame, .. } => crate::live::envelope_bounds(content, kind, *frame),
|
||||
NodeKind::Envelope { content, kind, frame, .. } => crate::live::envelope_bounds(content, kind, **frame),
|
||||
NodeKind::Mesh(m) => m.bounds(),
|
||||
NodeKind::Repeat(r) => r.bounds(),
|
||||
NodeKind::PlacedDocument(p) => Some(p.bounds()),
|
||||
|
|
@ -806,7 +1055,7 @@ impl Node {
|
|||
let o = self.appearance.outset();
|
||||
children
|
||||
.iter()
|
||||
.skip(usize::from(self.shaper.is_some()))
|
||||
.skip(usize::from(self.extras().shaper.is_some()))
|
||||
.fold(None, |acc, c| vectorcraft_geom::union_opt(acc, c.visual_bounds()))
|
||||
.map(|b| b.inflate(o, o))
|
||||
}
|
||||
|
|
@ -903,8 +1152,8 @@ impl Node {
|
|||
for c in content.iter_mut() {
|
||||
Arc::make_mut(c).transform_scaled(a, sc);
|
||||
}
|
||||
*frame = crate::live::envelope_frame(a * *frame);
|
||||
match kind {
|
||||
**frame = crate::live::envelope_frame(a * **frame);
|
||||
match &mut **kind {
|
||||
EnvelopeKind::Mesh { points, handles, .. } => {
|
||||
for p in points.iter_mut() {
|
||||
*p = a * *p;
|
||||
|
|
@ -923,7 +1172,7 @@ impl Node {
|
|||
NodeKind::Mesh(m) => m.transform(a),
|
||||
NodeKind::Repeat(r) => r.transform(a, *sc),
|
||||
}
|
||||
if let Some(m) = &mut self.mask
|
||||
if let Some(m) = self.extras_if_any().and_then(|e| e.mask.as_mut())
|
||||
&& m.linked
|
||||
{
|
||||
Arc::make_mut(&mut m.art).transform_scaled(a, sc);
|
||||
|
|
@ -1039,7 +1288,7 @@ impl Node {
|
|||
}
|
||||
}
|
||||
NodeKind::Group { children, .. } | NodeKind::Layer { children, .. } => {
|
||||
for c in children.iter().skip(usize::from(self.shaper.is_some())).filter(|c| c.visible) {
|
||||
for c in children.iter().skip(usize::from(self.extras().shaper.is_some())).filter(|c| c.visible) {
|
||||
c.push_clip_shapes(text, out);
|
||||
}
|
||||
}
|
||||
|
|
@ -1324,25 +1573,25 @@ impl ImageMap {
|
|||
impl Node {
|
||||
/// The URL this object links to, if any.
|
||||
pub fn url(&self) -> Option<&str> {
|
||||
self.attrs.as_deref().map(|a| a.url.as_str()).filter(|u| !u.is_empty())
|
||||
self.extras().attrs.as_deref().map(|a| a.url.as_str()).filter(|u| !u.is_empty())
|
||||
}
|
||||
|
||||
/// Whether the canvas shows this object's centre point when it is selected: as set in the
|
||||
/// Attributes panel, else [`Self::shows_center_by_default`].
|
||||
pub fn shows_center(&self) -> bool {
|
||||
self.attrs.as_deref().and_then(|a| a.show_center).unwrap_or_else(|| self.shows_center_by_default())
|
||||
self.extras().attrs.as_deref().and_then(|a| a.show_center).unwrap_or_else(|| self.shows_center_by_default())
|
||||
}
|
||||
|
||||
/// Shapes drawn with the shape tools (live rectangles, ellipses and polygons) show their centre.
|
||||
pub fn shows_center_by_default(&self) -> bool {
|
||||
matches!(&self.kind, NodeKind::Path { live: Some(l), .. } if !matches!(l, LiveShape::Line { .. } | LiveShape::Path { .. }))
|
||||
matches!(&self.kind, NodeKind::Path { live: Some(l), .. } if !matches!(**l, LiveShape::Line { .. } | LiveShape::Path { .. }))
|
||||
}
|
||||
|
||||
/// Change this object's attributes with `f`; all-default attributes are dropped.
|
||||
pub fn edit_attrs<R>(&mut self, f: impl FnOnce(&mut ObjectAttributes) -> R) -> R {
|
||||
let mut a = self.attrs.take().unwrap_or_default();
|
||||
let mut a = self.extras_mut().attrs.take().unwrap_or_default();
|
||||
let r = f(&mut a);
|
||||
self.attrs = (*a != ObjectAttributes::default()).then_some(a);
|
||||
self.extras_mut().attrs = (*a != ObjectAttributes::default()).then_some(a);
|
||||
r
|
||||
}
|
||||
}
|
||||
|
|
@ -1358,7 +1607,7 @@ impl Node {
|
|||
self.visible
|
||||
&& (self.opacity < 1.0
|
||||
|| self.blend != BlendMode::Normal
|
||||
|| self.mask.as_ref().is_some_and(|m| !m.disabled)
|
||||
|| self.extras().mask.as_ref().is_some_and(|m| !m.disabled)
|
||||
|| effects(&self.appearance.effects)
|
||||
|| self
|
||||
.appearance
|
||||
|
|
@ -1454,13 +1703,14 @@ mod tests {
|
|||
let live = LiveShape::Rectangle { w, h, radii: [r; 4], kinds: Default::default(), xf };
|
||||
let mut n = Node::path(NodeId(1), live.to_path(), Appearance::default_art());
|
||||
if let NodeKind::Path { live: slot, .. } = &mut n.kind {
|
||||
*slot = Some(live);
|
||||
*slot = Some(Box::new(live));
|
||||
}
|
||||
n
|
||||
}
|
||||
|
||||
fn live_of(n: &Node) -> (f64, f64, [f64; 4], Affine) {
|
||||
let NodeKind::Path { live: Some(LiveShape::Rectangle { w, h, radii, xf, .. }), .. } = &n.kind else { panic!("not a live rectangle") };
|
||||
let NodeKind::Path { live: Some(l), .. } = &n.kind else { panic!("not a live shape") };
|
||||
let LiveShape::Rectangle { w, h, radii, xf, .. } = &**l else { panic!("not a live rectangle") };
|
||||
(*w, *h, *radii, *xf)
|
||||
}
|
||||
|
||||
|
|
@ -1595,7 +1845,7 @@ mod tests {
|
|||
|n| n.knockout = Knockout::On,
|
||||
|n| n.knockout = Knockout::Off,
|
||||
|n| n.knockout_shape = true,
|
||||
|n| n.mask = Some(Box::new(OpacityMask::new(n.clone(), true))),
|
||||
|n| n.extras_mut().mask = Some(Box::new(OpacityMask::new(n.clone(), true))),
|
||||
];
|
||||
for tweak in tweaks {
|
||||
let mut n = p();
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ impl Node {
|
|||
/// The projective map type or a symbol instance in perspective is drawn through (not while
|
||||
/// Edit Text shows it flat).
|
||||
pub fn projection(&self) -> Option<Homography> {
|
||||
let a = self.perspective.as_deref().filter(|a| !a.editing)?;
|
||||
let a = self.extras().perspective.as_deref().filter(|a| !a.editing)?;
|
||||
matches!(self.kind, NodeKind::Text(_) | NodeKind::SymbolInstance { .. }).then(|| a.homography()).flatten()
|
||||
}
|
||||
|
||||
|
|
@ -60,7 +60,7 @@ impl Node {
|
|||
|
||||
/// Keep a projection drawing the same picture moved by `a` (the flat art moves by `a` too).
|
||||
pub(crate) fn transform_projection(&mut self, a: vectorcraft_geom::Affine) {
|
||||
if let Some(p) = self.perspective.as_deref_mut()
|
||||
if let Some(p) = self.extras_if_any().and_then(|e| e.perspective.as_deref_mut())
|
||||
&& let Some(h) = p.homography().and_then(|h| h.conjugated(a))
|
||||
{
|
||||
p.projection = Some(h.to_array());
|
||||
|
|
|
|||
|
|
@ -119,7 +119,7 @@ fn prefix(key: &str) -> String {
|
|||
/// `n` and every descendant, opacity-mask art included.
|
||||
fn walk_all<'a>(n: &'a Node, f: &mut impl FnMut(&'a Node)) {
|
||||
f(n);
|
||||
if let Some(m) = &n.mask {
|
||||
if let Some(m) = &n.extras().mask {
|
||||
walk_all(&m.art, f);
|
||||
}
|
||||
for c in n.children().into_iter().flatten() {
|
||||
|
|
@ -184,7 +184,7 @@ impl Document {
|
|||
}
|
||||
// The art shown may hold placed documents too.
|
||||
let n = Arc::make_mut(n);
|
||||
if let Some(m) = &mut n.mask {
|
||||
if let Some(m) = n.extras_if_any().and_then(|e| e.mask.as_mut()) {
|
||||
update(&mut m.art);
|
||||
}
|
||||
for c in n.children_mut().into_iter().flatten() {
|
||||
|
|
@ -210,11 +210,11 @@ impl Document {
|
|||
&& keys.contains(&p.key)
|
||||
{
|
||||
if let Some(im) = images.get(&p.key).and_then(|b| p.preview_image(&b.bytes)) {
|
||||
n.kind = NodeKind::Image(im);
|
||||
n.kind = NodeKind::Image(Box::new(im));
|
||||
}
|
||||
return;
|
||||
}
|
||||
if let Some(m) = &mut n.mask {
|
||||
if let Some(m) = n.extras_if_any().and_then(|e| e.mask.as_mut()) {
|
||||
update(&mut m.art, keys, images);
|
||||
}
|
||||
for c in n.children_mut().into_iter().flatten() {
|
||||
|
|
@ -406,7 +406,7 @@ fn read_preview(p: &PlacedDocument, bytes: &Arc<Vec<u8>>) -> Option<Arc<ReadDocu
|
|||
doc.images.insert("preview".into(), ImageBlob { mime: mime.into(), bytes: bytes.clone(), proxy: None });
|
||||
let xf = Affine::scale_non_uniform(frame.width() / w as f64, frame.height() / h as f64);
|
||||
let im = ImageObject { key: "preview".into(), width: w, height: h, xf, link: None, placement: Default::default() };
|
||||
let art = Node::new(art_id(), NodeKind::Group { children: vec![Arc::new(Node::new(art_id(), NodeKind::Image(im)))], clip: false });
|
||||
let art = Node::new(art_id(), NodeKind::Group { children: vec![Arc::new(Node::new(art_id(), NodeKind::Image(Box::new(im))))], clip: false });
|
||||
let e = Arc::new(ReadDocument { key: p.key.clone(), preview: true, bytes: bytes.clone(), frame, art: Arc::new(art), doc: Arc::new(doc) });
|
||||
cache().insert(e.clone());
|
||||
Some(e)
|
||||
|
|
@ -432,7 +432,7 @@ fn art_id() -> crate::NodeId {
|
|||
/// Visit `n` and every descendant (opacity-mask art included) mutably.
|
||||
fn each_mut(n: &mut Node, f: &mut impl FnMut(&mut Node)) {
|
||||
f(n);
|
||||
if let Some(m) = &mut n.mask {
|
||||
if let Some(m) = n.extras_if_any().and_then(|e| e.mask.as_mut()) {
|
||||
each_mut(Arc::make_mut(&mut m.art), f);
|
||||
}
|
||||
for c in n.children_mut().into_iter().flatten() {
|
||||
|
|
@ -590,7 +590,14 @@ mod tests {
|
|||
d.layers.clear();
|
||||
let art = Node::new(
|
||||
NodeId(50),
|
||||
NodeKind::Image(ImageObject { key: "pic".into(), width: 1, height: 1, xf: Affine::IDENTITY, link: None, placement: Default::default() }),
|
||||
NodeKind::Image(Box::new(ImageObject {
|
||||
key: "pic".into(),
|
||||
width: 1,
|
||||
height: 1,
|
||||
xf: Affine::IDENTITY,
|
||||
link: None,
|
||||
placement: Default::default(),
|
||||
})),
|
||||
);
|
||||
d.symbols.push(Symbol { name: "Star".into(), art: Arc::new(art) });
|
||||
d.images.insert("pic".into(), ImageBlob::new("image/png", vec![1, 2, 3]));
|
||||
|
|
|
|||
|
|
@ -177,13 +177,13 @@ impl Document {
|
|||
}
|
||||
/// Is `id` a user slice or an object with slice options?
|
||||
pub fn is_slice(&self, id: NodeId) -> bool {
|
||||
self.slice(id).is_some() || self.node(id).is_some_and(|n| n.slice.is_some())
|
||||
self.slice(id).is_some() || self.node(id).is_some_and(|n| n.extras().slice.is_some())
|
||||
}
|
||||
/// The options of slice `id` (user or object).
|
||||
pub fn slice_options(&self, id: NodeId) -> Option<&SliceOptions> {
|
||||
match self.slice(id) {
|
||||
Some(s) => Some(&s.options),
|
||||
None => self.node(id)?.slice.as_deref(),
|
||||
None => self.node(id)?.extras().slice.as_deref(),
|
||||
}
|
||||
}
|
||||
/// The rectangle of slice `id` before clipping: a user slice's own, an object slice's object's
|
||||
|
|
@ -191,14 +191,14 @@ impl Document {
|
|||
pub fn slice_bounds(&self, id: NodeId) -> Option<Rect> {
|
||||
match self.slice(id) {
|
||||
Some(s) => Some(s.rect),
|
||||
None => self.node(id).filter(|n| n.slice.is_some())?.visual_bounds(),
|
||||
None => self.node(id).filter(|n| n.extras().slice.is_some())?.visual_bounds(),
|
||||
}
|
||||
}
|
||||
/// The visible objects with slice options and their visual bounds, in paint order.
|
||||
pub fn object_slices(&self) -> Vec<(NodeId, Rect)> {
|
||||
fn walk(nodes: &[Arc<Node>], out: &mut Vec<(NodeId, Rect)>) {
|
||||
for n in nodes.iter().filter(|n| n.visible) {
|
||||
if n.slice.is_some()
|
||||
if n.extras().slice.is_some()
|
||||
&& let Some(b) = n.visual_bounds()
|
||||
{
|
||||
out.push((n.id, b));
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ fn an_object_slice_follows_its_object_and_numbers_run_by_rows() {
|
|||
let mut d = Document::new(300.0, 200.0);
|
||||
let a = rect(&mut d, Rect::new(100.0, 50.0, 200.0, 150.0));
|
||||
assert!(d.slice_layout().is_empty(), "no slices, no auto slices");
|
||||
d.node_mut(a).unwrap().slice = Some(Box::default());
|
||||
d.node_mut(a).unwrap().extras_mut().slice = Some(Box::default());
|
||||
let lay = d.slice_layout();
|
||||
let own = lay.iter().find(|s| s.id == Some(a)).unwrap();
|
||||
assert_eq!((own.source, own.rect), (SliceSource::Object, Rect::new(100.0, 50.0, 200.0, 150.0)));
|
||||
|
|
@ -69,14 +69,15 @@ fn clip_to_artboard_clips_slices_and_off_covers_the_art() {
|
|||
fn slices_survive_a_native_round_trip_and_old_files_load() {
|
||||
let mut d = Document::new(100.0, 100.0);
|
||||
let a = rect(&mut d, Rect::new(10.0, 10.0, 20.0, 20.0));
|
||||
d.node_mut(a).unwrap().slice = Some(Box::new(SliceOptions { kind: crate::SliceKind::NoImage, text: "<b>hi</b>".into(), ..Default::default() }));
|
||||
d.node_mut(a).unwrap().extras_mut().slice =
|
||||
Some(Box::new(SliceOptions { kind: crate::SliceKind::NoImage, text: "<b>hi</b>".into(), ..Default::default() }));
|
||||
let s = user(&mut d, Rect::new(30.0, 30.0, 60.0, 60.0));
|
||||
d.slice_mut(s).unwrap().options.url = "https://example.com".into();
|
||||
d.slices_clip_to_artboard = false;
|
||||
let json = serde_json::to_string(&d).unwrap();
|
||||
let back: Document = serde_json::from_str(&json).unwrap();
|
||||
assert_eq!(back.slices, d.slices);
|
||||
assert_eq!(back.node(a).unwrap().slice, d.node(a).unwrap().slice);
|
||||
assert_eq!(back.node(a).unwrap().extras().slice, d.node(a).unwrap().extras().slice);
|
||||
assert!(!back.slices_clip_to_artboard);
|
||||
// A document without slices writes none of the new keys (older readers see what they knew).
|
||||
let plain = serde_json::to_string(&Document::new(10.0, 10.0)).unwrap();
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ impl TraceView {
|
|||
impl Node {
|
||||
/// The View of an Image Trace object (the tracing result for anything else, or an unknown id).
|
||||
pub fn trace_view(&self) -> TraceView {
|
||||
self.trace.as_deref().map(TraceView::of).unwrap_or_default()
|
||||
self.extras().trace.as_deref().map(TraceView::of).unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -84,9 +84,9 @@ mod tests {
|
|||
}
|
||||
let mut g = Node::group(crate::NodeId(1), vec![]);
|
||||
assert_eq!(g.trace_view(), TraceView::Result);
|
||||
g.trace = Some(Box::new(serde_json::json!({"view": "outlines"})));
|
||||
g.extras_mut().trace = Some(Box::new(serde_json::json!({"view": "outlines"})));
|
||||
assert_eq!(g.trace_view(), TraceView::Outlines);
|
||||
g.trace = Some(Box::new(serde_json::json!({"view": "sepia"})));
|
||||
g.extras_mut().trace = Some(Box::new(serde_json::json!({"view": "sepia"})));
|
||||
assert_eq!(g.trace_view(), TraceView::Result);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -163,24 +163,25 @@ pub fn fresh_ids(d: &mut Document, n: &mut Node, seen: &mut std::collections::Ha
|
|||
fn piece(d: &mut Document, n: &Node, path: PathData, rule: FillRule, item: AppearanceItem) -> Node {
|
||||
let id = d.alloc_id();
|
||||
let kind = path_kind(d, path, rule);
|
||||
Node {
|
||||
id,
|
||||
name: None,
|
||||
appearance: Appearance { items: vec![item], ..Default::default() }.into(),
|
||||
opacity: 1.0,
|
||||
blend: Default::default(),
|
||||
isolate: false,
|
||||
knockout: Default::default(),
|
||||
knockout_shape: false,
|
||||
mask: None,
|
||||
trace: None,
|
||||
wrap: None,
|
||||
graph: None,
|
||||
// The group keeps the URL (one link around the pieces).
|
||||
attrs: None,
|
||||
kind,
|
||||
..n.clone()
|
||||
}
|
||||
let mut out = n.clone();
|
||||
out.id = id;
|
||||
out.name = None;
|
||||
out.appearance = Appearance { items: vec![item], ..Default::default() }.into();
|
||||
out.opacity = 1.0;
|
||||
out.blend = Default::default();
|
||||
out.isolate = false;
|
||||
out.knockout = Default::default();
|
||||
out.knockout_shape = false;
|
||||
out.kind = kind;
|
||||
let mut extras = out.extras_mut();
|
||||
extras.mask = None;
|
||||
extras.trace = None;
|
||||
extras.wrap = None;
|
||||
extras.graph = None;
|
||||
// The group keeps the URL (one link around the pieces).
|
||||
extras.attrs = None;
|
||||
drop(extras);
|
||||
out
|
||||
}
|
||||
|
||||
/// `n`'s geometry with its object-level geometry effects applied, its fill rule, the context of
|
||||
|
|
|
|||
|
|
@ -32,8 +32,8 @@ pub fn crop_marks_art(n: &Node) -> Option<Node> {
|
|||
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.mask = object.mask.take();
|
||||
g.attrs = object.attrs.take();
|
||||
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)];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -92,7 +92,7 @@ pub fn outline_art(n: &Node, symbol_art: Option<&Node>) -> Option<Node> {
|
|||
art.isolate = false;
|
||||
// One element: its pieces never knock each other out.
|
||||
art.knockout = vectorcraft_doc::Knockout::Off;
|
||||
art.mask = None;
|
||||
art.extras_mut().mask = None;
|
||||
art.appearance.effects.clear();
|
||||
Some(art)
|
||||
}
|
||||
|
|
@ -141,7 +141,7 @@ pub(crate) fn as_art(n: &Node, symbol_art: Option<&Node>) -> Option<Node> {
|
|||
if let NodeKind::Path { clipping, .. } = &mut clip.kind {
|
||||
*clipping = true;
|
||||
}
|
||||
let image = Node { appearance: Appearance::default().into(), ..Node::new(n.id, n.kind.clone()) };
|
||||
let image = Node::new(n.id, n.kind.clone());
|
||||
Node::new(n.id, NodeKind::Group { children: vec![Arc::new(clip), Arc::new(image)], clip: true })
|
||||
}
|
||||
_ => outline_art(n, symbol_art)?,
|
||||
|
|
@ -159,7 +159,7 @@ pub(crate) fn as_art(n: &Node, symbol_art: Option<&Node>) -> Option<Node> {
|
|||
out.isolate = n.isolate;
|
||||
// One element: its pieces never knock each other out.
|
||||
out.knockout = vectorcraft_doc::Knockout::Off;
|
||||
out.mask = n.mask.clone();
|
||||
out.extras_mut().mask = n.extras().mask.clone();
|
||||
out.appearance.effects = n.appearance.effects.iter().filter(|e| !is_geometry(&e.id)).cloned().collect();
|
||||
Some(out)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@ fn fill_rule_owner(d: &Document, id: NodeId) -> Option<&Node> {
|
|||
/// The Attributes panel's values of `info.ids` beyond overprinting.
|
||||
pub(super) fn fill_info(d: &Document, info: &mut AttributesInfo) {
|
||||
let nodes: Vec<&Node> = info.ids.iter().filter_map(|id| d.node(*id)).collect();
|
||||
let attrs = || nodes.iter().map(|n| n.attrs.as_deref().cloned().unwrap_or_default());
|
||||
let attrs = || nodes.iter().map(|n| n.extras().attrs.as_deref().cloned().unwrap_or_default());
|
||||
info.show_center = same(nodes.iter().map(|n| n.shows_center()));
|
||||
info.image_map = same(attrs().map(|a| a.image_map));
|
||||
info.url = same(attrs().map(|a| a.url));
|
||||
|
|
@ -126,7 +126,7 @@ impl Edits {
|
|||
|
||||
/// `n`'s attributes with the edits applied (Show Center kept only where it isn't the default).
|
||||
fn apply(&self, n: &Node) -> ObjectAttributes {
|
||||
let mut a = n.attrs.as_deref().cloned().unwrap_or_default();
|
||||
let mut a = n.extras().attrs.as_deref().cloned().unwrap_or_default();
|
||||
if let Some(v) = self.show_center {
|
||||
a.show_center = (v != n.shows_center_by_default()).then_some(v);
|
||||
}
|
||||
|
|
@ -158,7 +158,7 @@ fn set(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
for id in &ids {
|
||||
let Some(n) = d.node(*id) else { continue };
|
||||
let new = edits.apply(n);
|
||||
if new != n.attrs.as_deref().cloned().unwrap_or_default()
|
||||
if new != n.extras().attrs.as_deref().cloned().unwrap_or_default()
|
||||
&& let Some(n) = d.node_mut(*id)
|
||||
{
|
||||
n.edit_attrs(|a| *a = new);
|
||||
|
|
|
|||
|
|
@ -553,7 +553,7 @@ fn lp_expand(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
for g in &targets {
|
||||
let n = d.node_mut(*g).ok_or(EngineError::NoNode(*g))?;
|
||||
n.name = None;
|
||||
n.trace = None;
|
||||
n.extras_mut().trace = None;
|
||||
if let Some(ch) = n.children_mut() {
|
||||
ch.retain(|c| match c.name.as_deref() {
|
||||
Some(SOURCES_NAME) => false,
|
||||
|
|
@ -790,7 +790,7 @@ fn trace_make(s: &mut Session, p: &Value, expand: bool) -> Result<Value> {
|
|||
if !expand {
|
||||
// Tracing again keeps the object's view unless another is asked for.
|
||||
let view = view.unwrap_or_else(|| src.trace_view());
|
||||
g.trace = Some(Box::new(json!({ "preset": preset_label, "params": params_json, "view": view.id() })));
|
||||
g.extras_mut().trace = Some(Box::new(json!({ "preset": preset_label, "params": params_json, "view": view.id() })));
|
||||
}
|
||||
let (par, idx, _) = d.position(target).ok_or(EngineError::NoNode(target))?;
|
||||
d.remove(target)?;
|
||||
|
|
@ -821,10 +821,10 @@ fn trace_set_view(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
s.edit("Image Trace View", |d, _| {
|
||||
for g in &targets {
|
||||
let n = d.node_mut(*g).ok_or(EngineError::NoNode(*g))?;
|
||||
if let Some(t) = n.trace.as_deref_mut().and_then(Value::as_object_mut) {
|
||||
if let Some(t) = n.extras_if_any().and_then(|e| e.trace.as_deref_mut()).and_then(Value::as_object_mut) {
|
||||
t.insert("view".into(), json!(view.id()));
|
||||
} else {
|
||||
n.trace = Some(Box::new(json!({ "preset": "Custom", "view": view.id() })));
|
||||
n.extras_mut().trace = Some(Box::new(json!({ "preset": "Custom", "view": view.id() })));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
|
|
@ -854,7 +854,7 @@ fn trace_expand(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
for g in &targets {
|
||||
let n = d.node_mut(*g).ok_or(EngineError::NoNode(*g))?;
|
||||
n.name = None;
|
||||
n.trace = None;
|
||||
n.extras_mut().trace = None;
|
||||
if let Some(ch) = n.children_mut() {
|
||||
ch.retain(|c| !matches!(c.kind, NodeKind::Image(_)));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -315,7 +315,7 @@ fn import_image(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
let (w, h) = (img.width, img.height);
|
||||
let image =
|
||||
ImageObject { key: img.key.clone(), width: w, height: h, xf: Affine::scale_non_uniform(sx, sy), link: None, placement: Default::default() };
|
||||
let mut clip = Clipboard { nodes: vec![Node::new(NodeId(1), NodeKind::Image(image))], ..Default::default() };
|
||||
let mut clip = Clipboard { nodes: vec![Node::new(NodeId(1), NodeKind::Image(Box::new(image)))], ..Default::default() };
|
||||
clip.images.insert(img.key, img.blob);
|
||||
let count = s.load_clipboard(clip, p)?;
|
||||
Ok(json!({ "count": count, "width": w as f64 * sx, "height": h as f64 * sy, "pixelWidth": w, "pixelHeight": h }))
|
||||
|
|
|
|||
|
|
@ -107,10 +107,10 @@ fn attrs_refs(attrs: &Map<String, Value>, f: Visit) {
|
|||
fn node_refs(n: &Node, f: Visit) {
|
||||
n.walk(&mut |m| {
|
||||
appearance_refs(&m.appearance, f);
|
||||
if let Some(id) = m.graphic_style {
|
||||
if let Some(id) = m.extras().graphic_style {
|
||||
f(Ref::Style(id));
|
||||
}
|
||||
if let Some(mask) = &m.mask {
|
||||
if let Some(mask) = &m.extras().mask {
|
||||
node_refs(&mask.art, f);
|
||||
}
|
||||
match &m.kind {
|
||||
|
|
@ -427,8 +427,8 @@ impl Relink {
|
|||
/// Relink a node tree (the mirror of [`node_refs`]).
|
||||
fn node(&self, n: &mut Node) {
|
||||
self.appearance(&mut n.appearance);
|
||||
n.graphic_style = n.graphic_style.and_then(|id| self.styles.get(&id).copied());
|
||||
if let Some(m) = &mut n.mask {
|
||||
n.extras_mut().graphic_style = n.extras().graphic_style.and_then(|id| self.styles.get(&id).copied());
|
||||
if let Some(m) = n.extras_if_any().and_then(|e| e.mask.as_mut()) {
|
||||
self.node(Arc::make_mut(&mut m.art));
|
||||
}
|
||||
match &mut n.kind {
|
||||
|
|
|
|||
|
|
@ -157,7 +157,7 @@ fn draw_mode(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
}
|
||||
|
||||
fn path_kind(path: PathData, live: Option<LiveShape>) -> NodeKind {
|
||||
NodeKind::Path { path, rule: FillRule::NonZero, live, clipping: false, guide: false }
|
||||
NodeKind::Path { path, rule: FillRule::NonZero, live: live.map(Box::new), clipping: false, guide: false }
|
||||
}
|
||||
|
||||
fn rect_of(p: &Value, cmd: &str) -> Result<Rect> {
|
||||
|
|
|
|||
|
|
@ -439,7 +439,7 @@ struct Targets {
|
|||
/// What `n` is when Liquify can't distort it (and doesn't look inside it).
|
||||
fn not_liquified(n: &Node) -> Option<&'static str> {
|
||||
Some(match &n.kind {
|
||||
NodeKind::Group { .. } if n.graph.is_some() => "graphs",
|
||||
NodeKind::Group { .. } if n.extras().graph.is_some() => "graphs",
|
||||
NodeKind::Text(_) => "type",
|
||||
NodeKind::SymbolInstance { .. } => "symbols",
|
||||
NodeKind::Image(_) => "images",
|
||||
|
|
@ -1252,7 +1252,7 @@ fn edit_text(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
let (Some(flat), Some(drawn)) = (t.bounds(), t.bounds().and_then(|b| h.map_rect_bbox(b))) else { return Ok(()) };
|
||||
let shift = vectorcraft_geom::Affine::translate(drawn.center() - flat.center());
|
||||
t.transform(shift);
|
||||
if let Some(rec) = n.perspective.as_deref_mut() {
|
||||
if let Some(rec) = n.extras_if_any().and_then(|e| e.perspective.as_deref_mut()) {
|
||||
rec.projection = Some(h.then_after(&Homography::from_affine(shift.inverse())).to_array());
|
||||
rec.editing = true;
|
||||
}
|
||||
|
|
@ -1271,8 +1271,8 @@ pub(crate) fn finish_edit_text(st: &mut crate::DocState, id: NodeId) {
|
|||
let clear = |doc: &mut Arc<Document>| {
|
||||
// Undo steps keep no lookup cache: look without building one, and drop the one an edit
|
||||
// builds (the document now keeps its own).
|
||||
if doc.node_uncached(id).and_then(|n| n.perspective.as_deref()).is_some_and(|p| p.editing)
|
||||
&& let Some(p) = Arc::make_mut(doc).node_mut(id).and_then(|n| n.perspective.as_deref_mut())
|
||||
if doc.node_uncached(id).and_then(|n| n.extras().perspective.as_deref()).is_some_and(|p| p.editing)
|
||||
&& let Some(p) = Arc::make_mut(doc).node_mut(id).and_then(|n| n.extras_if_any().and_then(|e| e.perspective.as_deref_mut()))
|
||||
{
|
||||
p.editing = false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -290,7 +290,7 @@ fn info(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
spot.insert(name.clone());
|
||||
}
|
||||
}
|
||||
if n.mask.is_some() {
|
||||
if n.extras().mask.is_some() {
|
||||
*counts.entry("opacityMasks").or_default() += 1;
|
||||
}
|
||||
if !n.appearance.effects.is_empty() {
|
||||
|
|
|
|||
|
|
@ -214,11 +214,12 @@ fn undo(s: &mut Session, _: &Value) -> Result<Value> {
|
|||
let st = s.doc_mut()?;
|
||||
let e = st.history.undo.pop().ok_or_else(|| EngineError::Other("nothing to undo".into()))?;
|
||||
let label = e.label.clone();
|
||||
let costs = e.costs();
|
||||
let editing = st.doc.mask_edit.map(|m| m.layer);
|
||||
let doc = std::mem::replace(&mut st.doc, e.doc);
|
||||
let selection = std::mem::replace(&mut st.selection, Arc::unwrap_or_clone(e.selection));
|
||||
let selection = shared_selection(selection, st.history.redo.last());
|
||||
st.history.redo.push(crate::HistoryEntry::kept(e.label, doc, selection, &st.doc));
|
||||
st.history.redo.push(crate::HistoryEntry::turned(e.label, doc, selection, &st.doc, costs));
|
||||
st.history.trim();
|
||||
st.revision += 1;
|
||||
st.selection.prune(&st.doc);
|
||||
|
|
@ -230,11 +231,12 @@ fn redo(s: &mut Session, _: &Value) -> Result<Value> {
|
|||
let st = s.doc_mut()?;
|
||||
let e = st.history.redo.pop().ok_or_else(|| EngineError::Other("nothing to redo".into()))?;
|
||||
let label = e.label.clone();
|
||||
let costs = e.costs();
|
||||
let editing = st.doc.mask_edit.map(|m| m.layer);
|
||||
let doc = std::mem::replace(&mut st.doc, e.doc);
|
||||
let selection = std::mem::replace(&mut st.selection, Arc::unwrap_or_clone(e.selection));
|
||||
let selection = shared_selection(selection, st.history.undo.last());
|
||||
st.history.undo.push(crate::HistoryEntry::kept(e.label, doc, selection, &st.doc));
|
||||
st.history.undo.push(crate::HistoryEntry::turned(e.label, doc, selection, &st.doc, costs));
|
||||
st.history.trim();
|
||||
st.revision += 1;
|
||||
st.selection.prune(&st.doc);
|
||||
|
|
|
|||
|
|
@ -289,7 +289,7 @@ impl Builder {
|
|||
// shows outside it, as an opacity mask that clips.
|
||||
if let Some(kind) = self.image(img, to_doc) {
|
||||
let art = Node::new(self.doc.alloc_id(), kind);
|
||||
out.mask = Some(Box::new(OpacityMask::new(art, true)));
|
||||
out.extras_mut().mask = Some(Box::new(OpacityMask::new(art, true)));
|
||||
}
|
||||
}
|
||||
if let Some(mask) = &n.mask {
|
||||
|
|
@ -514,7 +514,7 @@ impl Builder {
|
|||
let sy = img.height as f64 / f64::from(r.height.max(1));
|
||||
let xf = to_doc * affine(img.transform) * Affine::scale_non_uniform(sx, sy);
|
||||
self.doc.images.entry(r.key.clone()).or_insert(r.blob);
|
||||
Some(NodeKind::Image(ImageObject { key: r.key, width: r.width, height: r.height, xf, link: None, placement: Default::default() }))
|
||||
Some(NodeKind::Image(Box::new(ImageObject { key: r.key, width: r.width, height: r.height, xf, link: None, placement: Default::default() })))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@ fn collect(n: &Node, region: Rect, scale: f64, out: &mut Vec<Area>) {
|
|||
if !n.visible || matches!(n.kind, NodeKind::Layer { template: true, .. }) {
|
||||
return;
|
||||
}
|
||||
let shape = n.attrs.as_deref().filter(|a| !a.url.is_empty()).map(|a| a.image_map).unwrap_or_default();
|
||||
let shape = n.extras().attrs.as_deref().filter(|a| !a.url.is_empty()).map(|a| a.image_map).unwrap_or_default();
|
||||
let px = |x: f64, y: f64| [((x - region.x0) * scale).round() as i64, ((y - region.y0) * scale).round() as i64];
|
||||
if let (Some(href), Some(b)) = (n.url(), n.visual_bounds().filter(|b| b.overlaps(region)))
|
||||
&& shape != ImageMap::None
|
||||
|
|
|
|||
|
|
@ -430,7 +430,8 @@ pub(super) fn raster_doc(name: &str, bytes: &[u8]) -> Result<Document> {
|
|||
let layer = d.layers[0].id;
|
||||
let id = d.alloc_id();
|
||||
let xf = Affine::scale_non_uniform(sx, sy);
|
||||
let mut n = Node::new(id, NodeKind::Image(ImageObject { key: key.clone(), width, height, xf, link: None, placement: Default::default() }));
|
||||
let mut n =
|
||||
Node::new(id, NodeKind::Image(Box::new(ImageObject { key: key.clone(), width, height, xf, link: None, placement: Default::default() })));
|
||||
n.name = Some(name.to_string());
|
||||
d.images.insert(key, blob);
|
||||
d.insert(Some(layer), 0, n).map_err(err)?;
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ fn image_bytes(w: u32, h: u32, format: image::ImageFormat) -> Vec<u8> {
|
|||
fn image_of(s: &Session) -> vectorcraft_doc::ImageObject {
|
||||
let doc = &s.doc().unwrap().doc;
|
||||
match &doc.layers[0].children().unwrap()[0].kind {
|
||||
NodeKind::Image(im) => im.clone(),
|
||||
NodeKind::Image(im) => (**im).clone(),
|
||||
other => panic!("expected an image, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ fn images(s: &Session) -> Vec<ImageObject> {
|
|||
let mut out = vec![];
|
||||
s.documents()[s.active_index().unwrap()].doc.walk(|n| {
|
||||
if let NodeKind::Image(im) = &n.kind {
|
||||
out.push(im.clone());
|
||||
out.push((**im).clone());
|
||||
}
|
||||
});
|
||||
out
|
||||
|
|
|
|||
|
|
@ -1093,7 +1093,7 @@ fn raster_node(d: &mut Document, r: Raster) -> Node {
|
|||
d.images.insert(key.clone(), ImageBlob::new("image/png", r.png));
|
||||
let image = Node::new(
|
||||
d.alloc_id(),
|
||||
NodeKind::Image(ImageObject { key, width: r.width, height: r.height, xf: r.xf, link: None, placement: Default::default() }),
|
||||
NodeKind::Image(Box::new(ImageObject { key, width: r.width, height: r.height, xf: r.xf, link: None, placement: Default::default() })),
|
||||
);
|
||||
let Some(path) = r.clip else { return image };
|
||||
super::rasterfx::clip_group(d, path, FillRule::NonZero, image)
|
||||
|
|
@ -1224,7 +1224,7 @@ fn carry(from: &Node, mut to: Node) -> Node {
|
|||
to.blend = from.blend;
|
||||
to.isolate = from.isolate;
|
||||
to.knockout_shape = from.knockout_shape;
|
||||
to.mask = from.mask.clone();
|
||||
to.extras_mut().mask = from.extras().mask.clone();
|
||||
to.appearance.effects = from.appearance.effects.clone();
|
||||
to
|
||||
}
|
||||
|
|
@ -1258,7 +1258,7 @@ fn has_effects(n: &Node) -> bool {
|
|||
}
|
||||
|
||||
fn masked(n: &Node) -> bool {
|
||||
n.mask.as_ref().is_some_and(|m| !m.disabled)
|
||||
n.extras().mask.as_ref().is_some_and(|m| !m.disabled)
|
||||
}
|
||||
|
||||
/// Where `n` can paint: its visual bounds grown by its effects (and a margin for antialiasing).
|
||||
|
|
|
|||
|
|
@ -389,7 +389,7 @@ impl Gen<'_> {
|
|||
fn series(&mut self, name: &str, index: usize, children: Vec<Arc<Node>>) -> Arc<Node> {
|
||||
let mut g = Node::group(self.d.alloc_id(), children);
|
||||
g.name = Some(name.into());
|
||||
g.series_index = u32::try_from(index).ok();
|
||||
g.extras_mut().series_index = u32::try_from(index).ok();
|
||||
Arc::new(g)
|
||||
}
|
||||
}
|
||||
|
|
@ -1146,7 +1146,7 @@ fn children_bounds(children: &[Arc<Node>]) -> Option<Rect> {
|
|||
/// generation, which is recorded in the spec's `placed` bounds).
|
||||
fn regenerate(d: &mut Document, id: NodeId, mut spec: GraphSpec) -> Result<()> {
|
||||
let n = d.node(id).ok_or(EngineError::NoNode(id))?;
|
||||
if let (Some(placed), Some(now)) = (n.graph.as_ref().and_then(|g| g.placed), n.children().and_then(|c| children_bounds(c)))
|
||||
if let (Some(placed), Some(now)) = (n.extras().graph.as_ref().and_then(|g| g.placed), n.children().and_then(|c| children_bounds(c)))
|
||||
&& placed.width() > 1e-9
|
||||
&& placed.height() > 1e-9
|
||||
&& (placed.x0 - now.x0).abs() + (placed.y0 - now.y0).abs() + (placed.x1 - now.x1).abs() + (placed.y1 - now.y1).abs() > 1e-6
|
||||
|
|
@ -1162,7 +1162,7 @@ fn regenerate(d: &mut Document, id: NodeId, mut spec: GraphSpec) -> Result<()> {
|
|||
if let Some(ch) = n.children_mut() {
|
||||
*ch = children;
|
||||
}
|
||||
n.graph = Some(Box::new(spec));
|
||||
n.extras_mut().graph = Some(Box::new(spec));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
@ -1199,7 +1199,7 @@ fn target(s: &Session, p: &Value, cmd: &str) -> Result<NodeId> {
|
|||
for id in start {
|
||||
let mut cur = Some(id);
|
||||
while let Some(c) = cur {
|
||||
if st.doc.node(c).is_some_and(|n| n.graph.is_some()) {
|
||||
if st.doc.node(c).is_some_and(|n| n.extras().graph.is_some()) {
|
||||
return Ok(c);
|
||||
}
|
||||
cur = st.doc.parent_of(c);
|
||||
|
|
@ -1209,7 +1209,7 @@ fn target(s: &Session, p: &Value, cmd: &str) -> Result<NodeId> {
|
|||
}
|
||||
|
||||
fn spec_of(s: &Session, id: NodeId) -> Result<GraphSpec> {
|
||||
s.doc()?.doc.node(id).and_then(|n| n.graph.as_deref().cloned()).ok_or(EngineError::NoNode(id))
|
||||
s.doc()?.doc.node(id).and_then(|n| n.extras().graph.as_deref().cloned()).ok_or(EngineError::NoNode(id))
|
||||
}
|
||||
|
||||
/// Where a node inside a graph's generated series art belongs: the graph, the series group and
|
||||
|
|
@ -1227,10 +1227,10 @@ struct SeriesRef {
|
|||
fn series_members(doc: &Document) -> HashMap<NodeId, SeriesRef> {
|
||||
let mut out = HashMap::new();
|
||||
doc.walk(|n| {
|
||||
let Some(spec) = n.graph.as_deref() else { return };
|
||||
let Some(spec) = n.extras().graph.as_deref() else { return };
|
||||
let count = spec.rows.iter().map(Vec::len).max().unwrap_or(0).clamp(1, vectorcraft_doc::MAX_GRAPH_SERIES);
|
||||
for group in n.children().into_iter().flatten() {
|
||||
let Some(index) = group.series_index.and_then(|i| usize::try_from(i).ok()).filter(|i| *i < count) else { continue };
|
||||
let Some(index) = group.extras().series_index.and_then(|i| usize::try_from(i).ok()).filter(|i| *i < count) else { continue };
|
||||
let r = SeriesRef { graph: n.id, group: group.id, index };
|
||||
group.walk(&mut |m| {
|
||||
out.insert(m.id, r);
|
||||
|
|
@ -1288,7 +1288,7 @@ pub(crate) fn capture_series_paints(doc: &mut Document, ids: &[NodeId], fill: bo
|
|||
.collect();
|
||||
let mut changed = false;
|
||||
for (graph, index, paint, width) in captures {
|
||||
let Some(spec) = doc.node_mut(graph).and_then(|n| n.graph.as_deref_mut()) else { continue };
|
||||
let Some(spec) = doc.node_mut(graph).and_then(|n| n.extras_if_any().and_then(|e| e.graph.as_deref_mut())) else { continue };
|
||||
let was = spec.series_paints.clone();
|
||||
// `index` is below the graph's series count (`series_members`).
|
||||
if spec.series_paints.len() <= index {
|
||||
|
|
@ -1342,9 +1342,9 @@ fn switch_xy(g: &mut GraphSpec) {
|
|||
fn graph_labels(doc: &Document) -> HashMap<NodeId, (NodeId, Role)> {
|
||||
let mut out = HashMap::new();
|
||||
doc.walk(|n| {
|
||||
let Some(g) = n.graph.as_deref() else { return };
|
||||
let Some(g) = n.extras().graph.as_deref() else { return };
|
||||
for c in n.children().into_iter().flatten() {
|
||||
match (&c.kind, c.name.as_deref(), c.series_index) {
|
||||
match (&c.kind, c.name.as_deref(), c.extras().series_index) {
|
||||
(NodeKind::Text(_), _, _) if g.kind == GraphKind::Pie => {
|
||||
out.insert(c.id, (n.id, Role::Category));
|
||||
}
|
||||
|
|
@ -1389,7 +1389,7 @@ pub(crate) fn capture_label_styles(doc: &mut Document, ids: &[NodeId], change: &
|
|||
kinds.dedup();
|
||||
let mut changed = false;
|
||||
for (graph, role) in kinds {
|
||||
let Some(spec) = doc.node_mut(graph).and_then(|n| n.graph.as_deref_mut()) else { continue };
|
||||
let Some(spec) = doc.node_mut(graph).and_then(|n| n.extras_if_any().and_then(|e| e.graph.as_deref_mut())) else { continue };
|
||||
let mut style = label_style(&spec.label_styles, role);
|
||||
change(&mut style);
|
||||
let slot = match role {
|
||||
|
|
@ -1470,7 +1470,7 @@ fn reselect_series(d: &Document, sel: &mut vectorcraft_doc::Selection, id: NodeI
|
|||
.and_then(Node::children)
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.filter(|c| c.series_index.is_some_and(|i| indexes.contains(&i)))
|
||||
.filter(|c| c.extras().series_index.is_some_and(|i| indexes.contains(&i)))
|
||||
.map(|c| c.id)
|
||||
.collect();
|
||||
sel.set(groups);
|
||||
|
|
@ -1485,12 +1485,12 @@ fn design_names(d: &Document) -> Vec<String> {
|
|||
fn refresh_graphs_using(d: &mut Document, name: &str) -> Result<()> {
|
||||
let mut ids = vec![];
|
||||
d.walk(|n| {
|
||||
if n.graph.as_deref().is_some_and(|g| g.series_markers.iter().flatten().any(|m| m == name)) {
|
||||
if n.extras().graph.as_deref().is_some_and(|g| g.series_markers.iter().flatten().any(|m| m == name)) {
|
||||
ids.push(n.id);
|
||||
}
|
||||
});
|
||||
for id in ids {
|
||||
if let Some(spec) = d.node(id).and_then(|n| n.graph.as_deref().cloned()) {
|
||||
if let Some(spec) = d.node(id).and_then(|n| n.extras().graph.as_deref().cloned()) {
|
||||
regenerate(d, id, spec)?;
|
||||
}
|
||||
}
|
||||
|
|
@ -1501,7 +1501,7 @@ fn refresh_graphs_using(d: &mut Document, name: &str) -> Result<()> {
|
|||
fn unused_designs(d: &Document) -> Vec<String> {
|
||||
let mut used = std::collections::HashSet::new();
|
||||
d.walk(|n| {
|
||||
if let Some(g) = n.graph.as_deref() {
|
||||
if let Some(g) = n.extras().graph.as_deref() {
|
||||
used.extend(g.series_markers.iter().flatten().cloned());
|
||||
}
|
||||
});
|
||||
|
|
@ -1576,12 +1576,12 @@ fn design(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
}
|
||||
let mut graphs = vec![];
|
||||
d.walk(|n| {
|
||||
if n.graph.as_deref().is_some_and(|g| g.series_markers.iter().flatten().any(|m| *m == old)) {
|
||||
if n.extras().graph.as_deref().is_some_and(|g| g.series_markers.iter().flatten().any(|m| *m == old)) {
|
||||
graphs.push(n.id);
|
||||
}
|
||||
});
|
||||
for id in graphs {
|
||||
if let Some(g) = d.node_mut(id).and_then(|n| n.graph.as_deref_mut()) {
|
||||
if let Some(g) = d.node_mut(id).and_then(|n| n.extras_if_any().and_then(|e| e.graph.as_deref_mut())) {
|
||||
for m in g.series_markers.iter_mut().flatten().filter(|m| **m == old) {
|
||||
*m = new.clone();
|
||||
}
|
||||
|
|
@ -1921,7 +1921,7 @@ mod tests {
|
|||
|
||||
/// The generated group stored for series `index`, not a sibling that happens to share its name.
|
||||
fn series(n: &vectorcraft_doc::Node, index: u32) -> &vectorcraft_doc::Node {
|
||||
n.children().unwrap().iter().find(|c| c.series_index == Some(index)).unwrap_or_else(|| panic!("no series {index}"))
|
||||
n.children().unwrap().iter().find(|c| c.extras().series_index == Some(index)).unwrap_or_else(|| panic!("no series {index}"))
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -1931,7 +1931,7 @@ mod tests {
|
|||
for ty in ["column", "stackedColumn", "bar", "stackedBar", "line", "area", "scatter", "pie", "radar"] {
|
||||
let id = graph(&mut s, ty);
|
||||
let n = s.doc().unwrap().doc.node(id).unwrap().clone();
|
||||
assert!(n.graph.is_some(), "{ty}");
|
||||
assert!(n.extras().graph.is_some(), "{ty}");
|
||||
if ty != "scatter" {
|
||||
assert!(!group(&n, "2024").children().unwrap().is_empty(), "{ty}");
|
||||
assert!(!group(&n, "2025").children().unwrap().is_empty(), "{ty}");
|
||||
|
|
@ -1966,7 +1966,7 @@ mod tests {
|
|||
assert!((s.doc().unwrap().doc.node(id).unwrap().geometric_bounds().unwrap().x0 - x0).abs() < 1.0);
|
||||
// Type change keeps the data; undo restores the columns.
|
||||
s.execute("graph.setType", &json!({"type": "pie"})).unwrap();
|
||||
assert_eq!(s.doc().unwrap().doc.node(id).unwrap().graph.as_ref().unwrap().kind, vectorcraft_doc::GraphKind::Pie);
|
||||
assert_eq!(s.doc().unwrap().doc.node(id).unwrap().extras().graph.as_ref().unwrap().kind, vectorcraft_doc::GraphKind::Pie);
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!(s.execute("graph.setType", &json!({})).unwrap()["type"], json!("column"));
|
||||
// Selecting a bar inside still targets the graph.
|
||||
|
|
@ -2068,7 +2068,7 @@ mod tests {
|
|||
let id = graph(&mut s, ty.id());
|
||||
let series_id = group(s.doc().unwrap().doc.node(id).unwrap(), "2024").id;
|
||||
s.execute("paint.setFill", &json!({"ids": [series_id.0], "color": "#ff0000"})).unwrap();
|
||||
let spec = s.doc().unwrap().doc.node(id).unwrap().graph.as_deref().unwrap().clone();
|
||||
let spec = s.doc().unwrap().doc.node(id).unwrap().extras().graph.as_deref().unwrap().clone();
|
||||
let restored: GraphSpec = serde_json::from_value(serde_json::to_value(&spec).unwrap()).unwrap();
|
||||
assert_eq!(spec, restored);
|
||||
let red = Paint::solid(vectorcraft_color::Color::rgb(1.0, 0.0, 0.0));
|
||||
|
|
@ -2077,7 +2077,7 @@ mod tests {
|
|||
assert!(selected_group.children().unwrap().iter().all(|n| n.appearance.fill_paint() == red), "{ty:?}");
|
||||
s.execute("graph.setData", &json!({"rows": [[2, 3], [4, 5]]})).unwrap();
|
||||
s.execute("graph.setType", &json!({"type": "line"})).unwrap();
|
||||
let spec = s.doc().unwrap().doc.node(id).unwrap().graph.as_deref().unwrap();
|
||||
let spec = s.doc().unwrap().doc.node(id).unwrap().extras().graph.as_deref().unwrap();
|
||||
assert_eq!(spec.series_paints.first().and_then(|p| p.fill.clone()), Some(red.clone()), "{ty:?}");
|
||||
let series = group(s.doc().unwrap().doc.node(id).unwrap(), "2024").children().unwrap();
|
||||
assert!(series.iter().any(|n| n.appearance.fill_paint() == red), "{ty:?}");
|
||||
|
|
@ -2092,18 +2092,28 @@ mod tests {
|
|||
let series_id = group(s.doc().unwrap().doc.node(id).unwrap(), "2024").id;
|
||||
s.execute("paint.setFill", &json!({"ids": [series_id.0], "color": "#e8573f"})).unwrap();
|
||||
assert_eq!(
|
||||
s.doc().unwrap().doc.node(id).unwrap().graph.as_ref().unwrap().series_paints[0].fill.as_ref().and_then(Paint::color).unwrap().to_hex(),
|
||||
s.doc().unwrap().doc.node(id).unwrap().extras().graph.as_ref().unwrap().series_paints[0]
|
||||
.fill
|
||||
.as_ref()
|
||||
.and_then(Paint::color)
|
||||
.unwrap()
|
||||
.to_hex(),
|
||||
"#e8573f"
|
||||
);
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert!(s.doc().unwrap().doc.node(id).unwrap().graph.as_ref().unwrap().series_paints.is_empty());
|
||||
assert!(s.doc().unwrap().doc.node(id).unwrap().extras().graph.as_ref().unwrap().series_paints.is_empty());
|
||||
assert_eq!(
|
||||
group(s.doc().unwrap().doc.node(id).unwrap(), "2024").children().unwrap()[0].appearance.fill_paint().color(),
|
||||
Some(super::default_series_color(0))
|
||||
);
|
||||
s.execute("edit.redo", &json!({})).unwrap();
|
||||
assert_eq!(
|
||||
s.doc().unwrap().doc.node(id).unwrap().graph.as_ref().unwrap().series_paints[0].fill.as_ref().and_then(Paint::color).unwrap().to_hex(),
|
||||
s.doc().unwrap().doc.node(id).unwrap().extras().graph.as_ref().unwrap().series_paints[0]
|
||||
.fill
|
||||
.as_ref()
|
||||
.and_then(Paint::color)
|
||||
.unwrap()
|
||||
.to_hex(),
|
||||
"#e8573f"
|
||||
);
|
||||
}
|
||||
|
|
@ -2123,7 +2133,7 @@ mod tests {
|
|||
s.execute("paint.setFill", &fill).unwrap();
|
||||
// The graph keeps nothing, as after a command that paints only the art.
|
||||
s.edit("Forget", |d, _| {
|
||||
let g = d.node_mut(id).unwrap().graph.as_deref_mut().unwrap();
|
||||
let g = d.node_mut(id).unwrap().extras_if_any().unwrap().graph.as_deref_mut().unwrap();
|
||||
g.series_paints.clear();
|
||||
g.label_styles = vectorcraft_doc::LabelStyles::default();
|
||||
Ok(())
|
||||
|
|
@ -2152,7 +2162,7 @@ mod tests {
|
|||
s.execute("graph.setData", &json!({"rows": [[1, 2], [3, 4]]})).unwrap();
|
||||
let node = s.doc().unwrap().doc.node(id).unwrap();
|
||||
let green = vectorcraft_color::Color::rgb(18.0 / 255.0, 171.0 / 255.0, 52.0 / 255.0);
|
||||
assert_eq!(node.graph.as_ref().unwrap().series_paints[0].fill.as_ref().and_then(Paint::color), Some(green));
|
||||
assert_eq!(node.extras().graph.as_ref().unwrap().series_paints[0].fill.as_ref().and_then(Paint::color), Some(green));
|
||||
assert!(group(node, "2024").children().unwrap().iter().all(|n| n.appearance.fill_paint().color() == Some(green)));
|
||||
}
|
||||
|
||||
|
|
@ -2165,14 +2175,14 @@ mod tests {
|
|||
let id = graph(&mut s, "column");
|
||||
let series_id = group(s.doc().unwrap().doc.node(id).unwrap(), "2024").id;
|
||||
s.edit("Junk", |d, _| {
|
||||
d.node_mut(series_id).unwrap().series_index = Some(u32::MAX);
|
||||
d.node_mut(series_id).unwrap().extras_mut().series_index = Some(u32::MAX);
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
let bar = group(s.doc().unwrap().doc.node(id).unwrap(), "2024").children().unwrap()[0].id;
|
||||
s.execute("paint.setFill", &json!({"ids": [bar.0, series_id.0], "color": "#12ab34"})).unwrap();
|
||||
let node = s.doc().unwrap().doc.node(id).unwrap();
|
||||
assert!(node.graph.as_ref().unwrap().series_paints.is_empty());
|
||||
assert!(node.extras().graph.as_ref().unwrap().series_paints.is_empty());
|
||||
assert_eq!(group(node, "2024").children().unwrap()[0].appearance.fill_paint().color().map(|c| c.to_hex()), Some("#12ab34".into()));
|
||||
}
|
||||
|
||||
|
|
@ -2185,10 +2195,10 @@ mod tests {
|
|||
s.execute("paint.setStroke", &json!({"ids": [series_id.0], "color": "#3751c8"})).unwrap();
|
||||
let blue = vectorcraft_color::Color::rgb(55.0 / 255.0, 81.0 / 255.0, 200.0 / 255.0);
|
||||
assert_eq!(
|
||||
s.doc().unwrap().doc.node(id).unwrap().graph.as_ref().unwrap().series_paints[0].stroke.as_ref().and_then(Paint::color),
|
||||
s.doc().unwrap().doc.node(id).unwrap().extras().graph.as_ref().unwrap().series_paints[0].stroke.as_ref().and_then(Paint::color),
|
||||
Some(blue)
|
||||
);
|
||||
assert!(s.doc().unwrap().doc.node(id).unwrap().graph.as_ref().unwrap().series_paints[0].fill.is_none());
|
||||
assert!(s.doc().unwrap().doc.node(id).unwrap().extras().graph.as_ref().unwrap().series_paints[0].fill.is_none());
|
||||
s.execute("graph.setData", &json!({"rows": [[1, 2], [3, 4]]})).unwrap();
|
||||
assert!(
|
||||
group(s.doc().unwrap().doc.node(id).unwrap(), "2024")
|
||||
|
|
@ -2212,7 +2222,7 @@ mod tests {
|
|||
let path = dir.join("palette.vectorcraft");
|
||||
s.execute("document.save", &json!({"path": path})).unwrap();
|
||||
s.execute("document.open", &json!({"path": path})).unwrap();
|
||||
let paints = &s.doc().unwrap().doc.node(id).unwrap().graph.as_ref().unwrap().series_paints;
|
||||
let paints = &s.doc().unwrap().doc.node(id).unwrap().extras().graph.as_ref().unwrap().series_paints;
|
||||
assert_eq!(paints.iter().map(|p| p.fill.as_ref().and_then(Paint::color).unwrap().to_hex()).collect::<Vec<_>>(), vec!["#ff0000", "#008080"]);
|
||||
s.execute("select.set", &json!({"ids": [id.0]})).unwrap();
|
||||
s.execute("graph.setData", &json!({"id": id.0, "rows": [[3, 4, 5]]})).unwrap();
|
||||
|
|
@ -2241,7 +2251,7 @@ mod tests {
|
|||
let series_id = group(s.doc().unwrap().doc.node(id).unwrap(), "2024").id;
|
||||
s.execute("paint.setStroke", &json!({"ids": [series_id.0], "color": "#0000ff"})).unwrap();
|
||||
s.execute("graph.setData", &json!({"rows": [[1, 2], [3, 4]]})).unwrap();
|
||||
let spec = s.doc().unwrap().doc.node(id).unwrap().graph.as_deref().unwrap().clone();
|
||||
let spec = s.doc().unwrap().doc.node(id).unwrap().extras().graph.as_deref().unwrap().clone();
|
||||
let blue = Paint::solid(vectorcraft_color::Color::rgb(0.0, 0.0, 1.0));
|
||||
assert_eq!(spec.series_paints[0].stroke, Some(blue.clone()));
|
||||
assert!(spec.series_paints[0].fill.is_none());
|
||||
|
|
@ -2253,7 +2263,7 @@ mod tests {
|
|||
let marker = group(s.doc().unwrap().doc.node(id).unwrap(), "2024").children().unwrap()[1].id;
|
||||
s.execute("paint.setFill", &json!({"ids": [marker.0], "color": "#ff0000"})).unwrap();
|
||||
s.execute("graph.setData", &json!({"rows": [[1, 2], [3, 4]]})).unwrap();
|
||||
let spec = s.doc().unwrap().doc.node(id).unwrap().graph.as_deref().unwrap().clone();
|
||||
let spec = s.doc().unwrap().doc.node(id).unwrap().extras().graph.as_deref().unwrap().clone();
|
||||
assert!(spec.series_paints[0].stroke.is_none());
|
||||
let series = group(s.doc().unwrap().doc.node(id).unwrap(), "2024").children().unwrap();
|
||||
assert_eq!(series[0].appearance.stroke_paint().color(), Some(super::default_series_color(0)));
|
||||
|
|
@ -2272,7 +2282,7 @@ mod tests {
|
|||
)
|
||||
.unwrap();
|
||||
s.execute("graph.setData", &json!({"rows": [[2, 3], [4, 5]]})).unwrap();
|
||||
let spec = s.doc().unwrap().doc.node(id).unwrap().graph.as_deref().unwrap().clone();
|
||||
let spec = s.doc().unwrap().doc.node(id).unwrap().extras().graph.as_deref().unwrap().clone();
|
||||
assert!(matches!(spec.series_paints[0].fill, Some(Paint::Gradient(_))));
|
||||
let bar = &group(s.doc().unwrap().doc.node(id).unwrap(), "2024").children().unwrap()[0];
|
||||
assert!(matches!(bar.appearance.fill_paint(), Paint::Gradient(_)));
|
||||
|
|
@ -2288,15 +2298,15 @@ mod tests {
|
|||
let n = s.doc().unwrap().doc.node(id).unwrap();
|
||||
let legend = series(n, 0);
|
||||
assert_eq!(legend.name.as_deref(), Some("Legend"));
|
||||
assert!(n.children().unwrap().iter().any(|c| c.name.as_deref() == Some("Legend") && c.series_index.is_none()));
|
||||
assert!(n.children().unwrap().iter().any(|c| c.name.as_deref() == Some("Axes") && c.series_index.is_none()));
|
||||
assert!(n.children().unwrap().iter().any(|c| c.name.as_deref() == Some("Legend") && c.extras().series_index.is_none()));
|
||||
assert!(n.children().unwrap().iter().any(|c| c.name.as_deref() == Some("Axes") && c.extras().series_index.is_none()));
|
||||
legend.id
|
||||
};
|
||||
s.execute("paint.setFill", &json!({"ids": [legend_id.0], "color": "#ff0000"})).unwrap();
|
||||
s.execute("graph.setData", &json!({"csv": csv})).unwrap();
|
||||
let n = s.doc().unwrap().doc.node(id).unwrap();
|
||||
assert_eq!(n.graph.as_ref().unwrap().series_paints[0].fill.as_ref().and_then(Paint::color).unwrap().to_hex(), "#ff0000");
|
||||
assert!(n.graph.as_ref().unwrap().series_paints.get(1).is_none_or(|p| p.fill.is_none()));
|
||||
assert_eq!(n.extras().graph.as_ref().unwrap().series_paints[0].fill.as_ref().and_then(Paint::color).unwrap().to_hex(), "#ff0000");
|
||||
assert!(n.extras().graph.as_ref().unwrap().series_paints.get(1).is_none_or(|p| p.fill.is_none()));
|
||||
assert_eq!(series(n, 0).children().unwrap()[0].appearance.fill_paint().color().unwrap().to_hex(), "#ff0000");
|
||||
assert_eq!(series(n, 1).name.as_deref(), Some("Axes"));
|
||||
assert_ne!(series(n, 1).children().unwrap()[0].appearance.fill_paint().color().unwrap().to_hex(), "#ff0000");
|
||||
|
|
@ -2311,8 +2321,8 @@ mod tests {
|
|||
s.execute("paint.setFill", &json!({"ids": [keep.0], "color": "#00ff00"})).unwrap();
|
||||
s.execute("graph.setData", &json!({"id": id.0, "rows": [[8, 0], [1, 0]]})).unwrap();
|
||||
let n = s.doc().unwrap().doc.node(id).unwrap();
|
||||
assert_eq!(group(n, "Keep").series_index, Some(0));
|
||||
assert_eq!(n.graph.as_ref().unwrap().series_paints[0].fill.as_ref().and_then(Paint::color).unwrap().to_hex(), "#00ff00");
|
||||
assert_eq!(group(n, "Keep").extras().series_index, Some(0));
|
||||
assert_eq!(n.extras().graph.as_ref().unwrap().series_paints[0].fill.as_ref().and_then(Paint::color).unwrap().to_hex(), "#00ff00");
|
||||
assert!(n.children().unwrap().iter().all(|c| c.name.as_deref() != Some("Drop")));
|
||||
}
|
||||
|
||||
|
|
@ -2395,7 +2405,7 @@ mod tests {
|
|||
let r =
|
||||
s.execute("graph.create", &json!({"type": "column", "x": 0, "y": 0, "width": 300, "height": 200, "csv": ",a\nQ1,1\nQ2\nQ3,3"})).unwrap();
|
||||
let id = NodeId(r["id"].as_u64().unwrap());
|
||||
let spec = s.doc().unwrap().doc.node(id).unwrap().graph.clone().unwrap();
|
||||
let spec = s.doc().unwrap().doc.node(id).unwrap().extras().graph.clone().unwrap();
|
||||
assert_eq!(spec.categories, vec!["Q1", "Q2", "Q3"]);
|
||||
assert_eq!(spec.cells(), vec![vec![Some(1.0)], vec![None], vec![Some(3.0)]]);
|
||||
}
|
||||
|
|
@ -2423,7 +2433,7 @@ mod tests {
|
|||
// A data window round trip keeps the blanks.
|
||||
let csv = s.execute("graph.setData", &json!({})).unwrap()["csv"].as_str().unwrap().to_string();
|
||||
assert_eq!(csv.lines().nth(2), Some(",2"), "{csv}");
|
||||
let spec = s.doc().unwrap().doc.node(id).unwrap().graph.clone().unwrap();
|
||||
let spec = s.doc().unwrap().doc.node(id).unwrap().extras().graph.clone().unwrap();
|
||||
assert_eq!(spec.cells()[1], vec![None, Some(2.0)]);
|
||||
// In the file a blank is a 0 placeholder plus its place in `blanks`, so a version without blanks reads
|
||||
// numbers (as zeros); a graph without blanks writes no `blanks` at all.
|
||||
|
|
@ -2434,7 +2444,7 @@ mod tests {
|
|||
}
|
||||
|
||||
fn spec(s: &Session, id: NodeId) -> GraphSpec {
|
||||
s.doc().unwrap().doc.node(id).unwrap().graph.as_deref().unwrap().clone()
|
||||
s.doc().unwrap().doc.node(id).unwrap().extras().graph.as_deref().unwrap().clone()
|
||||
}
|
||||
|
||||
/// Bounds of a series' marks, without its legend swatch (the last child).
|
||||
|
|
@ -2462,12 +2472,12 @@ mod tests {
|
|||
assert!((line[1].center().x - cols[0].center().x).abs() < 1e-6);
|
||||
// The line series is drawn after (in front of) the column series.
|
||||
let n = s.doc().unwrap().doc.node(id).unwrap().clone();
|
||||
let order: Vec<u32> = n.children().unwrap().iter().filter_map(|c| c.series_index).collect();
|
||||
let order: Vec<u32> = n.children().unwrap().iter().filter_map(|c| c.extras().series_index).collect();
|
||||
assert_eq!(order, [0, 1]);
|
||||
s.execute("graph.setType", &json!({"seriesIndexes": [0], "type": "line"})).unwrap();
|
||||
s.execute("graph.setType", &json!({"seriesIndexes": [1], "type": "column"})).unwrap();
|
||||
let n = s.doc().unwrap().doc.node(id).unwrap().clone();
|
||||
let order: Vec<u32> = n.children().unwrap().iter().filter_map(|c| c.series_index).collect();
|
||||
let order: Vec<u32> = n.children().unwrap().iter().filter_map(|c| c.extras().series_index).collect();
|
||||
assert_eq!(order, [1, 0]);
|
||||
// Back to the graph's own type: nothing is stored for that series.
|
||||
assert_eq!(spec(&s, id).series_kinds, [Some(GraphKind::Line)]);
|
||||
|
|
@ -2562,7 +2572,7 @@ mod tests {
|
|||
assert!((area.x0 - cols[0].center().x).abs() < 1e-6 && (area.x1 - cols[2].center().x).abs() < 1e-6, "{area:?}");
|
||||
// The area is drawn behind the columns.
|
||||
let n = s.doc().unwrap().doc.node(id).unwrap().clone();
|
||||
let order: Vec<u32> = n.children().unwrap().iter().filter_map(|c| c.series_index).collect();
|
||||
let order: Vec<u32> = n.children().unwrap().iter().filter_map(|c| c.extras().series_index).collect();
|
||||
assert_eq!(order, [0, 1]);
|
||||
// An area graph alone still spans the plot.
|
||||
s.execute("graph.setType", &json!({"seriesIndexes": [1], "type": "area"})).unwrap();
|
||||
|
|
@ -3173,7 +3183,7 @@ mod tests {
|
|||
let dot = vectorcraft_doc::Node::group(d.alloc_id(), vec![]);
|
||||
d.graph_designs.push(vectorcraft_doc::GraphDesign { name: "Dot".into(), art: std::sync::Arc::new(dot) });
|
||||
let art = super::generate(&mut d, &g);
|
||||
let s0 = art.iter().find(|n| n.series_index == Some(0)).unwrap();
|
||||
let s0 = art.iter().find(|n| n.extras().series_index == Some(0)).unwrap();
|
||||
assert!(s0.children().unwrap().iter().all(|n| matches!(n.kind, NodeKind::Path { .. })));
|
||||
}
|
||||
|
||||
|
|
@ -3188,13 +3198,13 @@ mod tests {
|
|||
s.execute("graph.design", &json!({"new": "Chart"})).unwrap();
|
||||
let art = s.doc().unwrap().doc.graph_designs[1].art.clone();
|
||||
let mut graphs = 0;
|
||||
art.walk(&mut |n| graphs += usize::from(n.graph.is_some() || n.series_index.is_some()));
|
||||
art.walk(&mut |n| graphs += usize::from(n.extras().graph.is_some() || n.extras().series_index.is_some()));
|
||||
assert_eq!(graphs, 0, "no graph inside a design");
|
||||
// The graph drawn with itself as a marker, then the design it used deleted: still one graph to edit.
|
||||
s.execute("graph.marker", &json!({"id": id.0, "seriesIndexes": [1], "design": "Chart"})).unwrap();
|
||||
s.execute("graph.design", &json!({"delete": "Pill"})).unwrap();
|
||||
let mut found = 0;
|
||||
s.doc().unwrap().doc.walk(|n| found += usize::from(n.graph.is_some()));
|
||||
s.doc().unwrap().doc.walk(|n| found += usize::from(n.extras().graph.is_some()));
|
||||
assert_eq!(found, 1);
|
||||
// A deleted design reads as none in the query, so the dialog opens on the default marker.
|
||||
s.execute("select.set", &json!({"ids": [id.0]})).unwrap();
|
||||
|
|
@ -3226,7 +3236,7 @@ mod tests {
|
|||
g.series_markers = vec![Some("Big".into())];
|
||||
let mut d = (*s.doc().unwrap().doc).clone();
|
||||
let art = super::generate(&mut d, &g);
|
||||
let s0 = art.iter().find(|n| n.series_index == Some(0)).unwrap();
|
||||
let s0 = art.iter().find(|n| n.extras().series_index == Some(0)).unwrap();
|
||||
assert!(s0.children().unwrap().iter().all(|n| matches!(n.kind, NodeKind::Path { .. })));
|
||||
// Too many objects for one design.
|
||||
let ids: Vec<u64> = (0..2001)
|
||||
|
|
@ -3241,7 +3251,7 @@ mod tests {
|
|||
let mut d = vectorcraft_doc::Document::new(100.0, 100.0);
|
||||
let art = |d: &mut vectorcraft_doc::Document| std::sync::Arc::new(vectorcraft_doc::Node::group(d.alloc_id(), vec![]));
|
||||
let mut graphy = vectorcraft_doc::Node::group(d.alloc_id(), vec![]);
|
||||
graphy.graph = Some(Box::new(GraphSpec::default()));
|
||||
graphy.extras_mut().graph = Some(Box::new(GraphSpec::default()));
|
||||
for name in [" A\u{7} ", "A", "", " ", &"x".repeat(100)] {
|
||||
let a = art(&mut d);
|
||||
d.graph_designs.push(vectorcraft_doc::GraphDesign { name: name.into(), art: a });
|
||||
|
|
@ -3250,7 +3260,7 @@ mod tests {
|
|||
d.tidy_graph_designs();
|
||||
let names: Vec<_> = d.graph_designs.iter().map(|x| x.name.clone()).collect();
|
||||
assert_eq!(names, ["A".to_string(), "x".repeat(64), "G".to_string()]);
|
||||
assert!(d.graph_designs[2].art.graph.is_none());
|
||||
assert!(d.graph_designs[2].art.extras().graph.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -3511,7 +3521,7 @@ mod tests {
|
|||
assert!(min > 1.0, "the ring keeps clear of the centre: {near:?}");
|
||||
// The names are drawn over the pies: after every series group.
|
||||
let kids = n.children().unwrap();
|
||||
let last_series = kids.iter().rposition(|c| c.series_index.is_some()).unwrap();
|
||||
let last_series = kids.iter().rposition(|c| c.extras().series_index.is_some()).unwrap();
|
||||
let names: Vec<usize> = kids.iter().enumerate().filter(|(_, c)| matches!(c.kind, NodeKind::Text(_))).map(|(i, _)| i).collect();
|
||||
assert_eq!(names.len(), 2, "of two equal pies, one has no ring and no name");
|
||||
assert!(names.iter().all(|i| *i > last_series), "{names:?} after {last_series}");
|
||||
|
|
@ -3576,7 +3586,7 @@ mod tests {
|
|||
let n = s.doc().unwrap().doc.node(id).unwrap().clone();
|
||||
let kids = n.children().unwrap();
|
||||
let at = kids.iter().position(|c| c.name.as_deref() == Some("Drop Shadow")).unwrap();
|
||||
assert!(kids.iter().position(|c| c.series_index.is_some()).unwrap() > at, "behind the series");
|
||||
assert!(kids.iter().position(|c| c.extras().series_index.is_some()).unwrap() > at, "behind the series");
|
||||
let shadow = &kids[at];
|
||||
assert!((shadow.opacity - 0.35).abs() < 1e-6);
|
||||
let copies: Vec<_> = shadow.children().unwrap().iter().filter_map(|c| c.geometric_bounds()).collect();
|
||||
|
|
@ -3639,8 +3649,12 @@ mod tests {
|
|||
s.execute("graph.setType", &json!({"seriesIndexes": [0], "type": "area"})).unwrap();
|
||||
s.execute("graph.setType", &json!({"seriesIndexes": [2], "type": "line", "dropShadow": true})).unwrap();
|
||||
let n = s.doc().unwrap().doc.node(id).unwrap().clone();
|
||||
let order: Vec<String> =
|
||||
n.children().unwrap().iter().map(|c| c.series_index.map_or_else(|| c.name.clone().unwrap_or_default(), |i| format!("s{i}"))).collect();
|
||||
let order: Vec<String> = n
|
||||
.children()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(|c| c.extras().series_index.map_or_else(|| c.name.clone().unwrap_or_default(), |i| format!("s{i}")))
|
||||
.collect();
|
||||
// Areas cast none; the columns' shadows sit between the area and the columns, the line's between the columns
|
||||
// and the line.
|
||||
let at = |k: &str| order.iter().position(|o| o == k).unwrap();
|
||||
|
|
@ -3702,8 +3716,9 @@ mod tests {
|
|||
assert!(a[0].x1 > b[0].x0, "the columns of a cluster overlap: {:?} {:?}", a[0], b[0]);
|
||||
assert!(b[0].x1 > a[1].x0, "and so do the clusters: {:?} {:?}", b[0], a[1]);
|
||||
assert_eq!(s.execute("graph.setType", &json!({"clusterWidth": 5000})).map(|_| spec(&s, id).cluster_width).unwrap(), 1000.0);
|
||||
let order =
|
||||
|s: &Session| -> Vec<u32> { s.doc().unwrap().doc.node(id).unwrap().children().unwrap().iter().filter_map(|c| c.series_index).collect() };
|
||||
let order = |s: &Session| -> Vec<u32> {
|
||||
s.doc().unwrap().doc.node(id).unwrap().children().unwrap().iter().filter_map(|c| c.extras().series_index).collect()
|
||||
};
|
||||
assert_eq!(order(&s), [0, 1]);
|
||||
s.execute("graph.setType", &json!({"firstColumnInFront": true})).unwrap();
|
||||
assert_eq!(order(&s), [1, 0], "the first series drawn last, on top");
|
||||
|
|
|
|||
|
|
@ -535,7 +535,7 @@ fn blend_make(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
extend_spine(&mut spec, old, old_spec, &children);
|
||||
}
|
||||
n.id = id;
|
||||
n.kind = NodeKind::Blend { children, spec };
|
||||
n.kind = NodeKind::Blend { children, spec: Box::new(spec) };
|
||||
n
|
||||
})
|
||||
})?;
|
||||
|
|
@ -563,7 +563,7 @@ fn blend_release(s: &mut Session, _: &Value) -> Result<Value> {
|
|||
|| n.opacity < 1.0
|
||||
|| n.blend != vectorcraft_color::BlendMode::Normal
|
||||
|| n.isolate
|
||||
|| n.mask.is_some()
|
||||
|| n.extras().mask.is_some()
|
||||
|| !n.appearance.items.is_empty()
|
||||
|| !n.appearance.effects.is_empty()
|
||||
{
|
||||
|
|
@ -714,7 +714,7 @@ fn edit_blends(s: &mut Session, label: &str, f: impl Fn(&mut Vec<Arc<Node>>, &mu
|
|||
fn selected_spec(s: &Session) -> Option<(NodeId, BlendSpec, Vec<NodeId>)> {
|
||||
let b = *selected_of(s, is_blend).first()?;
|
||||
match &s.doc().ok()?.doc.node(b)?.kind {
|
||||
NodeKind::Blend { spec, children } => Some((b, spec.clone(), children.iter().map(|c| c.id).collect())),
|
||||
NodeKind::Blend { spec, children } => Some((b, (**spec).clone(), children.iter().map(|c| c.id).collect())),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
|
@ -952,7 +952,7 @@ fn envelope_defaults(s: &Session) -> (EnvelopeOptions, f64) {
|
|||
}
|
||||
|
||||
fn envelope(id: NodeId, content: Vec<Arc<Node>>, kind: EnvelopeKind, (options, fidelity): (EnvelopeOptions, f64)) -> Node {
|
||||
Node::new(id, NodeKind::Envelope { content, kind, fidelity, editing: false, options, frame: Affine::IDENTITY })
|
||||
Node::new(id, NodeKind::Envelope { content, kind: Box::new(kind), fidelity, editing: false, options, frame: Box::new(Affine::IDENTITY) })
|
||||
}
|
||||
|
||||
/// `rows` and `cols` (or `columns`), 1..50 each, defaulting to `default`.
|
||||
|
|
@ -1048,7 +1048,7 @@ fn set_kinds(s: &mut Session, label: &str, kinds: Vec<(NodeId, EnvelopeKind)>) -
|
|||
s.edit(label, |d, _| {
|
||||
for (e, k) in kinds {
|
||||
if let Some(NodeKind::Envelope { kind, .. }) = d.node_mut(e).map(|n| &mut n.kind) {
|
||||
*kind = k;
|
||||
**kind = k;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
|
|
@ -1110,7 +1110,7 @@ fn env_info(s: &mut Session, _: &Value) -> Result<Value> {
|
|||
let (options, fidelity) = envelope_defaults(s);
|
||||
return Ok(envelope_options_json(json!({ "id": null }), &options, fidelity));
|
||||
};
|
||||
let v = match kind {
|
||||
let v = match &**kind {
|
||||
EnvelopeKind::Warp { style, bend, h, v, horizontal } => {
|
||||
json!({"type": "warp", "style": style, "bend": bend, "h": h, "v": v, "horizontal": horizontal})
|
||||
}
|
||||
|
|
@ -1154,7 +1154,7 @@ fn carry_transparency(d: &mut Document, env: &Node, mut nodes: Vec<Node>) -> Vec
|
|||
n.isolate = env.isolate;
|
||||
n.knockout = env.knockout;
|
||||
n.knockout_shape = env.knockout_shape;
|
||||
n.mask = env.mask.clone();
|
||||
n.extras_mut().mask = env.extras().mask.clone();
|
||||
};
|
||||
if let [one] = nodes.as_mut_slice()
|
||||
&& one.has_default_transparency()
|
||||
|
|
@ -1205,8 +1205,8 @@ fn env_options(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
let mut updates = vec![];
|
||||
for e in &envs {
|
||||
if let Some(NodeKind::Envelope { kind, options, fidelity, .. }) = doc.node(*e).map(|n| &n.kind) {
|
||||
let kind = match kind {
|
||||
EnvelopeKind::Warp { .. } => warp_kind(p, C, Some(kind))?,
|
||||
let kind = match &**kind {
|
||||
k @ EnvelopeKind::Warp { .. } => warp_kind(p, C, Some(k))?,
|
||||
k => k.clone(),
|
||||
};
|
||||
updates.push((*e, kind, options_param(p, C, *options)?, fid.unwrap_or(*fidelity)));
|
||||
|
|
@ -1215,7 +1215,7 @@ fn env_options(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
s.edit("Envelope Options", |d, _| {
|
||||
for (e, k, o, f) in updates {
|
||||
if let Some(NodeKind::Envelope { kind, options, fidelity, .. }) = d.node_mut(e).map(|n| &mut n.kind) {
|
||||
(*kind, *options, *fidelity) = (k, o, f);
|
||||
(**kind, *options, *fidelity) = (k, o, f);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
|
|
@ -1300,7 +1300,10 @@ fn env_set_mesh_point(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
let q = Point::new(f64_req(p, "x", C)?, f64_req(p, "y", C)?);
|
||||
let handle = handle_param(p, C)?;
|
||||
s.edit("Move Envelope Point", |d, _| {
|
||||
let Some(NodeKind::Envelope { kind: EnvelopeKind::Mesh { rows, cols, points, handles }, .. }) = d.node_mut(id).map(|n| &mut n.kind) else {
|
||||
let Some(NodeKind::Envelope { kind, .. }) = d.node_mut(id).map(|n| &mut n.kind) else {
|
||||
return Err(bad(C, "not a mesh envelope point"));
|
||||
};
|
||||
let EnvelopeKind::Mesh { rows, cols, points, handles } = &mut **kind else {
|
||||
return Err(bad(C, "not a mesh envelope point"));
|
||||
};
|
||||
let at = *points.get(index).ok_or_else(|| bad(C, "not a mesh envelope point"))?;
|
||||
|
|
@ -1384,7 +1387,10 @@ fn with_mesh<T>(s: &mut Session, p: &Value, cmd: &str, label: &str, envelopes: b
|
|||
let c = cmd.to_string();
|
||||
s.edit(label, move |d, _| match d.node_mut(id).map(|n| &mut n.kind) {
|
||||
Some(NodeKind::Mesh(m)) => f(m),
|
||||
Some(NodeKind::Envelope { kind: EnvelopeKind::Mesh { rows, cols, points, handles }, .. }) if envelopes => {
|
||||
Some(NodeKind::Envelope { kind, .. }) if envelopes && matches!(**kind, EnvelopeKind::Mesh { .. }) => {
|
||||
let EnvelopeKind::Mesh { rows, cols, points, handles } = &mut **kind else {
|
||||
return Err(bad(&c, "not a gradient mesh or mesh envelope"));
|
||||
};
|
||||
let mut m = live::envelope_grid(*rows, *cols, points, handles).ok_or_else(|| bad(&c, "the envelope's mesh is malformed"))?;
|
||||
let r = f(&mut m)?;
|
||||
(*rows, *cols) = (m.rows, m.cols);
|
||||
|
|
|
|||
|
|
@ -90,7 +90,7 @@ fn enter(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
|
||||
/// The first selected object with an opacity mask.
|
||||
fn first_masked(st: &crate::DocState) -> Option<NodeId> {
|
||||
st.selection.subjects().iter().copied().find(|i| st.doc.node(*i).is_some_and(|n| n.mask.is_some()))
|
||||
st.selection.subjects().iter().copied().find(|i| st.doc.node(*i).is_some_and(|n| n.extras().mask.is_some()))
|
||||
}
|
||||
|
||||
/// Enter mask editing for `id` (one undo step) and isolate the editing layer. Returns the layer.
|
||||
|
|
@ -127,8 +127,11 @@ fn view_mask(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
/// Enter mask editing for `id` inside an edit: copy its mask art onto a new editing layer and
|
||||
/// select it. Returns the layer.
|
||||
pub(crate) fn begin(d: &mut Document, sel: &mut Selection, id: NodeId) -> Result<NodeId> {
|
||||
let art =
|
||||
d.node(id).and_then(|n| n.mask.as_ref()).map(|m| m.art.clone()).ok_or_else(|| EngineError::Other("the object has no opacity mask".into()))?;
|
||||
let art = d
|
||||
.node(id)
|
||||
.and_then(|n| n.extras().mask.as_ref())
|
||||
.map(|m| m.art.clone())
|
||||
.ok_or_else(|| EngineError::Other("the object has no opacity mask".into()))?;
|
||||
let layer = d.add_layer(Some(MASK_EDIT_LAYER));
|
||||
let parts = mask_parts(&art);
|
||||
let mut ids = Vec::with_capacity(parts.len());
|
||||
|
|
@ -202,7 +205,7 @@ pub(crate) fn follow_history(st: &mut crate::DocState, was: Option<NodeId>) {
|
|||
pub(crate) fn sync(d: &mut Document) {
|
||||
let Some(me) = d.mask_edit else { return };
|
||||
let art: Option<Vec<Arc<Node>>> = d.node(me.layer).and_then(|l| l.children().cloned());
|
||||
let (Some(art), true) = (art, d.node(me.object).is_some_and(|n| n.mask.is_some())) else {
|
||||
let (Some(art), true) = (art, d.node(me.object).is_some_and(|n| n.extras().mask.is_some())) else {
|
||||
d.mask_edit = None;
|
||||
let _ = d.remove(me.layer);
|
||||
return;
|
||||
|
|
@ -212,12 +215,12 @@ pub(crate) fn sync(d: &mut Document) {
|
|||
// Several objects (or none): a group, so the mask keeps one art node.
|
||||
many => Arc::new(mask_group(many.to_vec())),
|
||||
};
|
||||
if let Some(m) = d.node(me.object).and_then(|n| n.mask.as_ref())
|
||||
if let Some(m) = d.node(me.object).and_then(|n| n.extras().mask.as_ref())
|
||||
&& Arc::ptr_eq(&m.art, &new_art)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if let Some(m) = d.node_mut(me.object).and_then(|n| n.mask.as_mut()) {
|
||||
if let Some(m) = d.node_mut(me.object).and_then(|n| n.extras_if_any().and_then(|e| e.mask.as_mut())) {
|
||||
m.art = new_art;
|
||||
}
|
||||
}
|
||||
|
|
@ -241,7 +244,8 @@ mod tests {
|
|||
assert_eq!(s.doc().unwrap().isolation, Some(NodeId(layer)));
|
||||
// Edit the mask art with ordinary commands: widen it; the object's mask follows live.
|
||||
s.execute("object.move", &json!({"dx": 50, "dy": 0})).unwrap();
|
||||
let mask_bounds = |s: &Session| s.doc().unwrap().doc.node(NodeId(obj)).unwrap().mask.as_ref().unwrap().art.geometric_bounds().unwrap();
|
||||
let mask_bounds =
|
||||
|s: &Session| s.doc().unwrap().doc.node(NodeId(obj)).unwrap().extras().mask.as_ref().unwrap().art.geometric_bounds().unwrap();
|
||||
assert!((mask_bounds(&s).x0 - 50.0).abs() < 1e-6);
|
||||
// Drawing while editing adds to the mask.
|
||||
s.execute("shape.ellipse", &json!({"x": 180, "y": 150, "width": 40, "height": 40})).unwrap();
|
||||
|
|
|
|||
|
|
@ -449,7 +449,8 @@ fn rasterize(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
d.images.insert(key.clone(), vectorcraft_doc::ImageBlob::new("image/png", png));
|
||||
let id = d.alloc_id();
|
||||
let xf = Affine::translate(region.origin().to_vec2()) * Affine::scale(1.0 / scale);
|
||||
let mut node = Node::new(id, NodeKind::Image(ImageObject { key, width: w, height: h, xf, link: None, placement: Default::default() }));
|
||||
let mut node =
|
||||
Node::new(id, NodeKind::Image(Box::new(ImageObject { key, width: w, height: h, xf, link: None, placement: Default::default() })));
|
||||
if let Some((path, rule)) = clip {
|
||||
node = super::rasterfx::clip_group(d, path, rule, node);
|
||||
}
|
||||
|
|
@ -562,7 +563,7 @@ fn crop_image(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
let node = st.doc.node(id).cloned().ok_or(EngineError::NoNode(id))?;
|
||||
let NodeKind::Image(im) = &node.kind else { return Err(bad(C, "select an image")) };
|
||||
if p.get("trim").and_then(Value::as_bool) == Some(true) {
|
||||
return trim_image(s, id, im.clone());
|
||||
return trim_image(s, id, (**im).clone());
|
||||
}
|
||||
let ib = node.geometric_bounds().ok_or_else(|| bad(C, "image has no bounds"))?;
|
||||
let rect = match p.get("rect").and_then(Value::as_array) {
|
||||
|
|
@ -603,14 +604,14 @@ fn crop_image(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
let key = unique_key(d, "crop");
|
||||
d.images.insert(key.clone(), vectorcraft_doc::ImageBlob::new("image/png", png));
|
||||
if let Some(n) = d.node_mut(id) {
|
||||
n.kind = NodeKind::Image(ImageObject {
|
||||
n.kind = NodeKind::Image(Box::new(ImageObject {
|
||||
key,
|
||||
width: w,
|
||||
height: h,
|
||||
xf: Affine::translate(region.origin().to_vec2()) * Affine::scale(1.0 / scale),
|
||||
link: None,
|
||||
placement: Default::default(),
|
||||
});
|
||||
}));
|
||||
}
|
||||
Ok(())
|
||||
})?;
|
||||
|
|
@ -646,14 +647,14 @@ fn trim_image(s: &mut Session, id: NodeId, im: ImageObject) -> Result<Value> {
|
|||
let key = unique_key(d, "crop");
|
||||
d.images.insert(key.clone(), vectorcraft_doc::ImageBlob::new("image/png", png));
|
||||
if let Some(n) = d.node_mut(id) {
|
||||
n.kind = NodeKind::Image(ImageObject {
|
||||
n.kind = NodeKind::Image(Box::new(ImageObject {
|
||||
key,
|
||||
width: cw,
|
||||
height: ch,
|
||||
xf: im.xf * Affine::translate((f64::from(x0), f64::from(y0))),
|
||||
link: None,
|
||||
placement: Default::default(),
|
||||
});
|
||||
}));
|
||||
}
|
||||
Ok(())
|
||||
})?;
|
||||
|
|
@ -799,7 +800,7 @@ fn convert_to_shape(s: &mut Session, _: &Value) -> Result<Value> {
|
|||
}
|
||||
if let Some(l) = detect_shape(path) {
|
||||
*path = l.to_path();
|
||||
*live = Some(l);
|
||||
*live = Some(Box::new(l));
|
||||
n += 1;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -105,7 +105,7 @@ impl NewArt {
|
|||
return;
|
||||
}
|
||||
(n.opacity, n.blend) = (self.opacity, self.blend);
|
||||
n.graphic_style = self.style.as_deref().and_then(|name| d.graphic_style_index(name)).map(|i| d.graphic_style_id(i));
|
||||
n.extras_mut().graphic_style = self.style.as_deref().and_then(|name| d.graphic_style_index(name)).map(|i| d.graphic_style_id(i));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -307,7 +307,7 @@ pub(crate) fn apply_transform(s: &mut Session, label: &str, ids: Vec<NodeId>, xf
|
|||
/// reflows at its size. False for anything else (and type in perspective), which transforms as
|
||||
/// usual.
|
||||
fn resize_type_area(n: &mut Node, xf: Affine) -> bool {
|
||||
if n.perspective.is_some() {
|
||||
if n.extras().perspective.is_some() {
|
||||
return false;
|
||||
}
|
||||
let NodeKind::Text(t) = &mut n.kind else { return false };
|
||||
|
|
@ -860,7 +860,7 @@ fn isolate(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
if st.doc.node(id).is_none_or(|n| !n.is_container()) {
|
||||
return Err(bad("object.isolate", "only groups and layers can be isolated"));
|
||||
}
|
||||
let isolation = st.doc.node(id).filter(|n| n.shaper.is_some()).and_then(|n| n.children()).and_then(|c| c.first()).map_or(id, |n| n.id);
|
||||
let isolation = st.doc.node(id).filter(|n| n.extras().shaper.is_some()).and_then(|n| n.children()).and_then(|c| c.first()).map_or(id, |n| n.id);
|
||||
st.isolation = Some(isolation);
|
||||
st.selection.clear();
|
||||
st.revision += 1;
|
||||
|
|
@ -871,7 +871,7 @@ fn exit_isolation(s: &mut Session, _: &Value) -> Result<Value> {
|
|||
let st = s.doc_mut()?;
|
||||
if let Some(i) = st.isolation.take() {
|
||||
super::distortcmds::finish_edit_text(st, i);
|
||||
let target = st.doc.parent_of(i).filter(|p| st.doc.node(*p).is_some_and(|n| n.shaper.is_some())).unwrap_or(i);
|
||||
let target = st.doc.parent_of(i).filter(|p| st.doc.node(*p).is_some_and(|n| n.extras().shaper.is_some())).unwrap_or(i);
|
||||
st.selection.set([target]);
|
||||
}
|
||||
st.revision += 1;
|
||||
|
|
@ -936,10 +936,9 @@ fn set_props(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
&& flag("isolate").is_none_or(|v| n.isolate == v)
|
||||
&& knockout.is_none_or(|k| n.knockout == k)
|
||||
&& flag("knockoutShape").is_none_or(|v| n.knockout_shape == v)
|
||||
&& data
|
||||
.iter()
|
||||
.flatten()
|
||||
.all(|(k, v)| n.attrs.as_ref().and_then(|a| a.data.iter().find(|(key, _)| key == k)).map(|(_, now)| now.as_str()) == v.as_deref())
|
||||
&& data.iter().flatten().all(|(k, v)| {
|
||||
n.extras().attrs.as_ref().and_then(|a| a.data.iter().find(|(key, _)| key == k)).map(|(_, now)| now.as_str()) == v.as_deref()
|
||||
})
|
||||
};
|
||||
let d = &s.doc()?.doc;
|
||||
if ids.iter().all(|id| d.node(*id).is_some_and(has_values)) {
|
||||
|
|
@ -1410,7 +1409,7 @@ fn set_live_shape(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
let st = s.doc()?;
|
||||
let has_values = |id: &NodeId| {
|
||||
let Some(NodeKind::Path { path, live, .. }) = st.doc.node(*id).map(|n| &n.kind) else { return true };
|
||||
match live {
|
||||
match live.as_deref() {
|
||||
Some(l @ LiveShape::Polygon { sides: now, .. })
|
||||
if !(sides.is_none_or(|n| n.clamp(3, 1000) == u64::from(*now))
|
||||
&& (!equal || l.polygon_sides_equal())
|
||||
|
|
@ -1425,11 +1424,11 @@ fn set_live_shape(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
if radius.is_none() && kind.is_none() {
|
||||
return true;
|
||||
}
|
||||
let Some(c) = LiveCorners::new(path, live.as_ref()) else { return true };
|
||||
let Some(c) = LiveCorners::new(path, live.as_deref()) else { return true };
|
||||
let picked = corners_of(*id).unwrap_or_else(|| c.picked(st.selection.anchors.get(id)));
|
||||
// Setting corners folds a scale left in a rectangle's transform into its size, which
|
||||
// changes it.
|
||||
live.as_ref().is_none_or(|l| l.folded() == *l)
|
||||
live.as_deref().is_none_or(|l| l.folded() == *l)
|
||||
&& picked
|
||||
.iter()
|
||||
.all(|k| *k < c.base.anchor_count() && radius.is_none_or(|r| c.radius(*k) == r.max(0.0)) && kind.is_none_or(|kd| c.kind(*k) == kd))
|
||||
|
|
@ -1440,7 +1439,7 @@ fn set_live_shape(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
s.edit("Live Shape", |d, sel| {
|
||||
for id in &ids {
|
||||
let Some(NodeKind::Path { path, live, .. }) = d.node_mut(*id).map(|n| &mut n.kind) else { continue };
|
||||
if let Some(l @ LiveShape::Polygon { .. }) = live.as_mut()
|
||||
if let Some(l @ LiveShape::Polygon { .. }) = live.as_deref_mut()
|
||||
&& (sides.is_some() || polygon_angle.is_some() || polygon_radius.is_some() || side.is_some() || equal)
|
||||
{
|
||||
if let Some(n) = sides {
|
||||
|
|
@ -1458,7 +1457,7 @@ fn set_live_shape(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
}
|
||||
*path = l.to_path();
|
||||
}
|
||||
if let Some(l @ LiveShape::Ellipse { .. }) = live.as_mut()
|
||||
if let Some(l @ LiveShape::Ellipse { .. }) = live.as_deref_mut()
|
||||
&& (pie_start.is_some() || pie_end.is_some() || invert)
|
||||
{
|
||||
if let LiveShape::Ellipse { pie, .. } = l {
|
||||
|
|
@ -1471,7 +1470,7 @@ fn set_live_shape(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
}
|
||||
let partial = sel.anchors.get_mut(id);
|
||||
// The corners to edit, the Direct-Selected ones and the anchor layout before the edit.
|
||||
let Some(c) = LiveCorners::new(path, live.as_ref()) else { continue };
|
||||
let Some(c) = LiveCorners::new(path, live.as_deref()) else { continue };
|
||||
let corners = corners_of(*id);
|
||||
let picked = match &corners {
|
||||
Some(k) if k.last().is_some_and(|k| *k >= c.base.anchor_count()) => {
|
||||
|
|
@ -1486,7 +1485,7 @@ fn set_live_shape(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
set_corners(path, live, &picked, radius, kind);
|
||||
// Direct-Selected corners stay selected as they gain or lose anchors.
|
||||
if let (Some(anchors), Some(selected)) = (partial, selected)
|
||||
&& let Some(c) = LiveCorners::new(path, live.as_ref())
|
||||
&& let Some(c) = LiveCorners::new(path, live.as_deref())
|
||||
&& c.sources() != layout
|
||||
{
|
||||
*anchors = c.anchors_of(&selected);
|
||||
|
|
|
|||
|
|
@ -154,7 +154,7 @@ pub(crate) fn percent(o: f64) -> f32 {
|
|||
/// The [`transparency_targets`] that have an opacity mask.
|
||||
fn masked_targets(s: &Session, p: &Value) -> Result<Vec<NodeId>> {
|
||||
let d = &s.doc()?.doc;
|
||||
let ids: Vec<NodeId> = transparency_targets(s, p)?.into_iter().filter(|id| d.node(*id).is_some_and(|n| n.mask.is_some())).collect();
|
||||
let ids: Vec<NodeId> = transparency_targets(s, p)?.into_iter().filter(|id| d.node(*id).is_some_and(|n| n.extras().mask.is_some())).collect();
|
||||
if ids.is_empty() {
|
||||
return Err(EngineError::Other("no target object has an opacity mask: select one or give `id`".into()));
|
||||
}
|
||||
|
|
@ -200,12 +200,12 @@ fn make(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
gid
|
||||
};
|
||||
let n = d.node_mut(target).ok_or(EngineError::NoNode(target))?;
|
||||
if n.mask.is_some() {
|
||||
if n.extras().mask.is_some() {
|
||||
return Err(EngineError::Other("the object already has an opacity mask".into()));
|
||||
}
|
||||
let mut m = OpacityMask::new(mask_art, clip);
|
||||
m.invert = invert;
|
||||
n.mask = Some(Box::new(m));
|
||||
n.extras_mut().mask = Some(Box::new(m));
|
||||
sel.set_target(d, target);
|
||||
let layer = if mask_id.is_none() { Some(begin(d, sel, target)?) } else { None };
|
||||
Ok((target, layer))
|
||||
|
|
@ -224,7 +224,7 @@ fn release(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
finish(d, sel);
|
||||
let mut out = vec![];
|
||||
for id in &ids {
|
||||
let Some(m) = d.node_mut(*id).and_then(|n| n.mask.take()) else { continue };
|
||||
let Some(m) = d.node_mut(*id).and_then(|n| n.extras_mut().mask.take()) else { continue };
|
||||
let (par, idx, _) = d.position(*id).ok_or(EngineError::NoNode(*id))?;
|
||||
out.push(*id);
|
||||
for (k, part) in mask_parts(&m.art).iter().enumerate() {
|
||||
|
|
@ -259,12 +259,12 @@ fn set_flags(s: &mut Session, target: &Value, label: &str, p: &Value) -> Result<
|
|||
&& linked.is_none_or(|v| m.linked == v)
|
||||
};
|
||||
let d = &s.doc()?.doc;
|
||||
if ids.iter().all(|id| d.node(*id).and_then(|n| n.mask.as_deref()).is_none_or(has_flags)) {
|
||||
if ids.iter().all(|id| d.node(*id).and_then(|n| n.extras().mask.as_deref()).is_none_or(has_flags)) {
|
||||
return ok();
|
||||
}
|
||||
s.edit(label, |d, _| {
|
||||
for id in &ids {
|
||||
if let Some(m) = d.node_mut(*id).and_then(|n| n.mask.as_mut()) {
|
||||
if let Some(m) = d.node_mut(*id).and_then(|n| n.extras_if_any().and_then(|e| e.mask.as_mut())) {
|
||||
m.clip = clip.unwrap_or(m.clip);
|
||||
m.invert = invert.unwrap_or(m.invert);
|
||||
m.disabled = disabled.unwrap_or(m.disabled);
|
||||
|
|
@ -391,7 +391,7 @@ fn mask_info(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
let v: Vec<Value> = nodes_of(d, &transparency_targets(s, p)?)
|
||||
.into_iter()
|
||||
.filter_map(|n| {
|
||||
let m = n.mask.as_deref()?;
|
||||
let m = n.extras().mask.as_deref()?;
|
||||
Some(json!({ "id": n.id.0, "clip": m.clip, "invert": m.invert, "disabled": m.disabled, "linked": m.linked, "art": m.art.id.0 }))
|
||||
})
|
||||
.collect();
|
||||
|
|
@ -424,7 +424,7 @@ mod tests {
|
|||
assert_eq!(r["editing"], false);
|
||||
let d = &s.doc().unwrap().doc;
|
||||
assert!(d.node(NodeId(b)).is_none(), "mask art left the layer tree");
|
||||
let m = d.node(NodeId(a)).unwrap().mask.as_deref().unwrap();
|
||||
let m = d.node(NodeId(a)).unwrap().extras().mask.as_deref().unwrap();
|
||||
assert!(m.clip && m.linked && !m.invert && !m.disabled);
|
||||
assert_eq!(m.art.id, NodeId(b));
|
||||
}
|
||||
|
|
@ -438,7 +438,7 @@ mod tests {
|
|||
let d = &s.doc().unwrap().doc;
|
||||
let n = d.node(NodeId(g)).unwrap();
|
||||
assert_eq!(n.children().unwrap().len(), 2);
|
||||
assert!(!n.mask.as_ref().unwrap().clip);
|
||||
assert!(!n.extras().mask.as_ref().unwrap().clip);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -448,11 +448,11 @@ mod tests {
|
|||
s.execute("transparency.makeOpacityMask", &json!({})).unwrap();
|
||||
s.execute("transparency.releaseOpacityMask", &json!({})).unwrap();
|
||||
let st = s.doc().unwrap();
|
||||
assert!(st.doc.node(NodeId(a)).unwrap().mask.is_none());
|
||||
assert!(st.doc.node(NodeId(a)).unwrap().extras().mask.is_none());
|
||||
assert_eq!(st.selection.len(), 2);
|
||||
assert_eq!(st.doc.layers[0].children().unwrap().len(), 2);
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert!(s.doc().unwrap().doc.node(NodeId(a)).unwrap().mask.is_some());
|
||||
assert!(s.doc().unwrap().doc.node(NodeId(a)).unwrap().extras().mask.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -469,13 +469,13 @@ mod tests {
|
|||
assert_eq!(info[0]["disabled"], true);
|
||||
assert_eq!(info[0]["linked"], false);
|
||||
// Unlinked: moving the object leaves the mask art where it was.
|
||||
let before = s.doc().unwrap().doc.node(NodeId(a)).unwrap().mask.as_ref().unwrap().art.clone();
|
||||
let before = s.doc().unwrap().doc.node(NodeId(a)).unwrap().extras().mask.as_ref().unwrap().art.clone();
|
||||
s.execute("object.move", &json!({"dx": 30, "dy": 0})).unwrap();
|
||||
let after = s.doc().unwrap().doc.node(NodeId(a)).unwrap().mask.as_ref().unwrap().art.clone();
|
||||
let after = s.doc().unwrap().doc.node(NodeId(a)).unwrap().extras().mask.as_ref().unwrap().art.clone();
|
||||
assert_eq!(before.geometric_bounds(), after.geometric_bounds());
|
||||
s.execute("transparency.linkOpacityMask", &json!({})).unwrap();
|
||||
s.execute("object.move", &json!({"dx": 30, "dy": 0})).unwrap();
|
||||
let moved = s.doc().unwrap().doc.node(NodeId(a)).unwrap().mask.as_ref().unwrap().art.clone();
|
||||
let moved = s.doc().unwrap().doc.node(NodeId(a)).unwrap().extras().mask.as_ref().unwrap().art.clone();
|
||||
assert!((moved.geometric_bounds().unwrap().x0 - after.geometric_bounds().unwrap().x0 - 30.0).abs() < 1e-9);
|
||||
}
|
||||
|
||||
|
|
@ -485,7 +485,7 @@ mod tests {
|
|||
assert_eq!(s.execute("transparency.toggleNewMasksClipping", &json!({})).unwrap()["value"], false);
|
||||
select(&mut s, &[a, b]);
|
||||
s.execute("transparency.makeOpacityMask", &json!({})).unwrap();
|
||||
assert!(!s.doc().unwrap().doc.node(NodeId(a)).unwrap().mask.as_ref().unwrap().clip);
|
||||
assert!(!s.doc().unwrap().doc.node(NodeId(a)).unwrap().extras().mask.as_ref().unwrap().clip);
|
||||
s.execute("transparency.toggleNewMasksInverted", &json!({})).unwrap();
|
||||
assert_eq!(s.new_mask_defaults(), (false, true));
|
||||
}
|
||||
|
|
@ -498,6 +498,6 @@ mod tests {
|
|||
let d = s.doc().unwrap().doc.clone();
|
||||
let bytes = vectorcraft_format::save(&d, false);
|
||||
let back = vectorcraft_format::load(&bytes).unwrap();
|
||||
assert_eq!(back.node(NodeId(a)).unwrap().mask, d.node(NodeId(a)).unwrap().mask);
|
||||
assert_eq!(back.node(NodeId(a)).unwrap().extras().mask, d.node(NodeId(a)).unwrap().extras().mask);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -558,7 +558,7 @@ pub(crate) fn styled_copy(d: &mut Document, l: &Node, items: Vec<AppearanceItem>
|
|||
f.opacity = 1.0;
|
||||
f.blend = BlendMode::Normal;
|
||||
f.name = None;
|
||||
f.mask = None;
|
||||
f.extras_mut().mask = None;
|
||||
if let NodeKind::Path { live, .. } = &mut f.kind {
|
||||
*live = None;
|
||||
}
|
||||
|
|
@ -575,7 +575,7 @@ pub(crate) fn assemble(d: &mut Document, l: &Node, mut parts: Vec<Node>) -> Opti
|
|||
};
|
||||
let mut root = with_transparency(d, root, l.opacity, l.blend);
|
||||
root.name = l.name.clone();
|
||||
root.mask = l.mask.clone();
|
||||
root.extras_mut().mask = l.extras().mask.clone();
|
||||
root.appearance.effects = l.appearance.effects.clone();
|
||||
Some(root)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ use super::unique_name;
|
|||
/// Visit `n` and every descendant (children and opacity-mask art) mutably.
|
||||
pub(super) fn each_mut(n: &mut Node, f: &mut impl FnMut(&mut Node)) {
|
||||
f(n);
|
||||
if let Some(m) = &mut n.mask {
|
||||
if let Some(m) = n.extras_if_any().and_then(|e| e.mask.as_mut()) {
|
||||
each_mut(Arc::make_mut(&mut m.art), f);
|
||||
}
|
||||
if let Some(ch) = n.children_mut() {
|
||||
|
|
|
|||
|
|
@ -285,7 +285,7 @@ fn build(d: &mut Document, l: Loaded, link: Option<LinkInfo>) -> Node {
|
|||
link,
|
||||
placement: Default::default(),
|
||||
};
|
||||
Node::new(d.alloc_id(), NodeKind::Image(im))
|
||||
Node::new(d.alloc_id(), NodeKind::Image(Box::new(im)))
|
||||
}
|
||||
Art::Text(t) => Node::new(d.alloc_id(), NodeKind::Text(Box::new(text::area_text(t, l.natural)))),
|
||||
Art::Document(s, link, bounding) => document::node(d, &s, link, bounding),
|
||||
|
|
|
|||
|
|
@ -96,7 +96,7 @@ fn bitmap_document(cmd: &str, doc: &Document, set: &PrintSettings) -> Result<Doc
|
|||
out.images.insert(key.clone(), ImageBlob::new("image/png", img.to_png().map_err(EngineError::Other)?));
|
||||
let xf = Affine::translate(region.origin().to_vec2()) * Affine::scale(1.0 / scale);
|
||||
let image = ImageObject { key, width: img.width, height: img.height, xf, link: None, placement: Default::default() };
|
||||
images.push(Arc::new(Node::new(out.alloc_id(), NodeKind::Image(image))));
|
||||
images.push(Arc::new(Node::new(out.alloc_id(), NodeKind::Image(Box::new(image)))));
|
||||
}
|
||||
let mut layer = Node::layer(out.alloc_id(), "Bitmap", LayerColor::Preset(0));
|
||||
if let Some(children) = layer.children_mut() {
|
||||
|
|
|
|||
|
|
@ -203,8 +203,10 @@ pub(crate) fn effect_image(out: &mut Document, whole: &Node, knockout: Option<&N
|
|||
}
|
||||
out.images.insert(key.clone(), ImageBlob::new("image/png", png));
|
||||
let xf = Affine::translate(region.origin().to_vec2()) * Affine::scale(1.0 / scale);
|
||||
let mut image =
|
||||
Node::new(id, NodeKind::Image(ImageObject { key, width: img.width, height: img.height, xf, link: None, placement: Default::default() }));
|
||||
let mut image = Node::new(
|
||||
id,
|
||||
NodeKind::Image(Box::new(ImageObject { key, width: img.width, height: img.height, xf, link: None, placement: Default::default() })),
|
||||
);
|
||||
image.name = Some("Raster effect".into());
|
||||
Some(image)
|
||||
}
|
||||
|
|
@ -277,7 +279,8 @@ fn below_images(d: &mut Document, n: &Node) -> Option<Vec<Node>> {
|
|||
/// effects (they get images of their own).
|
||||
pub(crate) fn raster_image(d: &mut Document, n: &Node, below: bool, members: bool) -> Option<Node> {
|
||||
let mut whole = n.clone();
|
||||
(whole.opacity, whole.blend, whole.mask) = (1.0, Default::default(), None);
|
||||
(whole.opacity, whole.blend) = (1.0, Default::default());
|
||||
whole.extras_mut().mask = None;
|
||||
if below && members && is_container(&whole) {
|
||||
for c in whole.children_mut().into_iter().flatten() {
|
||||
strip_raster(Arc::make_mut(c), true);
|
||||
|
|
@ -317,7 +320,8 @@ pub(crate) fn put_below(d: &mut Document, m: &mut Node, image: Node) {
|
|||
_ => {
|
||||
let mut inner = m.clone();
|
||||
inner.id = d.alloc_id();
|
||||
(inner.name, inner.opacity, inner.blend, inner.isolate, inner.mask) = (None, 1.0, Default::default(), false, None);
|
||||
(inner.name, inner.opacity, inner.blend, inner.isolate) = (None, 1.0, Default::default(), false);
|
||||
inner.extras_mut().mask = None;
|
||||
// Knockout is between the object's own parts (a blend's steps): it stays with them,
|
||||
// so the image doesn't knock out the art over it.
|
||||
m.knockout = Default::default();
|
||||
|
|
@ -379,7 +383,9 @@ fn placed_symbol(d: &Document, n: &Node) -> Option<Node> {
|
|||
let NodeKind::SymbolInstance { symbol, xf } = &n.kind else { return None };
|
||||
let mut art = vectorcraft_render::instance_art(&d.symbols.iter().find(|s| s.name == *symbol)?.art, n);
|
||||
art.transform(*xf, false);
|
||||
Some(Node { kind: NodeKind::Group { children: vec![Arc::new(art)], clip: false }, ..n.clone() })
|
||||
let mut g = n.clone();
|
||||
g.kind = NodeKind::Group { children: vec![Arc::new(art)], clip: false };
|
||||
Some(g)
|
||||
}
|
||||
|
||||
/// Which objects' raster effects become images.
|
||||
|
|
@ -407,13 +413,13 @@ fn needs(n: &Node, which: Which) -> bool {
|
|||
n.visible
|
||||
&& !matches!(n.kind, NodeKind::Layer { template: true, .. })
|
||||
&& (imaged(n, which)
|
||||
|| n.mask.as_ref().is_some_and(|m| needs(&m.art, which))
|
||||
|| n.extras().mask.as_ref().is_some_and(|m| needs(&m.art, which))
|
||||
|| (is_container(n) && n.children().is_some_and(|ch| ch.iter().any(|c| needs(c, which)))))
|
||||
}
|
||||
|
||||
/// The art of `n`'s opacity mask with its raster effects as images.
|
||||
fn walk_mask(out: &mut Document, n: &mut Node, report: &mut FlattenReport) {
|
||||
if let Some(m) = n.mask.as_mut()
|
||||
if let Some(m) = n.extras_if_any().and_then(|e| e.mask.as_mut())
|
||||
&& let Some(art) = walk(out, &m.art, report)
|
||||
{
|
||||
m.art = Arc::new(art);
|
||||
|
|
|
|||
|
|
@ -85,14 +85,14 @@ mod tests {
|
|||
fn image(ppi: f64) -> Node {
|
||||
Node::new(
|
||||
NodeId(999),
|
||||
NodeKind::Image(ImageObject {
|
||||
NodeKind::Image(Box::new(ImageObject {
|
||||
key: "effect".into(),
|
||||
width: 8000,
|
||||
height: 8000,
|
||||
xf: Affine::scale(72.0 / ppi),
|
||||
link: None,
|
||||
placement: Default::default(),
|
||||
}),
|
||||
})),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -547,7 +547,7 @@ fn same_graphic_style(s: &mut Session, _: &Value) -> Result<Value> {
|
|||
.and_then(|id| super::style::linked_style(&st.doc, st.doc.node(*id)?))
|
||||
.map(|g| g.id)
|
||||
.ok_or_else(|| bad(cmd, "the selection has no graphic style"))?;
|
||||
select_where(s, cmd, &json!({}), |d, n| n.graphic_style == Some(id) && super::style::linked_style(d, n).is_some())
|
||||
select_where(s, cmd, &json!({}), |d, n| n.extras().graphic_style == Some(id) && super::style::linked_style(d, n).is_some())
|
||||
}
|
||||
|
||||
/// An appearance attribute Select > Same > Appearance Attribute matches.
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
|||
}
|
||||
|
||||
fn source_group(n: &Node) -> Option<&Node> {
|
||||
n.shaper.as_ref()?;
|
||||
n.extras().shaper.as_ref()?;
|
||||
n.children()?.first().map(AsRef::as_ref).filter(|g| g.name.as_deref() == Some(SOURCES))
|
||||
}
|
||||
fn sources(n: &Node) -> Vec<Arc<Node>> {
|
||||
|
|
@ -376,7 +376,7 @@ fn generated(d: &mut Document, src: &[Arc<Node>], spec: &ShaperSpec, map: &Build
|
|||
pub(crate) fn refresh(before: &Document, d: &mut Document) -> Result<()> {
|
||||
let mut ids = vec![];
|
||||
d.walk(|n| {
|
||||
if n.shaper.is_some() {
|
||||
if n.extras().shaper.is_some() {
|
||||
ids.push(n.id)
|
||||
}
|
||||
});
|
||||
|
|
@ -399,7 +399,7 @@ pub(crate) fn refresh(before: &Document, d: &mut Document) -> Result<()> {
|
|||
continue;
|
||||
}
|
||||
let map = map_of(&src)?;
|
||||
let mut spec = current.shaper.as_deref().cloned().unwrap_or_default();
|
||||
let mut spec = current.extras().shaper.as_deref().cloned().unwrap_or_default();
|
||||
let previous = sources(old);
|
||||
if previous.iter().map(|s| s.id).ne(src.iter().map(|s| s.id)) {
|
||||
// Source insertion, deletion or reordering updates the index-based recipes.
|
||||
|
|
@ -489,7 +489,7 @@ pub(crate) fn refresh(before: &Document, d: &mut Document) -> Result<()> {
|
|||
children.push(n);
|
||||
}
|
||||
let n = d.node_mut(id).ok_or(EngineError::NoNode(id))?;
|
||||
n.shaper = Some(Box::new(spec));
|
||||
n.extras_mut().shaper = Some(Box::new(spec));
|
||||
if let Some(ch) = n.children_mut() {
|
||||
*ch = children;
|
||||
}
|
||||
|
|
@ -509,7 +509,7 @@ fn candidates(s: &Session, p: &Value, points: &[Point], tolerance: f64) -> Resul
|
|||
st.doc.is_editable(*id)
|
||||
&& st.doc.node(*id).is_some_and(|n| {
|
||||
!matches!(n.kind, NodeKind::Path { guide: true, .. } | NodeKind::Path { clipping: true, .. })
|
||||
&& (n.shaper.is_some() || b::node_outline(n).is_some())
|
||||
&& (n.extras().shaper.is_some() || b::node_outline(n).is_some())
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
|
@ -578,7 +578,7 @@ fn scribble(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
let mut next_merge = 1u64;
|
||||
for id in &roots {
|
||||
let n = s.doc()?.doc.node(*id).ok_or(EngineError::NoNode(*id))?;
|
||||
if let Some(previous) = n.shaper.as_deref() {
|
||||
if let Some(previous) = n.extras().shaper.as_deref() {
|
||||
let offset = src.len();
|
||||
let merge_offset = next_merge;
|
||||
for mut e in previous.faces.clone() {
|
||||
|
|
@ -669,7 +669,7 @@ fn scribble(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
let id = d.alloc_id();
|
||||
let mut group = Node::group(id, children);
|
||||
group.name = Some("Shaper Group".into());
|
||||
group.shaper = Some(Box::new(spec));
|
||||
group.extras_mut().shaper = Some(Box::new(spec));
|
||||
for id in &roots {
|
||||
d.remove(*id)?;
|
||||
}
|
||||
|
|
@ -684,7 +684,7 @@ fn scribble(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
fn group_at(d: &Document, point: Point, construction: bool) -> Option<NodeId> {
|
||||
let mut groups = vec![];
|
||||
d.walk(|n| {
|
||||
if n.shaper.is_some()
|
||||
if n.extras().shaper.is_some()
|
||||
&& d.is_editable(n.id)
|
||||
&& (if construction { source_group(n).and_then(Node::geometric_bounds) } else { n.geometric_bounds() })
|
||||
.is_some_and(|b| b.inflate(6.0, 6.0).contains(point))
|
||||
|
|
@ -733,7 +733,7 @@ fn target(s: &Session, p: &Value) -> Result<NodeId> {
|
|||
let id = id_param(p, "id")
|
||||
.or_else(|| s.active().and_then(|d| d.selection.objects.first().copied()))
|
||||
.ok_or_else(|| bad("shaper.release", "select a Shaper group"))?;
|
||||
s.doc()?.doc.node(id).filter(|n| n.shaper.is_some()).ok_or_else(|| bad("shaper.release", "select a Shaper group"))?;
|
||||
s.doc()?.doc.node(id).filter(|n| n.extras().shaper.is_some()).ok_or_else(|| bad("shaper.release", "select a Shaper group"))?;
|
||||
Ok(id)
|
||||
}
|
||||
fn release(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
|
|
@ -756,7 +756,7 @@ fn expand(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
let id = target(s, p)?;
|
||||
s.edit("Expand Shaper Group", |d, sel| {
|
||||
let n = d.node_mut(id).ok_or(EngineError::NoNode(id))?;
|
||||
n.shaper = None;
|
||||
n.extras_mut().shaper = None;
|
||||
n.name = None;
|
||||
if let Some(children) = n.children_mut()
|
||||
&& children.first().is_some_and(|c| c.name.as_deref() == Some(SOURCES))
|
||||
|
|
|
|||
|
|
@ -210,7 +210,7 @@ fn selected(s: &Session) -> Result<Vec<NodeId>> {
|
|||
}
|
||||
|
||||
pub(crate) fn selected_slices(d: &Document, sel: &vectorcraft_doc::Selection) -> Vec<NodeId> {
|
||||
let objects = sel.objects.iter().filter(|id| d.node(**id).is_some_and(|n| n.slice.is_some()));
|
||||
let objects = sel.objects.iter().filter(|id| d.node(**id).is_some_and(|n| n.extras().slice.is_some()));
|
||||
dedup(sel.slices.iter().chain(objects).copied().collect())
|
||||
}
|
||||
|
||||
|
|
@ -234,7 +234,7 @@ fn doc_has_slices(s: &Session) -> std::result::Result<(), String> {
|
|||
fn object_slice_ids(d: &Document) -> Vec<NodeId> {
|
||||
let mut ids = vec![];
|
||||
d.walk(|n| {
|
||||
if n.slice.is_some() {
|
||||
if n.extras().slice.is_some() {
|
||||
ids.push(n.id);
|
||||
}
|
||||
});
|
||||
|
|
@ -260,7 +260,7 @@ pub(crate) fn remove_slice(d: &mut Document, id: NodeId) {
|
|||
if d.slices.len() == before
|
||||
&& let Some(n) = d.node_mut(id)
|
||||
{
|
||||
n.slice = None;
|
||||
n.extras_mut().slice = None;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -284,14 +284,14 @@ fn make(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
None => super::edit::selected_roots(s)?,
|
||||
};
|
||||
let d = &s.doc()?.doc;
|
||||
let made: Vec<NodeId> = ids.into_iter().filter(|id| d.node(*id).is_some_and(|n| !n.is_layer() && n.slice.is_none())).collect();
|
||||
let made: Vec<NodeId> = ids.into_iter().filter(|id| d.node(*id).is_some_and(|n| !n.is_layer() && n.extras().slice.is_none())).collect();
|
||||
if made.is_empty() {
|
||||
return Ok(json!({ "ids": [] }));
|
||||
}
|
||||
s.edit("Make Slice", |d, _| {
|
||||
for id in &made {
|
||||
if let Some(n) = d.node_mut(*id) {
|
||||
n.slice = Some(Box::default());
|
||||
n.extras_mut().slice = Some(Box::default());
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
|
|
@ -559,7 +559,7 @@ fn options(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
for id in &targets {
|
||||
let o = match d.slice_mut(*id) {
|
||||
Some(sl) => Some(&mut sl.options),
|
||||
None => d.node_mut(*id).and_then(|n| n.slice.as_deref_mut()),
|
||||
None => d.node_mut(*id).and_then(|n| n.extras_if_any().and_then(|e| e.slice.as_deref_mut())),
|
||||
};
|
||||
if let Some(o) = o {
|
||||
edits.apply(o);
|
||||
|
|
|
|||
|
|
@ -175,7 +175,7 @@ pub(crate) fn in_sync(n: &Node, g: &GraphicStyle) -> bool {
|
|||
|
||||
/// The style `n` is linked to (and still looks like).
|
||||
pub(crate) fn linked_style<'a>(d: &'a Document, n: &Node) -> Option<&'a GraphicStyle> {
|
||||
d.graphic_style_by_id(n.graphic_style?).filter(|g| in_sync(n, g))
|
||||
d.graphic_style_by_id(n.extras().graphic_style?).filter(|g| in_sync(n, g))
|
||||
}
|
||||
|
||||
/// Objects anywhere in the art that `f` accepts.
|
||||
|
|
@ -272,14 +272,14 @@ fn style_node(n: &mut Node, g: &GraphicStyle, chars: bool) {
|
|||
fn apply_style(d: &mut Document, id: NodeId, g: &GraphicStyle, chars: bool) {
|
||||
if let Some(n) = d.node_mut(id) {
|
||||
style_node(n, g, chars);
|
||||
n.graphic_style = Some(g.id);
|
||||
n.extras_mut().graphic_style = Some(g.id);
|
||||
}
|
||||
}
|
||||
|
||||
fn set_link(d: &mut Document, ids: &[NodeId], link: Option<u32>) {
|
||||
for id in ids {
|
||||
if let Some(n) = d.node_mut(*id) {
|
||||
n.graphic_style = link;
|
||||
n.extras_mut().graphic_style = link;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -303,7 +303,7 @@ impl Session {
|
|||
pub fn selection_graphic_style(&self) -> Option<(&GraphicStyle, bool)> {
|
||||
let st = self.active()?;
|
||||
let n = st.doc.node(*st.selection.subjects().first()?)?;
|
||||
let g = st.doc.graphic_style_by_id(n.graphic_style?)?;
|
||||
let g = st.doc.graphic_style_by_id(n.extras().graphic_style?)?;
|
||||
Some((g, in_sync(n, g)))
|
||||
}
|
||||
|
||||
|
|
@ -341,7 +341,7 @@ fn style_apply(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
let ap = g.appearance_on(n).into_owned();
|
||||
n.appearance.items.extend(ap.items);
|
||||
n.appearance.effects.extend(ap.effects);
|
||||
n.graphic_style = None;
|
||||
n.extras_mut().graphic_style = None;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
|
@ -383,7 +383,7 @@ fn style_delete(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
let i = style_index(d, name)?;
|
||||
let id = d.graphic_styles.remove(i).id;
|
||||
if id != 0 {
|
||||
let ids = objects_where(d, |n| n.graphic_style == Some(id));
|
||||
let ids = objects_where(d, |n| n.extras().graphic_style == Some(id));
|
||||
set_link(d, &ids, None);
|
||||
}
|
||||
}
|
||||
|
|
@ -447,6 +447,7 @@ fn style_redefine(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
let i = match str_param(p, "name") {
|
||||
Some(name) => style_index(d, name)?,
|
||||
None => n
|
||||
.extras()
|
||||
.graphic_style
|
||||
.and_then(|id| d.graphic_style_by_id(id))
|
||||
.and_then(|g| d.graphic_style_index(&g.name))
|
||||
|
|
@ -460,7 +461,7 @@ fn style_redefine(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
let old = std::mem::replace(&mut d.graphic_styles[i], GraphicStyle { name: name.clone(), id, ..look });
|
||||
// Objects still linked follow the new definition; edited ones are unlinked (they keep
|
||||
// their look), and the source keeps its own.
|
||||
let linked = objects_where(d, |n| n.graphic_style == Some(id) && n.id != src);
|
||||
let linked = objects_where(d, |n| n.extras().graphic_style == Some(id) && n.id != src);
|
||||
let (follow, edited): (Vec<NodeId>, Vec<NodeId>) = linked.into_iter().partition(|nid| d.node(*nid).is_some_and(|n| in_sync(n, &old)));
|
||||
set_link(d, &edited, None);
|
||||
let g = d.graphic_styles[i].clone();
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ fn make(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
if matches!(n.kind, NodeKind::Layer { .. }) {
|
||||
return Err(EngineError::Other("layers can't be wrap objects".into()));
|
||||
}
|
||||
n.wrap = Some(w);
|
||||
n.extras_mut().wrap = Some(w);
|
||||
}
|
||||
Ok(())
|
||||
})?;
|
||||
|
|
@ -58,7 +58,10 @@ fn make(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
fn release(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let ids: Vec<NodeId> = {
|
||||
let d = &s.doc()?.doc;
|
||||
checked_ids_or_selection(s, p, "object.textWrap.release")?.into_iter().filter(|i| d.node(*i).is_some_and(|n| n.wrap.is_some())).collect()
|
||||
checked_ids_or_selection(s, p, "object.textWrap.release")?
|
||||
.into_iter()
|
||||
.filter(|i| d.node(*i).is_some_and(|n| n.extras().wrap.is_some()))
|
||||
.collect()
|
||||
};
|
||||
if ids.is_empty() {
|
||||
return Err(EngineError::Other("Text Wrap: select a wrap object".into()));
|
||||
|
|
@ -66,7 +69,7 @@ fn release(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
s.edit("Release Text Wrap", |d, _| {
|
||||
for id in &ids {
|
||||
if let Some(n) = d.node_mut(*id) {
|
||||
n.wrap = None;
|
||||
n.extras_mut().wrap = None;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
|
|
@ -78,7 +81,7 @@ fn options(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
let ids = checked_ids_or_selection(s, p, "object.textWrap.options")?;
|
||||
let current = {
|
||||
let d = &s.doc()?.doc;
|
||||
ids.iter().find_map(|i| d.node(*i).and_then(|n| n.wrap)).unwrap_or_default()
|
||||
ids.iter().find_map(|i| d.node(*i).and_then(|n| n.extras().wrap)).unwrap_or_default()
|
||||
};
|
||||
if p.get("offset").is_none() && p.get("invert").is_none() {
|
||||
return Ok(json!({ "offset": current.offset, "invert": current.invert }));
|
||||
|
|
@ -89,7 +92,7 @@ fn options(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
if let Some(n) = d.node_mut(*id)
|
||||
&& !matches!(n.kind, NodeKind::Layer { .. })
|
||||
{
|
||||
n.wrap = Some(w);
|
||||
n.extras_mut().wrap = Some(w);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
|
|
@ -131,7 +134,7 @@ fn collect<'a>(n: &'a Node, out: &mut Vec<Item<'a>>) {
|
|||
if !n.visible {
|
||||
return;
|
||||
}
|
||||
if n.wrap.is_some() && !matches!(n.kind, NodeKind::Layer { .. }) {
|
||||
if n.extras().wrap.is_some() && !matches!(n.kind, NodeKind::Layer { .. }) {
|
||||
out.push(Item::Wrap(n));
|
||||
return;
|
||||
}
|
||||
|
|
@ -147,7 +150,7 @@ fn collect<'a>(n: &'a Node, out: &mut Vec<Item<'a>>) {
|
|||
}
|
||||
|
||||
fn any_wrap_state(n: &Node) -> bool {
|
||||
n.wrap.is_some()
|
||||
n.extras().wrap.is_some()
|
||||
|| matches!(&n.kind, NodeKind::Text(t) if !t.wrap.is_empty())
|
||||
|| n.children().is_some_and(|ch| ch.iter().any(|c| any_wrap_state(c)))
|
||||
}
|
||||
|
|
@ -173,7 +176,7 @@ pub(crate) fn refresh(doc: &mut Document) {
|
|||
outline(n, &mut pd);
|
||||
(!pd.is_empty()).then(|| {
|
||||
pd.transform(inv);
|
||||
WrapShape { path: pd, wrap: n.wrap.unwrap_or_default() }
|
||||
WrapShape { path: pd, wrap: n.extras().wrap.unwrap_or_default() }
|
||||
})
|
||||
}
|
||||
Item::Text(_) => None,
|
||||
|
|
@ -219,18 +222,18 @@ mod tests {
|
|||
let e = s.execute(command, &json!({"ids": ids, "offset": 12})).unwrap_err().to_string();
|
||||
assert!(e.contains("id") || e.contains("array"), "{command}: {e}");
|
||||
assert_eq!(s.doc().unwrap().history.undo.len(), before, "{command} added history on error");
|
||||
assert!(s.doc().unwrap().doc.node(NodeId(id)).unwrap().wrap.is_none());
|
||||
assert!(s.doc().unwrap().doc.node(NodeId(other)).unwrap().wrap.is_none());
|
||||
assert!(s.doc().unwrap().doc.node(NodeId(id)).unwrap().extras().wrap.is_none());
|
||||
assert!(s.doc().unwrap().doc.node(NodeId(other)).unwrap().extras().wrap.is_none());
|
||||
}
|
||||
}
|
||||
|
||||
s.execute("object.textWrap.make", &json!({"ids": [id]})).unwrap();
|
||||
assert!(s.doc().unwrap().doc.node(NodeId(id)).unwrap().wrap.is_some());
|
||||
assert!(s.doc().unwrap().doc.node(NodeId(other)).unwrap().wrap.is_none());
|
||||
assert!(s.doc().unwrap().doc.node(NodeId(id)).unwrap().extras().wrap.is_some());
|
||||
assert!(s.doc().unwrap().doc.node(NodeId(other)).unwrap().extras().wrap.is_none());
|
||||
s.execute("object.textWrap.options", &json!({"ids": [id], "offset": 12})).unwrap();
|
||||
assert_eq!(s.doc().unwrap().doc.node(NodeId(id)).unwrap().wrap.unwrap().offset, 12.0);
|
||||
assert_eq!(s.doc().unwrap().doc.node(NodeId(id)).unwrap().extras().wrap.unwrap().offset, 12.0);
|
||||
s.execute("object.textWrap.release", &json!({"ids": [id]})).unwrap();
|
||||
assert!(s.doc().unwrap().doc.node(NodeId(id)).unwrap().wrap.is_none());
|
||||
assert!(s.doc().unwrap().doc.node(NodeId(id)).unwrap().extras().wrap.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -683,7 +683,7 @@ fn reshape_area(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
}
|
||||
let d = Vec2::new(f64_req(p, "dx", C)?, f64_req(p, "dy", C)?);
|
||||
let node = s.doc()?.doc.node(id);
|
||||
if area_text(node).is_none() || node.is_some_and(|n| n.perspective.is_some()) {
|
||||
if area_text(node).is_none() || node.is_some_and(|n| n.extras().perspective.is_some()) {
|
||||
return Err(bad(C, "not area type (text in a frame) outside perspective"));
|
||||
}
|
||||
s.edit("Reshape Type Area", |doc, _| {
|
||||
|
|
|
|||
|
|
@ -73,15 +73,47 @@ pub struct HistoryEntry {
|
|||
/// Roughly the memory this step keeps of its own: what of `doc` the step after it (or the
|
||||
/// document now) doesn't share ([`Document::retained_bytes`]).
|
||||
pub bytes: usize,
|
||||
/// The same the other way round: what that next state keeps that `doc` doesn't share. Undoing
|
||||
/// (redoing) this step makes a step that keeps the next state: this is its `bytes`.
|
||||
next_bytes: usize,
|
||||
/// Which document the figures were taken against (its address): the next state.
|
||||
next: usize,
|
||||
}
|
||||
|
||||
impl HistoryEntry {
|
||||
/// A step keeping `doc` and `selection`, as they were before the edit that made `next`.
|
||||
pub(crate) fn kept(label: String, doc: Arc<Document>, selection: Arc<Selection>, next: &Document) -> Self {
|
||||
doc.drop_lookup_cache();
|
||||
let bytes = doc.retained_bytes(next);
|
||||
HistoryEntry { label, doc, selection, bytes }
|
||||
let (bytes, next_bytes) = doc.retained_bytes_each(next);
|
||||
HistoryEntry { label, doc, selection, bytes, next_bytes, next: std::ptr::from_ref(next) as usize }
|
||||
}
|
||||
|
||||
/// The step that undoing (redoing) the step `costs` came from makes: it keeps `doc`, the
|
||||
/// document before the undo, whose next state is now `now` (that step's document). Its figures
|
||||
/// are that step's the other way round when `doc` is still the document they were taken
|
||||
/// against (nothing replaced it since), so undo and redo needn't walk the documents again. A
|
||||
/// change made to it in place without an undo step (a setting, a refresh) goes unnoticed: a
|
||||
/// small error, for an estimate.
|
||||
pub(crate) fn turned(label: String, doc: Arc<Document>, selection: Arc<Selection>, now: &Arc<Document>, costs: StepCosts) -> Self {
|
||||
if costs.next != Arc::as_ptr(&doc) as usize {
|
||||
return Self::kept(label, doc, selection, now);
|
||||
}
|
||||
doc.drop_lookup_cache();
|
||||
HistoryEntry { label, doc, selection, bytes: costs.next_bytes, next_bytes: costs.bytes, next: Arc::as_ptr(now) as usize }
|
||||
}
|
||||
|
||||
/// The figures [`HistoryEntry::turned`] reuses.
|
||||
pub(crate) fn costs(&self) -> StepCosts {
|
||||
StepCosts { bytes: self.bytes, next_bytes: self.next_bytes, next: self.next }
|
||||
}
|
||||
}
|
||||
|
||||
/// A step's memory figures ([`HistoryEntry::costs`]).
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub(crate) struct StepCosts {
|
||||
bytes: usize,
|
||||
next_bytes: usize,
|
||||
next: usize,
|
||||
}
|
||||
|
||||
/// The undo history of a document: bounded by a number of steps (Preferences › Performance ›
|
||||
|
|
@ -141,7 +173,8 @@ impl History {
|
|||
/// merged into it, or an undo group extended it).
|
||||
pub(crate) fn recost_last(&mut self, now: &Document) {
|
||||
if let Some(e) = self.undo.last_mut() {
|
||||
e.bytes = e.doc.retained_bytes(now);
|
||||
(e.bytes, e.next_bytes) = e.doc.retained_bytes_each(now);
|
||||
e.next = std::ptr::from_ref(now) as usize;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -981,7 +981,7 @@ fn live_shape_properties_already_set_record_no_undo_step() {
|
|||
let d = std::sync::Arc::make_mut(&mut s.doc_mut().unwrap().doc);
|
||||
let NodeKind::Path { path, live: Some(live), .. } = &mut d.node_mut(r).unwrap().kind else { panic!("not a live rectangle") };
|
||||
let xf = Affine::scale_non_uniform(2.0, 1.0);
|
||||
*live = vectorcraft_doc::LiveShape::Rectangle { w: 50.0, h: 60.0, radii: [10.0; 4], kinds: Default::default(), xf };
|
||||
**live = vectorcraft_doc::LiveShape::Rectangle { w: 50.0, h: 60.0, radii: [10.0; 4], kinds: Default::default(), xf };
|
||||
*path = live.to_path();
|
||||
}
|
||||
let shown = vectorcraft_doc::LiveCorners::of(s.doc().unwrap().doc.node(r).unwrap()).unwrap().radius(0);
|
||||
|
|
@ -1762,8 +1762,8 @@ fn graph_tool_drag_creates_a_graph_and_asks_for_data() {
|
|||
let st = s.doc().unwrap();
|
||||
assert_eq!(st.history.undo.len(), undo_before + 1);
|
||||
let g = st.doc.node(st.selection.objects[0]).unwrap();
|
||||
assert_eq!(g.graph.as_ref().unwrap().kind, vectorcraft_doc::GraphKind::Pie);
|
||||
assert_eq!(g.graph.as_ref().unwrap().rect, vectorcraft_geom::Rect::new(100.0, 100.0, 300.0, 250.0));
|
||||
assert_eq!(g.extras().graph.as_ref().unwrap().kind, vectorcraft_doc::GraphKind::Pie);
|
||||
assert_eq!(g.extras().graph.as_ref().unwrap().rect, vectorcraft_geom::Rect::new(100.0, 100.0, 300.0, 250.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -1857,17 +1857,13 @@ fn shaper_turns_rough_strokes_into_live_shapes_and_scribbles_punch_fills() {
|
|||
stroke(&mut s, &rect);
|
||||
let id = s.doc().unwrap().selection.objects[0];
|
||||
let n = s.doc().unwrap().doc.node(id).unwrap().clone();
|
||||
assert!(
|
||||
matches!(n.kind, vectorcraft_doc::NodeKind::Path { live: Some(vectorcraft_doc::LiveShape::Rectangle { .. }), .. }),
|
||||
"{:?}",
|
||||
n.kind_label()
|
||||
);
|
||||
assert!(matches!(n.live_shape(), Some(vectorcraft_doc::LiveShape::Rectangle { .. })), "{:?}", n.kind_label());
|
||||
// A zig-zag inside it clears its fill while retaining the editable original and stroke.
|
||||
let zig: Vec<(f64, f64)> = (0..30).map(|i| (150.0 + (i % 2) as f64 * 80.0, 120.0 + i as f64 * 2.0)).collect();
|
||||
stroke(&mut s, &zig);
|
||||
assert_eq!(s.doc().unwrap().doc.node(id).unwrap(), &n);
|
||||
let group = s.doc().unwrap().doc.node(s.doc().unwrap().selection.objects[0]).unwrap();
|
||||
assert!(group.shaper.is_some());
|
||||
assert!(group.extras().shaper.is_some());
|
||||
assert!(group.children().unwrap().iter().skip(1).all(|n| n.appearance.fill_paint().is_none()));
|
||||
assert!(group.children().unwrap().iter().skip(1).any(|n| !n.appearance.stroke_paint().is_none()));
|
||||
}
|
||||
|
|
@ -1889,10 +1885,7 @@ fn shaper_square_started_mid_edge_closes_on_pointer_release_and_undoes_once() {
|
|||
s.pointer(&PointerEvent::new(kind, x, y), v).unwrap();
|
||||
}
|
||||
let id = *s.doc().unwrap().selection.objects.first().expect("recognized square");
|
||||
assert!(matches!(
|
||||
s.doc().unwrap().doc.node(id).unwrap().kind,
|
||||
vectorcraft_doc::NodeKind::Path { live: Some(vectorcraft_doc::LiveShape::Rectangle { .. }), .. }
|
||||
));
|
||||
assert!(matches!(s.doc().unwrap().doc.node(id).unwrap().live_shape(), Some(vectorcraft_doc::LiveShape::Rectangle { .. })));
|
||||
assert_eq!(s.doc().unwrap().history.undo.len(), undo + 1);
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert!(s.doc().unwrap().doc.node(id).is_none());
|
||||
|
|
@ -1928,7 +1921,7 @@ fn shaper_creates_upright_or_inverted_live_triangles_with_one_undo_step() {
|
|||
s.pointer(&PointerEvent::new(PointerKind::Up, pts[0].x, pts[0].y), v).unwrap();
|
||||
let id = *s.doc().unwrap().selection.objects.first().expect("recognized triangle");
|
||||
let node = s.doc().unwrap().doc.node(id).unwrap().clone();
|
||||
let vectorcraft_doc::NodeKind::Path { live: Some(vectorcraft_doc::LiveShape::Polygon { sides: 3, xf, .. }), .. } = &node.kind else {
|
||||
let Some(vectorcraft_doc::LiveShape::Polygon { sides: 3, xf, .. }) = node.live_shape() else {
|
||||
panic!("expected live triangle, got {:?}", node.kind_label());
|
||||
};
|
||||
let [a, b, c, d, _, _] = xf.as_coeffs();
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ fn red_image(s: &mut Session) -> NodeId {
|
|||
link: None,
|
||||
placement: Default::default(),
|
||||
};
|
||||
d.insert(d.default_layer(), 0, Node::new(id, NodeKind::Image(im)))?;
|
||||
d.insert(d.default_layer(), 0, Node::new(id, NodeKind::Image(Box::new(im))))?;
|
||||
sel.set([id]);
|
||||
Ok(id)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -52,13 +52,13 @@ fn each_attribute_sets_reads_back_and_undoes_in_one_step() {
|
|||
assert_eq!(i[k], v, "{k}");
|
||||
}
|
||||
let n = s.doc().unwrap().doc.node(a).unwrap().clone();
|
||||
let at = n.attrs.as_deref().unwrap();
|
||||
let at = n.extras().attrs.as_deref().unwrap();
|
||||
assert_eq!((at.show_center, at.image_map, n.url()), (Some(false), ImageMap::Rectangle, Some("https://example.com")));
|
||||
assert!(!n.shows_center());
|
||||
// One undo step takes everything back, and the attributes go away entirely.
|
||||
run(&mut s, "edit.undo", json!({}));
|
||||
let n = s.doc().unwrap().doc.node(a).unwrap().clone();
|
||||
assert!(n.attrs.is_none() && !n.has_overprint());
|
||||
assert!(n.extras().attrs.is_none() && !n.has_overprint());
|
||||
// Nothing to change: no undo step; bad values are refused.
|
||||
let before = s.doc().unwrap().revision;
|
||||
assert_eq!(run(&mut s, "attributes.set", json!({"showCenter": true}))["changed"], 0);
|
||||
|
|
@ -68,7 +68,7 @@ fn each_attribute_sets_reads_back_and_undoes_in_one_step() {
|
|||
// Clearing the URL and note drops the attributes.
|
||||
run(&mut s, "attributes.set", json!({"url": "u", "note": "n"}));
|
||||
run(&mut s, "attributes.set", json!({"url": "", "note": ""}));
|
||||
assert!(s.doc().unwrap().doc.node(a).unwrap().attrs.is_none());
|
||||
assert!(s.doc().unwrap().doc.node(a).unwrap().extras().attrs.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -168,8 +168,8 @@ fn attributes_survive_a_native_round_trip() {
|
|||
run(&mut s, "attributes.set", json!({"showCenter": false, "imageMap": "polygon", "url": "https://example.com/x", "note": "keep"}));
|
||||
let doc = s.doc().unwrap().doc.clone();
|
||||
let back = vectorcraft_format::load(&vectorcraft_format::save(&doc, false)).unwrap();
|
||||
assert_eq!(back.node(a).unwrap().attrs, doc.node(a).unwrap().attrs);
|
||||
assert!(back.node(a).unwrap().attrs.is_some());
|
||||
assert_eq!(back.node(a).unwrap().extras().attrs, doc.node(a).unwrap().extras().attrs);
|
||||
assert!(back.node(a).unwrap().extras().attrs.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -197,7 +197,7 @@ fn svg_export_links_objects_and_import_reads_the_links_back() {
|
|||
fn object_data_is_set_saved_and_written_to_svg() {
|
||||
let mut s = session();
|
||||
let a = rect(&mut s, 10.0);
|
||||
let data = |s: &Session| s.doc().unwrap().doc.node(a).unwrap().attrs.as_deref().map(|x| x.data.clone()).unwrap_or_default();
|
||||
let data = |s: &Session| s.doc().unwrap().doc.node(a).unwrap().extras().attrs.as_deref().map(|x| x.data.clone()).unwrap_or_default();
|
||||
let pair = |k: &str, v: &str| (k.to_string(), v.to_string());
|
||||
run(&mut s, "object.setProps", json!({"data": {"pivot": "60,100", "z": 3}}));
|
||||
assert_eq!(data(&s), [pair("pivot", "60,100"), pair("z", "3")], "values are text");
|
||||
|
|
@ -212,5 +212,5 @@ fn object_data_is_set_saved_and_written_to_svg() {
|
|||
assert!(svg.contains("data-pivot=\"60,100\" data-z=\"3\""), "{svg}");
|
||||
let native = vectorcraft_format::save(&s.doc().unwrap().doc, false);
|
||||
let back = vectorcraft_format::load(&native).unwrap();
|
||||
assert_eq!(back.node(a).unwrap().attrs, s.doc().unwrap().doc.node(a).unwrap().attrs);
|
||||
assert_eq!(back.node(a).unwrap().extras().attrs, s.doc().unwrap().doc.node(a).unwrap().extras().attrs);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -166,7 +166,7 @@ fn expand_and_release_keep_what_the_blend_carries() {
|
|||
s.edit("mask", |d, _| {
|
||||
let art = Node::path(d.alloc_id(), vectorcraft_geom::shapes::rectangle(Rect::new(0.0, 0.0, 50.0, 10.0)), Default::default());
|
||||
let mask = vectorcraft_doc::OpacityMask::new(art, false);
|
||||
d.node_mut(g).unwrap().mask = Some(Box::new(mask));
|
||||
d.node_mut(g).unwrap().extras_mut().mask = Some(Box::new(mask));
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
|
|
@ -176,14 +176,14 @@ fn expand_and_release_keep_what_the_blend_carries() {
|
|||
let e = node(&s, g);
|
||||
assert!(matches!(e.kind, NodeKind::Group { .. }));
|
||||
assert_eq!((e.name.as_deref(), e.opacity, e.isolate, e.knockout), (Some("Fade"), before.opacity, true, Knockout::On));
|
||||
assert_eq!(e.mask, before.mask);
|
||||
assert_eq!(e.extras().mask, before.extras().mask);
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
s.execute("select.set", &json!({"ids": [g.0]})).unwrap();
|
||||
let r = s.execute("object.blend.release", &json!({})).unwrap();
|
||||
let grp = NodeId(r["groups"][0].as_u64().unwrap());
|
||||
let n = node(&s, grp);
|
||||
assert_eq!((n.name.as_deref(), n.opacity, n.isolate), (Some("Fade"), before.opacity, true));
|
||||
assert_eq!(n.mask, before.mask);
|
||||
assert_eq!(n.extras().mask, before.extras().mask);
|
||||
assert_eq!(n.children().unwrap().len(), 3, "spine and keys inside");
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ fn node(s: &Session, id: NodeId) -> Node {
|
|||
|
||||
fn spec(s: &Session, id: NodeId) -> (Vec<NodeId>, BlendSpec) {
|
||||
match node(s, id).kind {
|
||||
NodeKind::Blend { children, spec } => (children.iter().map(|c| c.id).collect(), spec),
|
||||
NodeKind::Blend { children, spec } => (children.iter().map(|c| c.id).collect(), *spec),
|
||||
_ => panic!("not a blend"),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ fn node(s: &Session, id: NodeId) -> Node {
|
|||
|
||||
fn spec(s: &Session, id: NodeId) -> BlendSpec {
|
||||
match node(s, id).kind {
|
||||
NodeKind::Blend { spec, .. } => spec,
|
||||
NodeKind::Blend { spec, .. } => *spec,
|
||||
_ => panic!("not a blend"),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -592,7 +592,10 @@ fn add_image(s: &mut Session, r: &vectorcraft_trace::Raster, xf: Affine) -> Node
|
|||
d.insert(
|
||||
l,
|
||||
0,
|
||||
Node::new(id, NodeKind::Image(ImageObject { key: "img1".into(), width: w, height: h, xf, link: None, placement: Default::default() })),
|
||||
Node::new(
|
||||
id,
|
||||
NodeKind::Image(Box::new(ImageObject { key: "img1".into(), width: w, height: h, xf, link: None, placement: Default::default() })),
|
||||
),
|
||||
)?;
|
||||
sel.set([id]);
|
||||
Ok(id)
|
||||
|
|
@ -687,7 +690,7 @@ fn image_trace_palettes_and_swatch_libraries() {
|
|||
let doc = crate::cmd::swatchlib::library_colors(&s, "document").unwrap();
|
||||
let got = fills(&s, &r["id"]);
|
||||
assert!(!got.is_empty() && got.iter().all(|c| doc.contains(c)), "{got:?}");
|
||||
let t = node(&s, NodeId(r["id"].as_u64().unwrap())).trace.unwrap();
|
||||
let t = node(&s, NodeId(r["id"].as_u64().unwrap())).extras().trace.clone().unwrap();
|
||||
assert_eq!((t["params"]["palette"].as_str(), t["params"]["library"].as_str()), (Some("documentLibrary"), Some("document")));
|
||||
assert!(t["params"].get("swatches").is_none(), "looked up again at every trace");
|
||||
// A swatch library by id: the web-safe colours.
|
||||
|
|
@ -733,7 +736,7 @@ fn image_trace_create_strokes_makes_stroked_centre_lines() {
|
|||
assert!(!sp.closed);
|
||||
let (a, b) = (sp.anchors[0].p, sp.anchors[sp.anchors.len() - 1].p);
|
||||
assert!((a.y - 201.0).abs() < 1.5 && (b.y - 201.0).abs() < 1.5 && (a.x - b.x).abs() > 140.0, "{a:?} … {b:?}");
|
||||
let t = node(&s, g).trace.unwrap();
|
||||
let t = node(&s, g).extras().trace.clone().unwrap();
|
||||
assert_eq!((t["params"]["strokes"].as_bool(), t["params"]["strokeWidth"].as_f64()), (Some(true), Some(5.0)));
|
||||
// Expand keeps the stroked line.
|
||||
s.execute("imageTrace.expand", &json!({})).unwrap();
|
||||
|
|
@ -777,7 +780,7 @@ fn image_trace_flat_logo_preset_makes_a_true_circle() {
|
|||
assert!(r["anchors"].as_u64().unwrap() <= 6, "a circle is four anchors: {r}");
|
||||
let g = NodeId(r["id"].as_u64().unwrap());
|
||||
let n = node(&s, g);
|
||||
let t = n.trace.clone().expect("settings stored");
|
||||
let t = n.extras().trace.clone().expect("settings stored");
|
||||
assert_eq!((t["preset"].as_str(), t["params"]["mode"].as_str()), (Some("Flat Logo"), Some("logo")));
|
||||
let paths = traced_paths(&n);
|
||||
let (p, rule) = outline(&paths[0]);
|
||||
|
|
@ -811,7 +814,7 @@ fn image_trace_object_remembers_its_settings() {
|
|||
image_doc(&mut s);
|
||||
let r = s.execute("imageTrace.make", &json!({"preset": "6 Colors"})).unwrap();
|
||||
let g = NodeId(r["id"].as_u64().unwrap());
|
||||
let t = node(&s, g).trace.clone().expect("settings stored");
|
||||
let t = node(&s, g).extras().trace.clone().expect("settings stored");
|
||||
assert_eq!(t["preset"], "6 Colors");
|
||||
assert_eq!(t["params"]["colors"], 6);
|
||||
// Changing a parameter makes it Custom; the settings survive save/open.
|
||||
|
|
@ -819,13 +822,13 @@ fn image_trace_object_remembers_its_settings() {
|
|||
let g = NodeId(r["id"].as_u64().unwrap());
|
||||
let d = s.doc().unwrap().doc.clone();
|
||||
let back = vectorcraft_format::load(&vectorcraft_format::save(&d, false)).unwrap();
|
||||
let t = back.node(g).unwrap().trace.clone().unwrap();
|
||||
let t = back.node(g).unwrap().extras().trace.clone().unwrap();
|
||||
assert_eq!((t["preset"].as_str(), t["params"]["colors"].as_u64()), (Some("Custom"), Some(9)));
|
||||
// Expanded traces are plain groups.
|
||||
s.execute("imageTrace.expand", &json!({})).unwrap();
|
||||
let st = s.doc().unwrap();
|
||||
let id = st.selection.in_paint_order(&st.doc)[0];
|
||||
assert!(st.doc.node(id).unwrap().trace.is_none());
|
||||
assert!(st.doc.node(id).unwrap().extras().trace.is_none());
|
||||
}
|
||||
|
||||
/// A black disc (r = 30 px) on light blue, 100 × 100 px, placed at 2× from (50, 50): traced in
|
||||
|
|
@ -852,7 +855,7 @@ fn render_doc(s: &Session, trace_views: bool) -> vectorcraft_render::Rendered {
|
|||
fn image_trace_view_is_stored_kept_and_validated() {
|
||||
let mut s = session();
|
||||
let g = disc_on_blue(&mut s);
|
||||
assert_eq!(node(&s, g).trace.unwrap()["view"], "tracingResult");
|
||||
assert_eq!(node(&s, g).extras().trace.clone().unwrap()["view"], "tracingResult");
|
||||
let r = s.execute("imageTrace.setView", &json!({"view": "outlinesWithSourceImage"})).unwrap();
|
||||
assert_eq!((ids(&r), r["view"].as_str()), (vec![g], Some("outlinesWithSourceImage")));
|
||||
assert_eq!(node(&s, g).trace_view(), vectorcraft_doc::TraceView::OutlinesWithSource);
|
||||
|
|
|
|||
|
|
@ -294,7 +294,7 @@ fn graphic_style_links_follow_the_style_into_the_document() {
|
|||
s.execute("paint.setFill", &json!({"ids": [a.0], "color": "#ffcc00"})).unwrap();
|
||||
sel(&mut s, &[a]);
|
||||
s.execute("graphicStyle.new", &json!({"name": "Sun"})).unwrap();
|
||||
let src_id = node(&s, a).graphic_style.unwrap();
|
||||
let src_id = node(&s, a).extras().graphic_style.unwrap();
|
||||
copy(&mut s, &[a]);
|
||||
assert_eq!(s.clipboard.graphic_styles.iter().map(|g| g.name.as_str()).collect::<Vec<_>>(), ["Sun"]);
|
||||
|
||||
|
|
@ -307,17 +307,17 @@ fn graphic_style_links_follow_the_style_into_the_document() {
|
|||
let v = s.execute("edit.paste", &json!({})).unwrap();
|
||||
assert_eq!(renamed(&v), [("graphicStyle".to_string(), "Sun".to_string(), "Sun 2".to_string())]);
|
||||
let d = doc(&s);
|
||||
let linked_to = d.graphic_style_by_id(node(&s, ids_of(&v)[0]).graphic_style.unwrap()).unwrap().clone();
|
||||
let linked_to = d.graphic_style_by_id(node(&s, ids_of(&v)[0]).extras().graphic_style.unwrap()).unwrap().clone();
|
||||
assert_eq!(linked_to.name, "Sun 2");
|
||||
assert_eq!(linked_to.appearance.fill_paint(), Paint::solid(hex("#ffcc00")));
|
||||
assert!(d.graphic_styles.iter().filter(|g| g.id == linked_to.id).count() == 1, "a fresh id");
|
||||
// Pasting again finds the same style.
|
||||
let v = s.execute("edit.paste", &json!({})).unwrap();
|
||||
assert_eq!((v["added"].as_u64(), node(&s, ids_of(&v)[0]).graphic_style), (Some(0), Some(linked_to.id)));
|
||||
assert_eq!((v["added"].as_u64(), node(&s, ids_of(&v)[0]).extras().graphic_style), (Some(0), Some(linked_to.id)));
|
||||
// Back in the source document the link is kept as it was.
|
||||
s.execute("document.activate", &json!({"index": 0})).unwrap();
|
||||
let v = s.execute("edit.paste", &json!({})).unwrap();
|
||||
assert_eq!(node(&s, ids_of(&v)[0]).graphic_style, Some(src_id));
|
||||
assert_eq!(node(&s, ids_of(&v)[0]).extras().graphic_style, Some(src_id));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -179,7 +179,7 @@ fn image_node(s: &mut Session) -> NodeId {
|
|||
link: None,
|
||||
placement: Default::default(),
|
||||
};
|
||||
d.insert(d.default_layer(), 0, Node::new(id, NodeKind::Image(im)))?;
|
||||
d.insert(d.default_layer(), 0, Node::new(id, NodeKind::Image(Box::new(im))))?;
|
||||
sel.set([id]);
|
||||
Ok(id)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ fn enveloped(s: &mut Session, cmd: &str, params: Value) -> (NodeId, NodeId) {
|
|||
|
||||
fn kind(s: &Session, id: NodeId) -> EnvelopeKind {
|
||||
match node(s, id).kind {
|
||||
NodeKind::Envelope { kind, .. } => kind,
|
||||
NodeKind::Envelope { kind, .. } => *kind,
|
||||
k => panic!("{k:?}"),
|
||||
}
|
||||
}
|
||||
|
|
@ -130,7 +130,7 @@ fn release_gives_back_the_warp_shape_as_a_mesh_and_keeps_the_transparency() {
|
|||
s.execute("transparency.set", &json!({"opacity": 40, "blend": "multiply"})).unwrap();
|
||||
let mask = Node::path(NodeId(0), vectorcraft_geom::shapes::rectangle(Rect::new(0.0, 0.0, 50.0, 50.0)), Default::default());
|
||||
s.edit("mask", |d, _| {
|
||||
d.node_mut(e).unwrap().mask = Some(Box::new(OpacityMask::new(mask, true)));
|
||||
d.node_mut(e).unwrap().extras_mut().mask = Some(Box::new(OpacityMask::new(mask, true)));
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
|
|
@ -139,7 +139,7 @@ fn release_gives_back_the_warp_shape_as_a_mesh_and_keeps_the_transparency() {
|
|||
assert_eq!(ids.len(), 2);
|
||||
assert_eq!(ids[0], a);
|
||||
let content = node(&s, a);
|
||||
assert_eq!((content.opacity, content.blend, content.mask.is_some()), (0.4, BlendMode::Multiply, true), "the content keeps the look");
|
||||
assert_eq!((content.opacity, content.blend, content.extras().mask.is_some()), (0.4, BlendMode::Multiply, true), "the content keeps the look");
|
||||
let NodeKind::Mesh(m) = node(&s, ids[1]).kind else { panic!("the warp's shape comes back as a mesh") };
|
||||
// The mesh follows the arc: its outline reaches below the content's bottom edge at the sides.
|
||||
let b = m.outline().bounds().unwrap();
|
||||
|
|
@ -172,14 +172,14 @@ fn expand_keeps_name_transparency_knockout_and_mask() {
|
|||
s.edit("name and mask", |d, _| {
|
||||
let n = d.node_mut(e).unwrap();
|
||||
n.name = Some("Banner".into());
|
||||
n.mask = Some(Box::new(OpacityMask::new(mask, true)));
|
||||
n.extras_mut().mask = Some(Box::new(OpacityMask::new(mask, true)));
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
s.execute("object.envelope.expand", &json!({})).unwrap();
|
||||
let g = node(&s, e);
|
||||
assert!(matches!(g.kind, NodeKind::Group { .. }));
|
||||
assert_eq!((g.name.as_deref(), g.opacity, g.isolate, g.knockout, g.mask.is_some()), (Some("Banner"), 0.7, true, Knockout::On, true));
|
||||
assert_eq!((g.name.as_deref(), g.opacity, g.isolate, g.knockout, g.extras().mask.is_some()), (Some("Banner"), 0.7, true, Knockout::On, true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ fn anchors(nodes: &[Node]) -> Vec<Point> {
|
|||
|
||||
fn frame(s: &Session, e: NodeId) -> Affine {
|
||||
match node(s, e).kind {
|
||||
NodeKind::Envelope { frame, .. } => frame,
|
||||
NodeKind::Envelope { frame, .. } => *frame,
|
||||
k => panic!("{k:?}"),
|
||||
}
|
||||
}
|
||||
|
|
@ -110,7 +110,7 @@ fn the_frame_round_trips_natively() {
|
|||
let mut old = serde_json::to_value(node(&s, e)).unwrap();
|
||||
old["kind"].as_object_mut().unwrap().remove("frame");
|
||||
let n: Node = serde_json::from_value(old).unwrap();
|
||||
assert!(matches!(n.kind, NodeKind::Envelope { frame, .. } if frame == Affine::IDENTITY), "older envelopes stand square to the page");
|
||||
assert!(matches!(n.kind, NodeKind::Envelope { frame, .. } if frame == Box::new(Affine::IDENTITY)), "older envelopes stand square to the page");
|
||||
}
|
||||
|
||||
/// A selected embedded 40×40 pt red image at (100, 100).
|
||||
|
|
@ -123,7 +123,7 @@ fn red_image(s: &mut Session) -> NodeId {
|
|||
let id = d.alloc_id();
|
||||
let xf = Affine::translate((100.0, 100.0)) * Affine::scale(10.0);
|
||||
let im = vectorcraft_doc::ImageObject { key: "pic".into(), width: 4, height: 4, xf, link: None, placement: Default::default() };
|
||||
d.insert(d.default_layer(), 0, Node::new(id, NodeKind::Image(im)))?;
|
||||
d.insert(d.default_layer(), 0, Node::new(id, NodeKind::Image(Box::new(im))))?;
|
||||
sel.set([id]);
|
||||
Ok(id)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -32,7 +32,10 @@ fn enveloped(s: &mut Session, cmd: &str, params: Value) -> (NodeId, NodeId) {
|
|||
|
||||
fn mesh(s: &Session, e: NodeId) -> (u32, u32, Vec<Point>, Vec<[Vec2; 4]>) {
|
||||
match node(s, e).kind {
|
||||
NodeKind::Envelope { kind: EnvelopeKind::Mesh { rows, cols, points, handles }, .. } => (rows, cols, points, handles),
|
||||
NodeKind::Envelope { kind, .. } => match *kind {
|
||||
EnvelopeKind::Mesh { rows, cols, points, handles } => (rows, cols, points, handles),
|
||||
k => panic!("{k:?}"),
|
||||
},
|
||||
k => panic!("{k:?}"),
|
||||
}
|
||||
}
|
||||
|
|
@ -120,7 +123,7 @@ fn mesh_handles_turn_with_the_envelope_and_round_trip() {
|
|||
let mut old = serde_json::to_value(node(&s, e)).unwrap();
|
||||
old["kind"]["kind"].as_object_mut().unwrap().remove("handles");
|
||||
let n: Node = serde_json::from_value(old).unwrap();
|
||||
assert!(matches!(n.kind, NodeKind::Envelope { kind: EnvelopeKind::Mesh { handles, .. }, .. } if handles.is_empty()));
|
||||
assert!(matches!(n.kind, NodeKind::Envelope { kind, .. } if matches!(&*kind, EnvelopeKind::Mesh { handles, .. } if handles.is_empty())));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -161,7 +161,7 @@ fn clicking_an_image_samples_its_pixel() {
|
|||
let id = d.alloc_id();
|
||||
let xf = vectorcraft_geom::Affine::translate((300.0, 0.0)) * vectorcraft_geom::Affine::scale(100.0);
|
||||
let im = ImageObject { key: "px".into(), width: 2, height: 1, xf, link: None, placement: Default::default() };
|
||||
d.insert(Some(l), 0, Node::new(id, NodeKind::Image(im))).map_err(|e| crate::EngineError::Other(e.to_string()))
|
||||
d.insert(Some(l), 0, Node::new(id, NodeKind::Image(Box::new(im)))).map_err(|e| crate::EngineError::Other(e.to_string()))
|
||||
})
|
||||
.unwrap();
|
||||
let dst = rect(&mut s, 10.0);
|
||||
|
|
|
|||
|
|
@ -451,13 +451,13 @@ fn opacity_masks_are_rasterized_with_their_look() {
|
|||
json!({"ids": [m], "gradient": {"kind": "linear", "stops": [{"offset": 0, "color": "#ffffff"}, {"offset": 1, "color": "#000000"}]}}),
|
||||
);
|
||||
run(&mut s, "transparency.makeOpacityMask", json!({"ids": [a, m]}));
|
||||
assert!(node(&s, a).mask.is_some());
|
||||
assert!(node(&s, a).extras().mask.is_some());
|
||||
let before = picture(&s);
|
||||
run(&mut s, "select.all", json!({}));
|
||||
let (ids, _, rasterized) = flatten(&mut s, json!({"lineArtPpi": 144}));
|
||||
assert_eq!((ids.len(), rasterized), (1, 1));
|
||||
let mut masks = 0;
|
||||
node(&s, ids[0]).walk(&mut |c| masks += c.mask.is_some() as usize);
|
||||
node(&s, ids[0]).walk(&mut |c| masks += c.extras().mask.is_some() as usize);
|
||||
assert_eq!(masks, 0, "no transparency is left");
|
||||
assert!(changed(&before, &picture(&s)) < 0.02);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -124,3 +124,32 @@ fn steps_share_an_unchanged_selection() {
|
|||
assert!(Arc::ptr_eq(&st.history.redo[0].selection, &st.history.redo[1].selection));
|
||||
assert_eq!(st.selection.objects.len(), 100);
|
||||
}
|
||||
|
||||
/// Undo and redo don't estimate the steps again: the step an undo (redo) makes takes the undone
|
||||
/// step's figures the other way round, which are what a fresh estimate gives.
|
||||
#[test]
|
||||
fn undo_and_redo_reuse_the_estimates() {
|
||||
let mut s = session_with_rects(200);
|
||||
s.execute("object.move", &json!({"dx": 5, "dy": 0})).unwrap();
|
||||
// A deletion is lopsided: the step before it keeps the objects, the one after keeps nothing.
|
||||
s.execute("edit.clear", &json!({})).unwrap();
|
||||
let fresh = |e: &HistoryEntry, now: &Document| e.doc.retained_bytes(now);
|
||||
let st = s.doc().unwrap();
|
||||
let delete = st.history.undo.last().unwrap();
|
||||
assert!(delete.bytes > 200 * 300, "the deleted objects: {}", delete.bytes);
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
let st = s.doc().unwrap();
|
||||
let redo = st.history.redo.last().unwrap();
|
||||
assert_eq!(redo.bytes, fresh(redo, &st.doc));
|
||||
assert!(redo.bytes < 4096, "the document without the objects keeps little of its own: {}", redo.bytes);
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
s.execute("edit.redo", &json!({})).unwrap();
|
||||
let st = s.doc().unwrap();
|
||||
let undo = st.history.undo.last().unwrap();
|
||||
assert_eq!(undo.label, "Move");
|
||||
assert_eq!(undo.bytes, fresh(undo, &st.doc));
|
||||
s.execute("edit.redo", &json!({})).unwrap();
|
||||
let st = s.doc().unwrap();
|
||||
let delete = st.history.undo.last().unwrap();
|
||||
assert_eq!(delete.bytes, fresh(delete, &st.doc));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ pub(crate) fn place(s: &mut Session, path: &str) -> NodeId {
|
|||
|
||||
pub(crate) fn image(s: &Session, id: NodeId) -> ImageObject {
|
||||
match &s.doc().unwrap().doc.node(id).unwrap().kind {
|
||||
NodeKind::Image(im) => im.clone(),
|
||||
NodeKind::Image(im) => (**im).clone(),
|
||||
k => panic!("not an image: {k:?}"),
|
||||
}
|
||||
}
|
||||
|
|
@ -323,7 +323,7 @@ fn relative_paths_are_written_against_the_saved_file() {
|
|||
link: Some(LinkInfo::new(format!("{root}{sep}art{sep}a.png"))),
|
||||
placement: Default::default(),
|
||||
};
|
||||
d.insert(Some(lid), 0, vectorcraft_doc::Node::new(nid, NodeKind::Image(im))).unwrap();
|
||||
d.insert(Some(lid), 0, vectorcraft_doc::Node::new(nid, NodeKind::Image(Box::new(im)))).unwrap();
|
||||
let rel = |dest: String| {
|
||||
let d = cmd::links::with_relative_paths(&d, &dest).unwrap_or_else(|| d.clone());
|
||||
match &d.node(nid).unwrap().kind {
|
||||
|
|
|
|||
|
|
@ -413,7 +413,8 @@ fn envelope_options_and_edit_contents() {
|
|||
let e = id_of(&s.execute("object.envelope.makeWithWarp", &json!({"style": "arc", "bend": 50})).unwrap());
|
||||
s.execute("object.envelope.options", &json!({"fidelity": 90, "bend": -30})).unwrap();
|
||||
match &node(&s, e).kind {
|
||||
NodeKind::Envelope { fidelity, kind: EnvelopeKind::Warp { bend, style, .. }, .. } => {
|
||||
NodeKind::Envelope { fidelity, kind, .. } => {
|
||||
let EnvelopeKind::Warp { bend, style, .. } = &**kind else { panic!("{kind:?}") };
|
||||
assert!(close(*fidelity, 90.0) && close(*bend, -30.0) && style == "arc");
|
||||
}
|
||||
k => panic!("{k:?}"),
|
||||
|
|
|
|||
|
|
@ -25,7 +25,8 @@ fn run(s: &mut Session, p: Value) {
|
|||
|
||||
fn corners(s: &Session, id: NodeId) -> ([f64; 4], [CornerKind; 4], usize) {
|
||||
let n = s.doc().unwrap().doc.node(id).unwrap();
|
||||
let NodeKind::Path { live: Some(LiveShape::Rectangle { radii, kinds, .. }), path, .. } = &n.kind else { panic!("not a live rectangle") };
|
||||
let NodeKind::Path { live: Some(l), path, .. } = &n.kind else { panic!("not a live shape") };
|
||||
let LiveShape::Rectangle { radii, kinds, .. } = &**l else { panic!("not a live rectangle") };
|
||||
(*radii, *kinds, path.anchor_count())
|
||||
}
|
||||
|
||||
|
|
@ -157,7 +158,7 @@ fn corners_of_a_non_square_rectangle_are_alike_circles() {
|
|||
let d = std::sync::Arc::make_mut(&mut s.doc_mut().unwrap().doc);
|
||||
let NodeKind::Path { path, live: Some(live), .. } = &mut d.node_mut(id).unwrap().kind else { panic!("not live") };
|
||||
let xf = Affine::translate((10.0, 10.0)) * Affine::scale_non_uniform(2.0, 1.0);
|
||||
*live = LiveShape::Rectangle { w: 50.0, h: 60.0, radii: [10.0; 4], kinds: Default::default(), xf };
|
||||
**live = LiveShape::Rectangle { w: 50.0, h: 60.0, radii: [10.0; 4], kinds: Default::default(), xf };
|
||||
*path = live.to_path();
|
||||
}
|
||||
assert_eq!(spans(&s, id)[0], (20.0, 10.0));
|
||||
|
|
@ -178,7 +179,7 @@ fn star(s: &mut Session) -> NodeId {
|
|||
|
||||
fn node_path(s: &Session, id: NodeId) -> (vectorcraft_geom::PathData, Option<LiveShape>) {
|
||||
let NodeKind::Path { path, live, .. } = &s.doc().unwrap().doc.node(id).unwrap().kind else { panic!("not a path") };
|
||||
(path.clone(), live.clone())
|
||||
(path.clone(), live.as_deref().cloned())
|
||||
}
|
||||
|
||||
/// The radius and kind every corner of `id` shares, and how many corners it has.
|
||||
|
|
@ -307,7 +308,10 @@ fn an_ellipse_takes_pie_angles_and_inverts() {
|
|||
s.execute("file.new", &json!({"width": 300, "height": 300})).unwrap();
|
||||
let id = NodeId(s.execute("shape.ellipse", &json!({"x": 0, "y": 0, "width": 200, "height": 100})).unwrap()["id"].as_u64().unwrap());
|
||||
let pie = |s: &Session| match &s.doc().unwrap().doc.node(id).unwrap().kind {
|
||||
NodeKind::Path { live: Some(LiveShape::Ellipse { pie, .. }), path, .. } => (*pie, path.bounds().unwrap()),
|
||||
NodeKind::Path { live: Some(l), path, .. } => match &**l {
|
||||
LiveShape::Ellipse { pie, .. } => (*pie, path.bounds().unwrap()),
|
||||
k => panic!("{k:?}"),
|
||||
},
|
||||
k => panic!("{k:?}"),
|
||||
};
|
||||
s.execute("object.setLiveShape", &json!({"pieStart": 0, "pieEnd": 90})).unwrap();
|
||||
|
|
@ -335,7 +339,7 @@ fn a_polygon_takes_its_angle_radius_side_length_and_equal_sides() {
|
|||
s.execute("file.new", &json!({"width": 400, "height": 400})).unwrap();
|
||||
let id = NodeId(s.execute("shape.polygon", &json!({"cx": 200, "cy": 200, "radius": 50, "sides": 6})).unwrap()["id"].as_u64().unwrap());
|
||||
let live = |s: &Session| match &s.doc().unwrap().doc.node(id).unwrap().kind {
|
||||
NodeKind::Path { live: Some(l @ LiveShape::Polygon { .. }), path, .. } => (l.clone(), path.bounds().unwrap()),
|
||||
NodeKind::Path { live: Some(l), path, .. } if matches!(**l, LiveShape::Polygon { .. }) => ((**l).clone(), path.bounds().unwrap()),
|
||||
k => panic!("{k:?}"),
|
||||
};
|
||||
let close = |a: f64, b: f64| (a - b).abs() < 1e-6;
|
||||
|
|
|
|||
|
|
@ -426,7 +426,8 @@ fn convert_rectangle_path_to_live_shape() {
|
|||
);
|
||||
sel(&mut s, &[p]);
|
||||
assert_eq!(s.execute("object.shape.convertToShape", &json!({})).unwrap()["converted"], 1);
|
||||
let NodeKind::Path { live: Some(LiveShape::Rectangle { w, h, .. }), path, .. } = &node(&s, p).kind else { panic!() };
|
||||
let n = node(&s, p);
|
||||
let (Some(LiveShape::Rectangle { w, h, .. }), NodeKind::Path { path, .. }) = (n.live_shape(), &n.kind) else { panic!() };
|
||||
assert!(close(*w, 40.0) && close(*h, 20.0));
|
||||
assert_eq!(path.bounds(), Some(Rect::new(0.0, 0.0, 40.0, 20.0)));
|
||||
}
|
||||
|
|
@ -448,7 +449,7 @@ fn convert_rotated_rectangle_and_ellipse() {
|
|||
assert_eq!(s.execute("object.shape.convertToShape", &json!({})).unwrap()["converted"], 2);
|
||||
let b = bounds(&s, p);
|
||||
assert!(close(b.x0, 0.0) && close(b.x1, 100.0) && close(b.y1, 100.0), "{b:?}");
|
||||
assert!(matches!(node(&s, e).kind, NodeKind::Path { live: Some(LiveShape::Ellipse { .. }), .. }));
|
||||
assert!(matches!(node(&s, e).live_shape(), Some(LiveShape::Ellipse { .. })));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ fn doc(s: &Session) -> &Document {
|
|||
}
|
||||
|
||||
fn mask(s: &Session, id: NodeId) -> Option<&vectorcraft_doc::OpacityMask> {
|
||||
doc(s).node(id).and_then(|n| n.mask.as_deref())
|
||||
doc(s).node(id).and_then(|n| n.extras().mask.as_deref())
|
||||
}
|
||||
|
||||
fn has_edit_layer(d: &Document) -> bool {
|
||||
|
|
@ -272,7 +272,7 @@ fn saving_while_editing_writes_no_editing_layer() {
|
|||
assert!(back.mask_edit.is_none() && !has_edit_layer(&back));
|
||||
assert_eq!(back.layers.len(), 1);
|
||||
// The edit made while editing is in the saved mask.
|
||||
assert_eq!(back.node(obj).unwrap().mask.as_ref().unwrap().art.geometric_bounds().unwrap().x0, 50.0);
|
||||
assert_eq!(back.node(obj).unwrap().extras().mask.as_ref().unwrap().art.geometric_bounds().unwrap().x0, 50.0);
|
||||
// Still editing in the session.
|
||||
assert!(doc(&s).mask_edit.is_some() && has_edit_layer(doc(&s)));
|
||||
// A file saved mid-edit by an older version (editing record and layer written) opens clean.
|
||||
|
|
|
|||
|
|
@ -103,7 +103,7 @@ fn groups_layers_type_images_and_symbols_keep_their_raster_effects() {
|
|||
let xf = Affine::translate((40.0, 50.0)) * Affine::scale(2.5);
|
||||
let im = Node::new(
|
||||
d.alloc_id(),
|
||||
NodeKind::Image(ImageObject { key: "pic".into(), width: 40, height: 30, xf, link: None, placement: Default::default() }),
|
||||
NodeKind::Image(Box::new(ImageObject { key: "pic".into(), width: 40, height: 30, xf, link: None, placement: Default::default() })),
|
||||
);
|
||||
add(&mut d, with(im, shadow()));
|
||||
assert_written("image", &d);
|
||||
|
|
|
|||
|
|
@ -117,7 +117,7 @@ fn edit_text_shows_it_flat_in_isolation_then_projects_it_again() {
|
|||
assert!(n.projection().is_some(), "projected again");
|
||||
// Nothing left flat anywhere in the history.
|
||||
for e in &s.doc().unwrap().history.undo {
|
||||
if let Some(p) = e.doc.node(id).and_then(|n| n.perspective.as_deref()) {
|
||||
if let Some(p) = e.doc.node(id).and_then(|n| n.extras().perspective.as_deref()) {
|
||||
assert!(!p.editing);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -458,3 +458,119 @@ fn without_a_store_the_commands_say_where_to_set_one() {
|
|||
assert!(!area.join("Untitled-1-1.vectorcraft").exists());
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
/// A store in memory that notes the entries written to it.
|
||||
#[derive(Default)]
|
||||
struct Counting {
|
||||
inner: MemoryStore,
|
||||
written: std::sync::Mutex<Vec<(String, usize)>>,
|
||||
}
|
||||
|
||||
impl Counting {
|
||||
/// The image entries written since the last call, with their sizes.
|
||||
fn images_written(&self) -> Vec<(String, usize)> {
|
||||
std::mem::take(&mut *self.written.lock().unwrap()).into_iter().filter(|(n, _)| n.ends_with(".blob")).collect()
|
||||
}
|
||||
|
||||
fn images(&self) -> Vec<String> {
|
||||
self.inner.list().unwrap().into_iter().filter(|n| n.ends_with(".blob")).collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl RecoveryStore for Counting {
|
||||
fn list(&self) -> std::result::Result<Vec<String>, String> {
|
||||
self.inner.list()
|
||||
}
|
||||
fn read(&self, name: &str) -> std::result::Result<Vec<u8>, String> {
|
||||
self.inner.read(name)
|
||||
}
|
||||
fn write(&self, name: &str, bytes: &[u8]) -> std::result::Result<(), String> {
|
||||
self.written.lock().unwrap().push((name.to_string(), bytes.len()));
|
||||
self.inner.write(name, bytes)
|
||||
}
|
||||
fn remove(&self, name: &str) -> std::result::Result<(), String> {
|
||||
self.inner.remove(name)
|
||||
}
|
||||
fn location(&self) -> String {
|
||||
self.inner.location()
|
||||
}
|
||||
fn lock(&self, area: &str) -> std::result::Result<crate::cmd::recovery::Lock, String> {
|
||||
self.inner.lock(area)
|
||||
}
|
||||
fn now(&self) -> Option<i64> {
|
||||
self.inner.now()
|
||||
}
|
||||
}
|
||||
|
||||
fn place_png(s: &mut Session, name: &str, rgb: [u8; 3]) -> Vec<u8> {
|
||||
let png = crate::tests_links::png(64, 48, rgb);
|
||||
s.execute("file.place", &json!({"name": name, "dataBase64": vectorcraft_format::base64_encode(&png), "link": false})).unwrap();
|
||||
png
|
||||
}
|
||||
|
||||
/// The document's images: their bytes by key.
|
||||
fn images_of(s: &Session) -> std::collections::BTreeMap<String, Vec<u8>> {
|
||||
s.doc().unwrap().doc.images.iter().map(|(k, b)| (k.clone(), b.bytes.to_vec())).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn images_are_kept_apart_and_written_again_only_when_they_change() {
|
||||
let store = Arc::new(Counting::default());
|
||||
let mut s = Session::new();
|
||||
s.recovery.set_store(store.clone());
|
||||
s.execute("file.new", &json!({"width": 200, "height": 100})).unwrap();
|
||||
let red = place_png(&mut s, "red.png", crate::tests_links::RED);
|
||||
let r = s.execute("file.recovery.save", &json!({})).unwrap();
|
||||
assert_eq!(r["saved"][0]["imageBytes"], red.len(), "{r}");
|
||||
assert_eq!(store.images_written().len(), 1);
|
||||
let area = s.recovery.own_area().unwrap().to_string();
|
||||
// The document entry doesn't hold the image.
|
||||
let doc = store.read(&format!("{area}/Untitled-1-1.vectorcraft")).unwrap();
|
||||
assert!(vectorcraft_format::load(&doc).unwrap().images.is_empty());
|
||||
// An edit that leaves the image alone: the copy is written again, its image isn't.
|
||||
s.execute("shape.rectangle", &json!({"x": 10, "y": 10, "width": 50, "height": 20})).unwrap();
|
||||
let r = s.execute("file.recovery.save", &json!({})).unwrap();
|
||||
assert_eq!((r["saved"].as_array().unwrap().len(), r["saved"][0]["imageBytes"].as_u64()), (1, Some(0)), "{r}");
|
||||
assert!(store.images_written().is_empty(), "the image isn't written again");
|
||||
// A new image: only it is written.
|
||||
let blue = place_png(&mut s, "blue.png", crate::tests_links::BLUE);
|
||||
let r = s.execute("file.recovery.save", &json!({})).unwrap();
|
||||
assert_eq!(r["saved"][0]["imageBytes"], blue.len());
|
||||
assert_eq!(store.images_written().iter().map(|(_, n)| *n).collect::<Vec<_>>(), [blue.len()]);
|
||||
assert_eq!(store.images().len(), 2);
|
||||
let images = images_of(&s);
|
||||
assert_eq!(images.len(), 2);
|
||||
|
||||
// A crash: the copy restores with its images.
|
||||
drop(s);
|
||||
let mut s = Session::new();
|
||||
s.recovery.set_store(store.clone());
|
||||
assert_eq!(restored(s.execute("file.recovery.restore", &json!({})).unwrap()), 1);
|
||||
assert_eq!(images_of(&s), images);
|
||||
let mine = s.recovery.own_area().unwrap().to_string();
|
||||
assert!(store.images().iter().all(|n| n.starts_with(&format!("{mine}/"))), "the images moved with the copy, the old area went");
|
||||
assert_eq!(store.images().len(), 2);
|
||||
// Its copy and another document's: once the first is closed, the next copy sweeps its images.
|
||||
s.execute("file.new", &json!({"width": 50, "height": 50})).unwrap();
|
||||
s.execute("shape.rectangle", &json!({"x": 1, "y": 1, "width": 5, "height": 5})).unwrap();
|
||||
s.execute("file.close", &json!({"index": 0})).unwrap();
|
||||
s.execute("file.recovery.save", &json!({})).unwrap();
|
||||
assert!(store.images().is_empty(), "{:?}", store.list());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn copies_from_before_images_were_kept_apart_still_restore() {
|
||||
let store = Arc::new(MemoryStore::default());
|
||||
// The document entry is a whole native file, images inside; the details say nothing of them.
|
||||
let mut s = session_in(&store);
|
||||
place_png(&mut s, "red.png", crate::tests_links::RED);
|
||||
let d = vectorcraft_doc::Document::clone(&s.doc().unwrap().doc);
|
||||
let images = images_of(&s);
|
||||
drop(s);
|
||||
store.write("1-1/Old-1.vectorcraft", &vectorcraft_format::save_file(&d)).unwrap();
|
||||
store.write("1-1/Old-1.json", br#"{"title": "Old", "format": "vectorcraft", "saved": 5}"#).unwrap();
|
||||
let mut s = relaunch(&store);
|
||||
assert_eq!(restored(s.execute("file.recovery.restore", &json!({})).unwrap()), 1);
|
||||
assert_eq!(images_of(&s), images);
|
||||
assert_eq!(s.doc().unwrap().title(), "Untitled-1 [Recovered]");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -138,8 +138,8 @@ fn group_appearance_scales_with_its_members() {
|
|||
|
||||
#[test]
|
||||
fn scale_corners_keeps_or_scales_live_corner_radii() {
|
||||
let radius = |s: &Session, id| match &node(s, id).kind {
|
||||
NodeKind::Path { live: Some(LiveShape::Rectangle { radii, xf, .. }), .. } => radii[0] * xf.as_coeffs()[0],
|
||||
let radius = |s: &Session, id| match node(s, id).live_shape() {
|
||||
Some(LiveShape::Rectangle { radii, xf, .. }) => radii[0] * xf.as_coeffs()[0],
|
||||
_ => panic!("not a live rectangle"),
|
||||
};
|
||||
let mut s = session();
|
||||
|
|
@ -175,7 +175,7 @@ fn live_corners_stay_circular_when_a_rectangle_is_scaled_unevenly() {
|
|||
let n = node(&s, id);
|
||||
assert!((n.geometric_bounds().unwrap().width() - 300.0).abs() < 1e-9);
|
||||
assert!(round_corners(&n, 10.0), "{:?}", corner_spans(&n));
|
||||
let NodeKind::Path { live: Some(LiveShape::Rectangle { w, h, radii, .. }), .. } = &n.kind else { panic!("not live") };
|
||||
let Some(LiveShape::Rectangle { w, h, radii, .. }) = n.live_shape() else { panic!("not live") };
|
||||
assert!((w - 300.0).abs() < 1e-9 && (h - 100.0).abs() < 1e-9 && (radii[0] - 10.0).abs() < 1e-9, "the panels read document units");
|
||||
// Scale Corners on: the radius scales by the mean scale (√(2 × 0.5) = 1 here, 2 below).
|
||||
run(&mut s, "object.scale", json!({"sx": 200, "sy": 50, "corners": true}));
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
use super::*;
|
||||
use serde_json::json;
|
||||
use vectorcraft_doc::shaper::{FILL_PREFIX, SOURCES, STROKE_PREFIX};
|
||||
use vectorcraft_doc::{LiveShape, Node, NodeKind};
|
||||
use vectorcraft_doc::{LiveShape, Node};
|
||||
use vectorcraft_geom::{Point, Shape as _};
|
||||
use vectorcraft_tools::{Mods, PointerEvent, PointerKind};
|
||||
|
||||
|
|
@ -134,7 +134,7 @@ fn shaper_erases_only_protruding_stroke_pieces_keeps_fill_and_original_line() {
|
|||
assert!(stroke(&s, g, 225.0, 150.0));
|
||||
assert_eq!(fill(&s, g, 150.0, 130.0).as_deref(), Some("#ffff00"));
|
||||
assert_eq!(node(&s, line), &original_line);
|
||||
assert!(matches!(node(&s, circle).kind, NodeKind::Path { live: Some(LiveShape::Ellipse { .. }), .. }));
|
||||
assert!(matches!(node(&s, circle).live_shape(), Some(LiveShape::Ellipse { .. })));
|
||||
let g = scribble(&mut s, &[[210.0, 140.0], [220.0, 160.0], [230.0, 140.0], [240.0, 160.0]]);
|
||||
assert!(!stroke(&s, g, 225.0, 150.0));
|
||||
assert!(stroke(&s, g, 150.0, 150.0));
|
||||
|
|
@ -152,7 +152,7 @@ fn shaper_source_move_resize_native_roundtrip_and_release_preserve_live_art() {
|
|||
assert_eq!(node(&s, a).geometric_bounds().unwrap().x0, 20.0);
|
||||
s.execute("select.set", &json!({"ids":[a.0]})).unwrap();
|
||||
s.execute("object.setBounds", &json!({"width":120.0,"height":110.0,"reference":0})).unwrap();
|
||||
assert!(matches!(node(&s, a).kind, NodeKind::Path { live: Some(LiveShape::Rectangle { .. }), .. }));
|
||||
assert!(matches!(node(&s, a).live_shape(), Some(LiveShape::Rectangle { .. })));
|
||||
let doc = &s.doc().unwrap().doc;
|
||||
let back = vectorcraft_format::load(&vectorcraft_format::save(doc, false)).unwrap();
|
||||
assert_eq!(back.layers, doc.layers);
|
||||
|
|
@ -175,7 +175,7 @@ fn shaper_face_paint_survives_source_edits_and_expand_discards_only_sources() {
|
|||
s.execute("object.move", &json!({"ids":[a.0],"dx":5,"dy":0})).unwrap();
|
||||
assert_eq!(fill(&s, g, 75.0, 50.0).as_deref(), Some("#00ff00"));
|
||||
s.execute("shaper.expand", &json!({"id":g.0})).unwrap();
|
||||
assert!(node(&s, g).shaper.is_none());
|
||||
assert!(node(&s, g).extras().shaper.is_none());
|
||||
assert!(s.doc().unwrap().doc.node(a).is_none());
|
||||
assert_eq!(fill(&s, g, 75.0, 50.0).as_deref(), Some("#00ff00"));
|
||||
}
|
||||
|
|
@ -211,7 +211,7 @@ fn shaper_lines_keep_free_angles_with_or_without_shift() {
|
|||
s.pointer(&PointerEvent::new(kind, x, y).with_mods(Mods { shift, ..Default::default() }), v).unwrap();
|
||||
}
|
||||
let n = node(&s, s.doc().unwrap().selection.objects[0]);
|
||||
assert!(matches!(n.kind,NodeKind::Path{live:Some(LiveShape::Line{a,b}),..} if a==Point::new(30.0,40.0) && b==Point::new(230.0,100.0)));
|
||||
assert!(matches!(n.live_shape(), Some(LiveShape::Line { a, b }) if *a == Point::new(30.0, 40.0) && *b == Point::new(230.0, 100.0)));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -258,7 +258,7 @@ fn shaper_tool_trims_both_diagonal_ends_and_only_construction_can_pick_them() {
|
|||
}
|
||||
}
|
||||
let g = s.doc().unwrap().selection.objects[0];
|
||||
assert!(node(&s, g).shaper.is_some());
|
||||
assert!(node(&s, g).extras().shaper.is_some());
|
||||
assert_eq!(node(&s, line), &original);
|
||||
assert!(stroke(&s, g, 150.0, 150.0));
|
||||
assert_eq!(fill(&s, g, 150.0, 130.0).as_deref(), Some("#ffff00"));
|
||||
|
|
|
|||
|
|
@ -64,10 +64,10 @@ fn an_object_slice_follows_its_object_and_releases_back_to_it() {
|
|||
// Make again changes nothing; Release leaves the object.
|
||||
assert_eq!(run(&mut s, "object.slice.make", json!({}))["ids"], json!([]));
|
||||
assert_eq!(run(&mut s, "object.slice.release", json!({}))["ids"], json!([a]));
|
||||
assert!(s.doc().unwrap().doc.node(NodeId(a)).unwrap().slice.is_none());
|
||||
assert!(s.doc().unwrap().doc.node(NodeId(a)).unwrap().extras().slice.is_none());
|
||||
assert!(slices(&mut s).is_empty());
|
||||
run(&mut s, "edit.undo", json!({}));
|
||||
assert!(s.doc().unwrap().doc.node(NodeId(a)).unwrap().slice.is_some());
|
||||
assert!(s.doc().unwrap().doc.node(NodeId(a)).unwrap().extras().slice.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -155,7 +155,7 @@ fn combine_and_duplicate() {
|
|||
let c = run(&mut s, "object.slice.combine", json!({}))["id"].as_u64().unwrap();
|
||||
assert_eq!(rect_of(&mut s, c), [0.0, 0.0, 60.0, 50.0]);
|
||||
assert_eq!(user_count(&s), 1);
|
||||
assert!(s.doc().unwrap().doc.node(NodeId(b)).unwrap().slice.is_none(), "the object slice was released");
|
||||
assert!(s.doc().unwrap().doc.node(NodeId(b)).unwrap().extras().slice.is_none(), "the object slice was released");
|
||||
assert_eq!(s.doc().unwrap().selection.slices, vec![NodeId(c)]);
|
||||
let dup = run(&mut s, "object.slice.duplicate", json!({}))["ids"][0].as_u64().unwrap();
|
||||
assert_eq!(rect_of(&mut s, dup), [10.0, 10.0, 60.0, 50.0]);
|
||||
|
|
@ -191,7 +191,7 @@ fn slice_options_set_read_and_undo_in_one_step() {
|
|||
run(&mut s, "select.set", json!({"ids": [t]}));
|
||||
let o = run(&mut s, "object.slice.options", json!({"kind": "htmlText"}));
|
||||
assert_eq!((o["kind"].clone(), o["htmlText"].clone()), (json!("htmlText"), json!(true)));
|
||||
assert_eq!(s.doc().unwrap().doc.node(NodeId(t)).unwrap().slice.as_ref().unwrap().kind, SliceKind::HtmlText);
|
||||
assert_eq!(s.doc().unwrap().doc.node(NodeId(t)).unwrap().extras().slice.as_ref().unwrap().kind, SliceKind::HtmlText);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -225,7 +225,7 @@ fn clip_to_artboard_hide_lock_select_and_delete_all() {
|
|||
|
||||
assert_eq!(run(&mut s, "object.slice.deleteAll", json!({}))["count"], 2);
|
||||
assert_eq!(user_count(&s), 0);
|
||||
assert!(s.doc().unwrap().doc.node(NodeId(a)).unwrap().slice.is_none());
|
||||
assert!(s.doc().unwrap().doc.node(NodeId(a)).unwrap().extras().slice.is_none());
|
||||
assert!(s.execute("object.slice.deleteAll", &json!({})).is_err(), "nothing to delete");
|
||||
}
|
||||
|
||||
|
|
@ -243,7 +243,7 @@ fn slices_survive_a_native_round_trip() {
|
|||
vectorcraft_testkit::invariants::check_native_roundtrip_exact(&doc).unwrap();
|
||||
let back = vectorcraft_format::load(&vectorcraft_format::save(&doc, false)).unwrap();
|
||||
assert_eq!(back.slices, doc.slices);
|
||||
assert_eq!(back.node(NodeId(a)).unwrap().slice.as_ref().unwrap().alt, "logo");
|
||||
assert_eq!(back.node(NodeId(a)).unwrap().extras().slice.as_ref().unwrap().alt, "logo");
|
||||
assert!(!back.slices_clip_to_artboard);
|
||||
// A copy of an object slice is an object slice too, under its own id.
|
||||
run(&mut s, "select.set", json!({"ids": [a]}));
|
||||
|
|
|
|||
|
|
@ -107,7 +107,7 @@ fn add_with_apply_styles_the_selection_in_the_same_undo_step() {
|
|||
assert!((geom.start - Point::new(10.0 + 0.35 * 100.0, 20.0 + 0.3 * 50.0)).hypot() < 1e-6, "{geom:?}");
|
||||
run(&mut s, "edit.undo", json!({}));
|
||||
assert!(doc(&s).graphic_style("Spotlight").is_none());
|
||||
assert_eq!(doc(&s).node(id).unwrap().graphic_style, None);
|
||||
assert_eq!(doc(&s).node(id).unwrap().extras().graphic_style, None);
|
||||
// Without a selection it only adds; `ids` targets objects; an existing style is applied as is.
|
||||
run(&mut s, "select.none", json!({}));
|
||||
let r = run(&mut s, "graphicStyle.addFromLibrary", json!({"library": "blends-transparency", "name": "Ghosted", "apply": true}));
|
||||
|
|
@ -224,7 +224,7 @@ fn every_builtin_style_paints_visibly_on_a_rectangle() {
|
|||
run(&mut s, "file.new", json!({"width": 100, "height": 100}));
|
||||
let id = rect(&mut s, 25.0, 30.0, 50.0, 40.0);
|
||||
run(&mut s, "graphicStyle.addFromLibrary", json!({"library": b.id, "name": g.name, "apply": true}));
|
||||
assert_eq!(doc(&s).node(id).unwrap().graphic_style, doc(&s).graphic_style(&g.name).map(|x| x.id), "{}", g.name);
|
||||
assert_eq!(doc(&s).node(id).unwrap().extras().graphic_style, doc(&s).graphic_style(&g.name).map(|x| x.id), "{}", g.name);
|
||||
let img = renderer.render_region(doc(&s), Rect::new(0.0, 0.0, 100.0, 100.0), 0.5, true);
|
||||
let painted = img.pixels.as_chunks::<4>().0.iter().filter(|p| p[..3] != [255, 255, 255]).count();
|
||||
assert!(painted > 40, "{} / {} paints {painted} pixels", b.id, g.name);
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@ fn styles_carry_opacity_and_blend_through_a_save() {
|
|||
let doc = s.doc().unwrap().doc.clone();
|
||||
let back = vectorcraft_format::load(&vectorcraft_format::save(&doc, false)).unwrap();
|
||||
assert_eq!(back.graphic_styles, doc.graphic_styles);
|
||||
assert_eq!(back.node(b).unwrap().graphic_style, Some(g.id));
|
||||
assert_eq!(back.node(b).unwrap().extras().graphic_style, Some(g.id));
|
||||
// Styles and objects from files without links still load (and get an id when first linked).
|
||||
let old: GraphicStyle = serde_json::from_value(json!({"name": "Old", "appearance": {"items": []}})).unwrap();
|
||||
assert_eq!((old.id, old.opacity, old.blend, old.isolate), (0, 1.0, BlendMode::Normal, false));
|
||||
|
|
@ -106,7 +106,7 @@ fn a_style_from_a_group_or_type_is_not_empty() {
|
|||
run(&mut s, "graphicStyle.apply", json!({"name": "Black Outline", "ids": [g.0]}));
|
||||
assert_eq!(node(&s, g).appearance, style(&s, "Black Outline").appearance);
|
||||
assert_eq!(node(&s, a).appearance, before);
|
||||
assert_eq!(node(&s, g).graphic_style, Some(style(&s, "Black Outline").id));
|
||||
assert_eq!(node(&s, g).extras().graphic_style, Some(style(&s, "Black Outline").id));
|
||||
assert_eq!(linked(&mut s, "Black Outline"), vec![g.0]);
|
||||
assert_ne!(node(&s, c).appearance, style(&s, "Black Outline").appearance);
|
||||
run(&mut s, "graphicStyle.apply", json!({"name": "Black Outline", "ids": [g.0], "target": "contents"}));
|
||||
|
|
@ -130,7 +130,7 @@ fn alt_apply_adds_the_style_on_top() {
|
|||
assert_eq!(after.items[..before.items.len()], before.items[..]);
|
||||
assert_eq!(after.items.last(), rough.appearance.items.last());
|
||||
assert_eq!(after.effects.len(), before.effects.len() + 1);
|
||||
assert_eq!(node(&s, b).graphic_style, None);
|
||||
assert_eq!(node(&s, b).extras().graphic_style, None);
|
||||
// One undo step.
|
||||
run(&mut s, "edit.undo", json!({}));
|
||||
assert_eq!(node(&s, b).appearance, before);
|
||||
|
|
@ -184,7 +184,7 @@ fn redefine_leaves_unlinked_and_edited_objects_alone() {
|
|||
let mut s = session();
|
||||
let (a, b, c) = linked_three(&mut s);
|
||||
run(&mut s, "graphicStyle.breakLink", json!({"ids": [c.0]}));
|
||||
assert_eq!(node(&s, c).graphic_style, None);
|
||||
assert_eq!(node(&s, c).extras().graphic_style, None);
|
||||
let d = rect(&mut s, 300.0);
|
||||
run(&mut s, "graphicStyle.apply", json!({"name": "S"}));
|
||||
set_fill(&mut s, d, "#00ff00");
|
||||
|
|
@ -194,7 +194,7 @@ fn redefine_leaves_unlinked_and_edited_objects_alone() {
|
|||
assert_eq!(fill(&s, c), Paint::solid(Color::WHITE));
|
||||
// The edited object keeps its look and is unlinked.
|
||||
assert_eq!(fill(&s, d), Paint::solid(Color::from_hex("#00ff00").unwrap()));
|
||||
assert_eq!(node(&s, d).graphic_style, None);
|
||||
assert_eq!(node(&s, d).extras().graphic_style, None);
|
||||
assert!(s.execute("graphicStyle.redefine", &json!({"id": c.0})).is_err());
|
||||
}
|
||||
|
||||
|
|
@ -208,7 +208,7 @@ fn deleting_a_style_unlinks_and_renaming_keeps_links() {
|
|||
assert_eq!(run(&mut s, "select.same.graphicStyle", json!({}))["count"], 3);
|
||||
let look = node(&s, c).appearance;
|
||||
run(&mut s, "graphicStyle.delete", json!({"name": "T"}));
|
||||
assert_eq!((node(&s, b).graphic_style, node(&s, c).graphic_style), (None, None));
|
||||
assert_eq!((node(&s, b).extras().graphic_style, node(&s, c).extras().graphic_style), (None, None));
|
||||
assert_eq!(node(&s, c).appearance, look);
|
||||
run(&mut s, "select.set", json!({"ids": [b.0]}));
|
||||
assert!(s.execute("select.same.graphicStyle", &json!({})).is_err());
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ fn a_targeted_layer_gets_an_opacity_mask() {
|
|||
run(&mut s, "layer.target", json!({"id": layer.0}));
|
||||
assert_eq!(run(&mut s, "transparency.makeOpacityMask", json!({}))["id"], json!(layer.0));
|
||||
run(&mut s, "transparency.stopEditingOpacityMask", json!({}));
|
||||
assert!(node(&s, layer).mask.is_some());
|
||||
assert!(node(&s, layer).extras().mask.is_some());
|
||||
assert_eq!(s.doc().unwrap().selection.target, Some(layer), "the masked layer stays targeted");
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1054,7 +1054,7 @@ impl<'a> Reader<'a> {
|
|||
true
|
||||
});
|
||||
}
|
||||
if let Some(m) = &mut n.mask {
|
||||
if let Some(m) = n.extras_if_any().and_then(|e| e.mask.as_mut()) {
|
||||
self.reid(Arc::make_mut(&mut m.art));
|
||||
}
|
||||
if let Some(children) = n.children_mut() {
|
||||
|
|
@ -1437,7 +1437,7 @@ impl<'a> Reader<'a> {
|
|||
self.doc.images.entry(key.clone()).or_insert(blob);
|
||||
let gs = self.gs.clone();
|
||||
let im = ImageObject { key, width: w as u32, height: h as u32, xf, link: None, placement: Default::default() };
|
||||
let mut node = self.new_node(NodeKind::Image(im), &gs);
|
||||
let mut node = self.new_node(NodeKind::Image(Box::new(im)), &gs);
|
||||
node.opacity = gs.opacity;
|
||||
node.blend = gs.blend;
|
||||
self.add(node, false)
|
||||
|
|
|
|||
|
|
@ -973,7 +973,7 @@ impl Interp<'_> {
|
|||
self.out.doc.images.entry(key.clone()).or_insert(blob);
|
||||
let im = ImageObject { key, width: img.w, height: img.h, xf, link: None, placement: Default::default() };
|
||||
let clips = self.g.clips.clone();
|
||||
self.out.push(Node::new(NodeId(0), NodeKind::Image(im)), &clips);
|
||||
self.out.push(Node::new(NodeId(0), NodeKind::Image(Box::new(im))), &clips);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,7 +48,8 @@ fn image_doc(img: image::RgbaImage, frame: Rect) -> Option<Document> {
|
|||
let key = blob.content_key();
|
||||
doc.images.insert(key.clone(), blob);
|
||||
let xf = Affine::scale_non_uniform(frame.width() / f64::from(w), frame.height() / f64::from(h));
|
||||
let mut n = Node::new(doc.alloc_id(), NodeKind::Image(ImageObject { key, width: w, height: h, xf, link: None, placement: Default::default() }));
|
||||
let mut n =
|
||||
Node::new(doc.alloc_id(), NodeKind::Image(Box::new(ImageObject { key, width: w, height: h, xf, link: None, placement: Default::default() })));
|
||||
n.name = Some("Preview".into());
|
||||
let layer = doc.layers.first().map(|l| l.id)?;
|
||||
doc.insert(Some(layer), 0, n).ok()?;
|
||||
|
|
|
|||
|
|
@ -472,7 +472,7 @@ fn rich_doc() -> Document {
|
|||
link: None,
|
||||
placement: Default::default(),
|
||||
};
|
||||
let n = Node::new(d.alloc_id(), NodeKind::Image(im));
|
||||
let n = Node::new(d.alloc_id(), NodeKind::Image(Box::new(im)));
|
||||
add(&mut d, n);
|
||||
d.swatches.push(Swatch { name: "Spot".into(), paint: Paint::solid(Color::cmyk(0.0, 0.5, 1.0, 0.0)), global: true, spot: true });
|
||||
let spot = Paint::Solid { color: Color::cmyk(0.0, 0.5, 1.0, 0.0), swatch: Some("Spot".into()), tint: 1.0 };
|
||||
|
|
|
|||
|
|
@ -322,7 +322,7 @@ impl<'a> Scene<'a> {
|
|||
|
||||
/// Warn about what EPS can't hold in `n`'s look.
|
||||
fn note_losses(&mut self, n: &Node) {
|
||||
if n.opacity < 1.0 || n.blend != BlendMode::Normal || n.mask.as_ref().is_some_and(|m| !m.disabled) {
|
||||
if n.opacity < 1.0 || n.blend != BlendMode::Normal || n.extras().mask.as_ref().is_some_and(|m| !m.disabled) {
|
||||
self.warn(TRANSPARENCY);
|
||||
}
|
||||
let raster = |fx: &[Effect]| fx.iter().any(|e| e.visible && vectorcraft_effects::is_raster(&e.id));
|
||||
|
|
|
|||
|
|
@ -269,7 +269,7 @@ fn png_doc() -> Document {
|
|||
link: None,
|
||||
placement: Default::default(),
|
||||
};
|
||||
let n = Node::new(d.alloc_id(), NodeKind::Image(im));
|
||||
let n = Node::new(d.alloc_id(), NodeKind::Image(Box::new(im)));
|
||||
let layer = d.layers[0].id;
|
||||
d.insert(Some(layer), 0, n).unwrap();
|
||||
d
|
||||
|
|
|
|||
|
|
@ -605,7 +605,7 @@ mod tests {
|
|||
// Only images in use are saved.
|
||||
let img = d.alloc_id();
|
||||
let im = ImageObject { key: "k".into(), width: 1, height: 1, xf: Affine::IDENTITY, link: None, placement: Default::default() };
|
||||
d.insert(Some(l), 1, Node::new(img, NodeKind::Image(im))).unwrap();
|
||||
d.insert(Some(l), 1, Node::new(img, NodeKind::Image(Box::new(im)))).unwrap();
|
||||
let bytes = save(&d, true);
|
||||
assert!(sniff(&bytes));
|
||||
let back = load(&bytes).unwrap();
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ use vectorcraft_geom::Affine;
|
|||
fn image(d: &mut Document, key: &str, link: Option<LinkInfo>) {
|
||||
let (id, layer) = (d.alloc_id(), d.layers[0].id);
|
||||
let im = ImageObject { key: key.into(), width: 600, height: 300, xf: Affine::IDENTITY, link, placement: Default::default() };
|
||||
d.insert(Some(layer), 0, Node::new(id, NodeKind::Image(im))).unwrap();
|
||||
d.insert(Some(layer), 0, Node::new(id, NodeKind::Image(Box::new(im)))).unwrap();
|
||||
}
|
||||
|
||||
fn blob(full: &[u8], proxy: Option<&[u8]>) -> ImageBlob {
|
||||
|
|
|
|||
|
|
@ -35,7 +35,14 @@ fn image_node(d: &mut Document, key: &str) -> Node {
|
|||
let id = d.alloc_id();
|
||||
Node::new(
|
||||
id,
|
||||
NodeKind::Image(ImageObject { key: key.into(), width: 2, height: 2, xf: Affine::IDENTITY, link: None, placement: Default::default() }),
|
||||
NodeKind::Image(Box::new(ImageObject {
|
||||
key: key.into(),
|
||||
width: 2,
|
||||
height: 2,
|
||||
xf: Affine::IDENTITY,
|
||||
link: None,
|
||||
placement: Default::default(),
|
||||
})),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -120,7 +127,7 @@ fn unused_images_are_pruned() {
|
|||
d.images.insert(k.into(), blob(b));
|
||||
}
|
||||
let mut n = image_node(&mut d, "used");
|
||||
n.mask = Some(Box::new(OpacityMask::new(image_node(&mut d, "in mask"), false)));
|
||||
n.extras_mut().mask = Some(Box::new(OpacityMask::new(image_node(&mut d, "in mask"), false)));
|
||||
d.insert(Some(layer), 0, n).unwrap();
|
||||
let art = image_node(&mut d, "in symbol");
|
||||
d.symbols.push(Symbol { name: "S".into(), art: Arc::new(art) });
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ fn doc_with_image(key: &str, len: usize) -> Document {
|
|||
let l = d.layers[0].id;
|
||||
let id = d.alloc_id();
|
||||
let im = ImageObject { key: key.into(), width: 10, height: 10, xf: Affine::IDENTITY, link: None, placement: Default::default() };
|
||||
d.insert(Some(l), 0, Node::new(id, NodeKind::Image(im))).unwrap();
|
||||
d.insert(Some(l), 0, Node::new(id, NodeKind::Image(Box::new(im)))).unwrap();
|
||||
d
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -76,7 +76,7 @@ proptest! {
|
|||
let layer = d.layers[0].id;
|
||||
let id = d.alloc_id();
|
||||
let im = ImageObject { key: key.clone(), width: 1, height: 1, xf: Affine::IDENTITY, link: None, placement: Default::default() };
|
||||
d.insert(Some(layer), 0, Node::new(id, NodeKind::Image(im))).unwrap();
|
||||
d.insert(Some(layer), 0, Node::new(id, NodeKind::Image(Box::new(im)))).unwrap();
|
||||
for pretty in [false, true] {
|
||||
let back = load(&save(&d, pretty)).unwrap();
|
||||
prop_assert_eq!(back.images.get(&key).map(|b| b.bytes.as_ref().clone()), Some(data.clone()));
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ fn linked_doc() -> Document {
|
|||
link: Some(LinkInfo::new("/art/photo.png")),
|
||||
placement: Default::default(),
|
||||
};
|
||||
d.insert(Some(layer), 0, Node::new(id, NodeKind::Image(im))).unwrap();
|
||||
d.insert(Some(layer), 0, Node::new(id, NodeKind::Image(Box::new(im)))).unwrap();
|
||||
let mut blob = ImageBlob::new("image/png", vec![1, 2, 3, 4, 5, 6]);
|
||||
blob.proxy = Some(Arc::new(vec![9, 9]));
|
||||
d.images.insert("k".into(), blob);
|
||||
|
|
|
|||
|
|
@ -789,7 +789,7 @@ impl Player {
|
|||
return;
|
||||
}
|
||||
let im = ImageObject { key, width: img.width, height: img.height, xf, link: None, placement: Default::default() };
|
||||
let mut n = Node::new(vectorcraft_doc::NodeId(0), NodeKind::Image(im));
|
||||
let mut n = Node::new(vectorcraft_doc::NodeId(0), NodeKind::Image(Box::new(im)));
|
||||
n.opacity = opacity.clamp(0.0, 1.0) as f32;
|
||||
self.push(n);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -236,7 +236,7 @@ impl<'a> Scene<'a> {
|
|||
if n.blend != BlendMode::Normal {
|
||||
self.warn(BLENDING);
|
||||
}
|
||||
if n.mask.as_ref().is_some_and(|m| !m.disabled) {
|
||||
if n.extras().mask.as_ref().is_some_and(|m| !m.disabled) {
|
||||
self.warn(MASKS);
|
||||
}
|
||||
let raster = |fx: &[vectorcraft_doc::Effect]| fx.iter().any(|e| e.visible && vectorcraft_effects::is_raster(&e.id));
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ pub(crate) fn image_node(d: &mut Document, bytes: Vec<u8>, w: u32, h: u32, xf: A
|
|||
let blob = ImageBlob::new("image/png", bytes);
|
||||
let key = blob.content_key();
|
||||
d.images.insert(key.clone(), blob);
|
||||
Node::new(NodeId(0), NodeKind::Image(ImageObject { key, width: w, height: h, xf, link: None, placement: Default::default() }))
|
||||
Node::new(NodeId(0), NodeKind::Image(Box::new(ImageObject { key, width: w, height: h, xf, link: None, placement: Default::default() })))
|
||||
}
|
||||
|
||||
pub(crate) fn u32_at(b: &[u8], at: usize) -> u32 {
|
||||
|
|
|
|||
|
|
@ -880,7 +880,13 @@ impl Exporter<'_> {
|
|||
/// A luminosity mask that is 1 − the knockout shape of `c`: white, then black through an alpha
|
||||
/// mask of `c` (at full object opacity without its own mask, unless those define its shape).
|
||||
fn knockout_mask(&mut self, s: &mut Surface, c: &Node, page: Rect) -> krilla::mask::Mask {
|
||||
let shape = if c.knockout_shape { c.clone() } else { Node { opacity: 1.0, mask: None, ..c.clone() } };
|
||||
let shape = if c.knockout_shape {
|
||||
c.clone()
|
||||
} else {
|
||||
let mut s = c.clone().with_mask(None);
|
||||
s.opacity = 1.0;
|
||||
s
|
||||
};
|
||||
let mut sb = s.stream_builder();
|
||||
let mut ms = sb.surface();
|
||||
cover(&mut ms, page, 255, 1.0);
|
||||
|
|
@ -948,7 +954,7 @@ impl Exporter<'_> {
|
|||
}
|
||||
pushes += 1;
|
||||
}
|
||||
if let Some(m) = n.mask.as_deref()
|
||||
if let Some(m) = n.extras().mask.as_deref()
|
||||
&& !m.disabled
|
||||
{
|
||||
let mask = self.soft_mask(s, m, page);
|
||||
|
|
|
|||
|
|
@ -1016,17 +1016,17 @@ impl<'p> Builder<'p> {
|
|||
MaskSpec::Mask(m) => {
|
||||
let m = self.fresh_mask(m.clone());
|
||||
let node = match <[Arc<Node>; 1]>::try_from(art) {
|
||||
Ok([one]) if one.mask.is_none() => {
|
||||
fold_stop_opacity(&one, &m).unwrap_or_else(|| Node { mask: Some(Box::new(m)), ..Arc::unwrap_or_clone(one) })
|
||||
Ok([one]) if one.extras().mask.is_none() => {
|
||||
fold_stop_opacity(&one, &m).unwrap_or_else(|| Arc::unwrap_or_clone(one).with_mask(Some(Box::new(m))))
|
||||
}
|
||||
Ok([one]) => {
|
||||
let mut g = Node::group(self.id(), vec![one]);
|
||||
g.mask = Some(Box::new(m));
|
||||
g.extras_mut().mask = Some(Box::new(m));
|
||||
g
|
||||
}
|
||||
Err(all) => {
|
||||
let mut g = Node::group(self.id(), all);
|
||||
g.mask = Some(Box::new(m));
|
||||
g.extras_mut().mask = Some(Box::new(m));
|
||||
g
|
||||
}
|
||||
};
|
||||
|
|
@ -1089,7 +1089,7 @@ impl<'p> Builder<'p> {
|
|||
&& !only.is_container()
|
||||
&& !isolate
|
||||
&& !knockout
|
||||
&& (mask.is_none() || only.mask.is_none())
|
||||
&& (mask.is_none() || only.extras().mask.is_none())
|
||||
{
|
||||
// A single object in a group: fold opacity, blend and mask into the object.
|
||||
let mut only = (**only).clone();
|
||||
|
|
@ -1099,7 +1099,7 @@ impl<'p> Builder<'p> {
|
|||
}
|
||||
if let Some(m) = mask {
|
||||
let m = self.fresh_mask(m);
|
||||
only = fold_stop_opacity(&only, &m).unwrap_or(Node { mask: Some(Box::new(m)), ..only });
|
||||
only = fold_stop_opacity(&only, &m).unwrap_or_else(|| only.with_mask(Some(Box::new(m))));
|
||||
}
|
||||
Some(only)
|
||||
} else {
|
||||
|
|
@ -1108,7 +1108,7 @@ impl<'p> Builder<'p> {
|
|||
if knockout {
|
||||
g.knockout = Knockout::On;
|
||||
}
|
||||
g.mask = mask.map(|m| Box::new(self.fresh_mask(m)));
|
||||
g.extras_mut().mask = mask.map(|m| Box::new(self.fresh_mask(m)));
|
||||
Some(g)
|
||||
}
|
||||
}
|
||||
|
|
@ -1289,7 +1289,10 @@ impl<'p> Builder<'p> {
|
|||
(k, a.width, a.height)
|
||||
};
|
||||
let xf = transform * Affine::scale_non_uniform(f64::from(w) / f64::from(mw.max(1)), f64::from(h) / f64::from(mh.max(1)));
|
||||
let art = Node::new(self.id(), NodeKind::Image(ImageObject { key: k, width: mw, height: mh, xf, link: None, placement: Default::default() }));
|
||||
let art = Node::new(
|
||||
self.id(),
|
||||
NodeKind::Image(Box::new(ImageObject { key: k, width: mw, height: mh, xf, link: None, placement: Default::default() })),
|
||||
);
|
||||
Some(OpacityMask::new(art, true))
|
||||
}
|
||||
|
||||
|
|
@ -1308,8 +1311,9 @@ impl<'p> Builder<'p> {
|
|||
(k, w, h)
|
||||
};
|
||||
let id = self.id();
|
||||
let mut n = Node::new(id, NodeKind::Image(ImageObject { key: k, width: w, height: h, xf, link: None, placement: Default::default() }));
|
||||
n.mask = mask.map(Box::new);
|
||||
let mut n =
|
||||
Node::new(id, NodeKind::Image(Box::new(ImageObject { key: k, width: w, height: h, xf, link: None, placement: Default::default() })));
|
||||
n.extras_mut().mask = mask.map(Box::new);
|
||||
self.push_node(n);
|
||||
}
|
||||
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue