mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-03 06:30:00 -06:00
Intertwine: a right-click where three or more objects overlap arranges the nearest one
Squashed from #4862 onto main.
This commit is contained in:
@@ -44,6 +44,11 @@ pub struct NodeExtras {
|
||||
/// Object › Intertwine: as a member of an Intertwine group, the area (document coordinates,
|
||||
/// filled non-zero) where this object is drawn over the group's other members.
|
||||
pub intertwine: Option<Box<PathData>>,
|
||||
/// Object › Intertwine: as a member of an Intertwine group, the areas (document coordinates,
|
||||
/// filled non-zero) where this object is drawn over members that are themselves drawn over
|
||||
/// others there. Item `k` is the area of tier `k + 2`; [`NodeExtras::intertwine`] is tier 1
|
||||
/// (see [`crate::intertwine`]).
|
||||
pub intertwine_tiers: Vec<PathData>,
|
||||
/// Live Paint group: its Gap Options (Object › Live Paint › Gap Options; `None`: the defaults).
|
||||
pub live_paint_gaps: Option<Box<crate::live_paint::GapOptions>>,
|
||||
/// Object › Objects on Path: the group's path and the objects attached to it (see
|
||||
@@ -76,6 +81,7 @@ pub(super) static NO_EXTRAS: NodeExtras = NodeExtras {
|
||||
perspective: None,
|
||||
dimension: None,
|
||||
intertwine: None,
|
||||
intertwine_tiers: Vec::new(),
|
||||
live_paint_gaps: None,
|
||||
objects_on_path: None,
|
||||
colorize: None,
|
||||
|
||||
@@ -496,8 +496,13 @@ impl Node {
|
||||
Arc::make_mut(&mut m.art).transform_scaled(a, sc);
|
||||
}
|
||||
// The area where an Intertwine member is drawn over the others moves with it.
|
||||
if let Some(area) = self.extras_if_any().and_then(|e| e.intertwine.as_mut()) {
|
||||
area.transform(a);
|
||||
if let Some(e) = self.extras_if_any() {
|
||||
if let Some(area) = e.intertwine.as_mut() {
|
||||
area.transform(a);
|
||||
}
|
||||
for area in &mut e.intertwine_tiers {
|
||||
area.transform(a);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -484,6 +484,23 @@ fn objects_are_read_within_the_node_budget() {
|
||||
assert!(with_node_budget(3, read).is_ok());
|
||||
}
|
||||
|
||||
/// An Intertwine member's areas of tier 2 and up are written and read back, and each one counts
|
||||
/// against [`with_node_budget`] as an object does, so a file of many empty areas is refused.
|
||||
#[test]
|
||||
fn intertwine_tiers_are_read_within_the_node_budget() {
|
||||
let mut n = Node::group(NodeId(1), vec![]);
|
||||
let area = vectorcraft_geom::shapes::rectangle(vectorcraft_geom::Rect::new(0.0, 0.0, 10.0, 10.0));
|
||||
n.extras_mut().intertwine_tiers = vec![PathData::default(), area];
|
||||
let json = serde_json::to_string(&n).unwrap();
|
||||
assert!(json.contains("\"intertwine_tiers\""), "{json}");
|
||||
let read = || serde_json::from_str::<Node>(&json);
|
||||
assert_eq!(with_node_budget(3, read).unwrap(), n, "the object and its two areas fit a budget of three");
|
||||
let why = with_node_budget(2, read).unwrap_err().to_string();
|
||||
assert!(why.contains("more than 2 objects"), "{why}");
|
||||
let plain = serde_json::to_string(&Node::group(NodeId(1), vec![])).unwrap();
|
||||
assert!(!plain.contains("intertwine_tiers"), "an object without tiers writes none: {plain}");
|
||||
}
|
||||
|
||||
/// #3587: fields read before an object's `type` are buffered value by value within
|
||||
/// [`with_buffered_value_budget`], each counted once: a group read back from the buffer buffers
|
||||
/// its own fields again without counting them twice. VectorCraft writes `type` first, which
|
||||
|
||||
@@ -46,6 +46,7 @@ impl Serialize for Node {
|
||||
e.perspective.is_none(),
|
||||
e.dimension.is_none(),
|
||||
e.intertwine.is_none(),
|
||||
e.intertwine_tiers.is_empty(),
|
||||
e.live_paint_gaps.is_none(),
|
||||
e.objects_on_path.is_none(),
|
||||
e.colorize.is_none(),
|
||||
@@ -85,6 +86,7 @@ impl Serialize for Node {
|
||||
field!("perspective", &e.perspective);
|
||||
field!("dimension", &e.dimension);
|
||||
field!("intertwine", &e.intertwine);
|
||||
field!("intertwine_tiers", &e.intertwine_tiers);
|
||||
field!("live_paint_gaps", &e.live_paint_gaps);
|
||||
field!("objects_on_path", &e.objects_on_path);
|
||||
field!("colorize", &e.colorize);
|
||||
@@ -123,11 +125,39 @@ pub fn with_node_budget<T>(max: usize, f: impl FnOnce() -> T) -> T {
|
||||
/// each counts once, and the id usually comes first, before the node's children are read.
|
||||
fn counted_id<'de, D: serde::Deserializer<'de>>(d: D) -> Result<NodeId, D::Error> {
|
||||
let id = NodeId::deserialize(d)?;
|
||||
count_one()?;
|
||||
Ok(id)
|
||||
}
|
||||
|
||||
/// Count one object against [`with_node_budget`].
|
||||
fn count_one<E: serde::de::Error>() -> Result<(), E> {
|
||||
if let Some((left, max)) = NODES_LEFT.get() {
|
||||
let left = left.checked_sub(1).ok_or_else(|| serde::de::Error::custom(format!("the document has more than {max} objects")))?;
|
||||
let left = left.checked_sub(1).ok_or_else(|| E::custom(format!("the document has more than {max} objects")))?;
|
||||
NODES_LEFT.set(Some((left, max)));
|
||||
}
|
||||
Ok(id)
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// A member's Intertwine areas of tier 2 and up, each counting against [`with_node_budget`] as
|
||||
/// an object does: a compressed file of repeated empty areas stays small while they take memory
|
||||
/// once read.
|
||||
fn counted_tiers<'de, D: serde::Deserializer<'de>>(d: D) -> Result<Vec<PathData>, D::Error> {
|
||||
struct Tiers;
|
||||
impl<'de> serde::de::Visitor<'de> for Tiers {
|
||||
type Value = Vec<PathData>;
|
||||
fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
f.write_str("a list of areas")
|
||||
}
|
||||
fn visit_seq<A: serde::de::SeqAccess<'de>>(self, mut seq: A) -> Result<Vec<PathData>, A::Error> {
|
||||
let mut out = Vec::new();
|
||||
while let Some(area) = seq.next_element::<PathData>()? {
|
||||
count_one()?;
|
||||
out.push(area);
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
}
|
||||
d.deserialize_seq(Tiers)
|
||||
}
|
||||
|
||||
/// How a [`Node`] is read: the flat field list of the file format. Fields move into the node
|
||||
@@ -186,6 +216,8 @@ pub(super) struct NodeWire {
|
||||
dimension: Option<Box<crate::dimension::Dimension>>,
|
||||
#[serde(default)]
|
||||
intertwine: Option<Box<PathData>>,
|
||||
#[serde(default, deserialize_with = "counted_tiers")]
|
||||
intertwine_tiers: Vec<PathData>,
|
||||
#[serde(default)]
|
||||
live_paint_gaps: Option<Box<crate::live_paint::GapOptions>>,
|
||||
#[serde(default)]
|
||||
@@ -215,6 +247,7 @@ impl From<NodeWire> for Node {
|
||||
perspective: w.perspective,
|
||||
dimension: w.dimension,
|
||||
intertwine: w.intertwine,
|
||||
intertwine_tiers: w.intertwine_tiers,
|
||||
live_paint_gaps: w.live_paint_gaps,
|
||||
objects_on_path: w.objects_on_path,
|
||||
colorize: w.colorize,
|
||||
|
||||
Reference in New Issue
Block a user