mirror of
https://github.com/storytold/figcraft.git
synced 2026-11-01 14:26:49 -06:00
M4: preserve inherited region paints and linked fill styles
This commit is contained in:
@@ -64,6 +64,22 @@ fn ordinary_hit_uses_region_paints_without_layer_fill() {
|
||||
assert_eq!(d.hit_test(p, Point::new(15.0, 5.0), 0.0, None), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inherited_paint_frames_use_per_region_hit_testing() {
|
||||
let mut d = Document::new("inherited region hit");
|
||||
let p = page(&d);
|
||||
let mut network = VectorNetwork::from_points(&[Point::new(0.0, 0.0), Point::new(10.0, 0.0), Point::new(10.0, 10.0), Point::new(0.0, 10.0)], true);
|
||||
network.regions[0].winding = WindingRule::EvenOdd;
|
||||
network.regions.push(network.regions[0].clone());
|
||||
let id = d.create(p, NodeKind::Vector { network }, "V", 0.0, 0.0, 10.0, 10.0).unwrap();
|
||||
assert_eq!(d.hit_test(p, Point::new(5.0, 5.0), 0.0, None), None, "legacy even-odd loops cancel");
|
||||
let NodeKind::Vector { network } = &mut d.get_mut(id).unwrap().kind else { panic!("expected vector") };
|
||||
network.regions[0].paint_space = Some(crate::RegionPaintSpace { width: 10.0, height: 10.0, transform: figcraft_geom::Affine::IDENTITY });
|
||||
assert!(network.regions.iter().all(|region| region.fills.is_none()));
|
||||
assert_eq!(d.hit_test(p, Point::new(5.0, 5.0), 0.0, None), Some(id), "paint-framed regions inherit the node fill independently");
|
||||
assert_eq!(d.hit_test(p, Point::new(15.0, 5.0), 0.0, None), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicitly_unfilled_regions_keep_the_stroke_only_selection_band() {
|
||||
let mut d = Document::new("stroke selection");
|
||||
|
||||
@@ -74,7 +74,7 @@ fn hits_node_itself(doc: &Document, id: NodeId, n: &Node, pt: Point, tol: f64, o
|
||||
NodeKind::Frame | NodeKind::Component { .. } | NodeKind::ComponentSet { .. } | NodeKind::Instance { .. } | NodeKind::Section { .. } => {
|
||||
n.local_rect().inflate(tol_l, tol_l).contains(lp)
|
||||
}
|
||||
NodeKind::Vector { network } if network.regions.iter().any(|r| r.fills.is_some()) => {
|
||||
NodeKind::Vector { network } if network.regions.iter().any(|r| r.fills.is_some() || r.paint_space.is_some()) => {
|
||||
let rounded = network.rounded();
|
||||
let net = rounded.as_ref().unwrap_or(network);
|
||||
if !outline {
|
||||
|
||||
@@ -387,9 +387,19 @@ fn flatten(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
.as_ref()
|
||||
.filter(|node| matches!(node.kind, NodeKind::Group | NodeKind::Line) && node.fills.is_empty())
|
||||
.map(|_| vec![Paint::solid(Color::DEFAULT_SHAPE)]);
|
||||
let output_fills = first
|
||||
.as_ref()
|
||||
.map(|node| {
|
||||
if matches!(node.kind, NodeKind::Group | NodeKind::Line) && node.fills.is_empty() {
|
||||
fallback_fills.clone().unwrap_or_default()
|
||||
} else {
|
||||
node.fills.clone()
|
||||
}
|
||||
})
|
||||
.unwrap_or_default();
|
||||
let mut net = VectorNetwork::default();
|
||||
for id in &ids {
|
||||
collect_flatten_parts(doc, *id, pw, fallback_fills.as_deref(), &mut net, 0)?;
|
||||
collect_flatten_parts(doc, *id, pw, fallback_fills.as_deref(), &output_fills, &mut net, 0)?;
|
||||
}
|
||||
let Some(bb) = net.bounds() else {
|
||||
return Err(EngineError::Disabled("vector.flatten".into(), "nothing to flatten".into()));
|
||||
@@ -400,11 +410,7 @@ fn flatten(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let mut v = Node::new(vid, NodeKind::Vector { network: net }, name, bb.width(), bb.height());
|
||||
v.transform = Affine::translate((bb.x0, bb.y0));
|
||||
if let Some(f) = first {
|
||||
v.fills = if matches!(f.kind, NodeKind::Group | NodeKind::Line) && f.fills.is_empty() {
|
||||
vec![Paint::solid(Color::DEFAULT_SHAPE)]
|
||||
} else {
|
||||
f.fills.clone()
|
||||
};
|
||||
v.fills = output_fills;
|
||||
v.strokes = f.strokes.clone();
|
||||
v.stroke = StrokeProps { align: StrokeAlign::Center, ..f.stroke.clone() };
|
||||
v.effects = f.effects.clone();
|
||||
@@ -424,6 +430,7 @@ fn collect_flatten_parts(
|
||||
id: NodeId,
|
||||
parent_world: Affine,
|
||||
fallback_fills: Option<&[Paint]>,
|
||||
output_fills: &[Paint],
|
||||
net: &mut VectorNetwork,
|
||||
depth: usize,
|
||||
) -> Result<()> {
|
||||
@@ -438,17 +445,15 @@ fn collect_flatten_parts(
|
||||
&& node.fills.iter().any(|paint| paint.visible)
|
||||
&& let Some(path) = figcraft_doc::geometry::shape_path(node)
|
||||
{
|
||||
let mut frame = VectorNetwork::from_path(&path);
|
||||
for region in &mut frame.regions {
|
||||
region.fills = Some(node.fills.clone());
|
||||
}
|
||||
append_flatten_part(doc, id, node, parent_world, fallback_fills, frame, net);
|
||||
let frame = VectorNetwork::from_path(&path);
|
||||
let into_parent = parent_world.inverse() * doc.world_transform(id);
|
||||
append_flatten_part(node, fallback_fills, output_fills, into_parent, frame, net);
|
||||
}
|
||||
for child in &node.children {
|
||||
if !doc.node(*child).is_some_and(|child| child.visible) {
|
||||
continue;
|
||||
}
|
||||
collect_flatten_parts(doc, *child, parent_world, fallback_fills, net, depth + 1)?;
|
||||
collect_flatten_parts(doc, *child, parent_world, fallback_fills, output_fills, net, depth + 1)?;
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
@@ -473,29 +478,30 @@ fn collect_flatten_parts(
|
||||
if rule == figcraft_render::FillRule::EvenOdd {
|
||||
region.winding = WindingRule::EvenOdd;
|
||||
}
|
||||
region.fills = Some(if node.fills.is_empty() { fallback_fills.unwrap_or(&node.fills).to_vec() } else { node.fills.clone() });
|
||||
}
|
||||
part
|
||||
}
|
||||
};
|
||||
append_flatten_part(doc, id, node, parent_world, fallback_fills, part, net);
|
||||
let into_parent = parent_world.inverse() * doc.world_transform(id);
|
||||
append_flatten_part(node, fallback_fills, output_fills, into_parent, part, net);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn append_flatten_part(
|
||||
doc: &Document,
|
||||
id: NodeId,
|
||||
node: &Node,
|
||||
parent_world: Affine,
|
||||
fallback_fills: Option<&[Paint]>,
|
||||
output_fills: &[Paint],
|
||||
into_parent: Affine,
|
||||
mut part: VectorNetwork,
|
||||
net: &mut VectorNetwork,
|
||||
) {
|
||||
let into_parent = parent_world.inverse() * doc.world_transform(id);
|
||||
for region in &mut part.regions {
|
||||
let (width, height, paint_transform) = region.paint_frame(node.width, node.height);
|
||||
if region.fills.is_none() {
|
||||
region.fills = Some(if node.fills.is_empty() { fallback_fills.unwrap_or(&node.fills).to_vec() } else { node.fills.clone() });
|
||||
let effective = if node.fills.is_empty() { fallback_fills.unwrap_or(&node.fills) } else { &node.fills };
|
||||
if effective != output_fills {
|
||||
region.fills = Some(effective.to_vec());
|
||||
}
|
||||
}
|
||||
region.paint_space = Some(RegionPaintSpace { width, height, transform: paint_transform });
|
||||
}
|
||||
@@ -700,11 +706,28 @@ fn simplify(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
if let NodeKind::Vector { network } = &mut n.kind {
|
||||
let source = network.clone();
|
||||
let mut net = VectorNetwork::default();
|
||||
for (index, region) in source.regions.iter().enumerate() {
|
||||
let Some(path) = source.region_path(index) else { continue };
|
||||
let legacy = source.regions.iter().all(|region| region.fills.is_none() && region.paint_space.is_none());
|
||||
if legacy && !source.regions.is_empty() {
|
||||
let winding = if source.regions.iter().any(|region| region.winding == WindingRule::EvenOdd) {
|
||||
WindingRule::EvenOdd
|
||||
} else {
|
||||
WindingRule::NonZero
|
||||
};
|
||||
let path = source.fill_path();
|
||||
let path = figcraft_geom::kurbo::simplify::simplify_bezpath(path.iter(), tol, &Default::default());
|
||||
let path = figcraft_pathops::simplify_with_rule(&path, region.winding == WindingRule::EvenOdd).unwrap_or(path);
|
||||
append_region_path(&mut net, &path, region, region.winding);
|
||||
let (path, output_winding) = match figcraft_pathops::simplify_with_rule(&path, winding == WindingRule::EvenOdd) {
|
||||
Ok(path) => (path, WindingRule::EvenOdd),
|
||||
Err(_) => (path, winding),
|
||||
};
|
||||
let combined = Region { winding: output_winding, ..Default::default() };
|
||||
append_region_path(&mut net, &path, &combined, output_winding);
|
||||
} else {
|
||||
for (index, region) in source.regions.iter().enumerate() {
|
||||
let Some(path) = source.region_path(index) else { continue };
|
||||
let path = figcraft_geom::kurbo::simplify::simplify_bezpath(path.iter(), tol, &Default::default());
|
||||
let path = figcraft_pathops::simplify_with_rule(&path, region.winding == WindingRule::EvenOdd).unwrap_or(path);
|
||||
append_region_path(&mut net, &path, region, region.winding);
|
||||
}
|
||||
}
|
||||
if !source.regions.is_empty() {
|
||||
let mut used = vec![false; source.segments.len()];
|
||||
@@ -803,18 +826,15 @@ fn offset_path(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let (paint_width, paint_height, paint_transform) = source.regions[0].paint_frame(n.width, n.height);
|
||||
for region in &mut part.regions {
|
||||
region.winding = WindingRule::EvenOdd;
|
||||
region.fills = Some(n.fills.clone());
|
||||
region.fills = source.regions[0].fills.clone();
|
||||
region.paint_space = Some(RegionPaintSpace { width: paint_width, height: paint_height, transform: paint_transform });
|
||||
}
|
||||
append_network(&mut net, part);
|
||||
} else {
|
||||
for (index, region) in source.regions.iter().enumerate() {
|
||||
let Some(path) = source.region_path(index) else { continue };
|
||||
let (paint_width, paint_height, paint_transform) = region.paint_frame(n.width, n.height);
|
||||
let mut paint_region = region.clone();
|
||||
if paint_region.fills.is_none() {
|
||||
paint_region.fills = Some(n.fills.clone());
|
||||
}
|
||||
let (paint_width, paint_height, paint_transform) = region.paint_frame(n.width, n.height);
|
||||
paint_region.paint_space = Some(RegionPaintSpace { width: paint_width, height: paint_height, transform: paint_transform });
|
||||
let path = figcraft_pathops::simplify_with_rule(&path, region.winding == WindingRule::EvenOdd)
|
||||
.map_err(|error| bad("vector.offset", error.to_string()))?;
|
||||
|
||||
@@ -7,9 +7,9 @@
|
||||
// The tables read best as tuples.
|
||||
#![allow(clippy::type_complexity)]
|
||||
|
||||
use figcraft_doc::{HandleMirroring, NodeId, NodeKind, VectorNetwork};
|
||||
use figcraft_doc::{HandleMirroring, NodeId, NodeKind, Region, Segment, VectorNetwork, WindingRule};
|
||||
use figcraft_geom::kurbo::ParamCurve;
|
||||
use figcraft_geom::{Point, Shape};
|
||||
use figcraft_geom::{BezPath, Point, Shape};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use crate::Session;
|
||||
@@ -100,17 +100,70 @@ fn region_paints_survive_simplify_offset_and_bucket_normalization() {
|
||||
network.regions[0].fills = None;
|
||||
}
|
||||
}
|
||||
let inherited_fills = s.doc().unwrap().get(inherited_id).unwrap().fills.clone();
|
||||
let (old_width, old_height) = {
|
||||
let node = s.doc().unwrap().get(inherited_id).unwrap();
|
||||
(node.width, node.height)
|
||||
};
|
||||
ex(&mut s, "vector.offset", json!({"ids": [inherited_id], "offset": 2}));
|
||||
let inherited_net = net(&s, inherited_id);
|
||||
let inherited = &inherited_net.regions[0];
|
||||
assert_eq!(inherited.fills, Some(inherited_fills), "inherited paints become explicit when the node box refits");
|
||||
let frame = inherited.paint_space.expect("refitted inherited paint has a frame");
|
||||
assert_eq!((frame.width, frame.height), (old_width, old_height), "paint coordinates retain the original node box");
|
||||
assert_eq!(inherited.fills, None, "inherited paints remain inherited after offset");
|
||||
let inherited_space = inherited.paint_space.expect("offset binds inherited paints to the original frame");
|
||||
assert_eq!((inherited_space.width, inherited_space.height), (100.0, 100.0));
|
||||
|
||||
let inherited_flatten = square(&mut s, false);
|
||||
let inherited_fills = s.doc().unwrap().get(inherited_flatten).unwrap().fills.clone();
|
||||
ex_to_vector(s.doc_mut().unwrap(), inherited_flatten);
|
||||
if let NodeKind::Vector { network } = &mut s.doc_mut().unwrap().get_mut(inherited_flatten).unwrap().kind {
|
||||
network.regions[0].fills = None;
|
||||
}
|
||||
let group = id_of(&ex(&mut s, "object.group", json!({"ids": [inherited_flatten]}))["id"]).unwrap();
|
||||
let flattened = id_of(&ex(&mut s, "vector.flatten", json!({"ids": [group]}))["id"]).unwrap();
|
||||
assert!(
|
||||
net(&s, flattened).regions.iter().all(|region| region.fills.is_none()),
|
||||
"flatten keeps inherited paints inherited when output node has same fills"
|
||||
);
|
||||
assert_eq!(s.doc().unwrap().get(flattened).unwrap().fills, inherited_fills);
|
||||
|
||||
let plain = id_of(&ex(&mut s, "shape.rectangle", json!({"x": 0, "y": 0, "width": 80, "height": 60}))["id"]).unwrap();
|
||||
let plain_fills = s.doc().unwrap().get(plain).unwrap().fills.clone();
|
||||
let group = id_of(&ex(&mut s, "object.group", json!({"ids": [plain]}))["id"]).unwrap();
|
||||
let flattened_plain = id_of(&ex(&mut s, "vector.flatten", json!({"ids": [group]}))["id"]).unwrap();
|
||||
assert!(net(&s, flattened_plain).regions.iter().all(|region| region.fills.is_none()), "plain shapes inherit the flattened node's fill");
|
||||
assert_eq!(s.doc().unwrap().get(flattened_plain).unwrap().fills, plain_fills);
|
||||
let edited_fills = vec![figcraft_color::Paint::solid(figcraft_color::Color::BLACK)];
|
||||
s.doc_mut().unwrap().get_mut(flattened_plain).unwrap().fills = edited_fills.clone();
|
||||
assert_eq!(net(&s, flattened_plain).regions[0].fills, None, "changing the flattened node fill updates inherited geometry");
|
||||
|
||||
let different_source = square(&mut s, false);
|
||||
s.doc_mut().unwrap().get_mut(different_source).unwrap().fills = vec![figcraft_color::Paint::solid(figcraft_color::Color::BLACK)];
|
||||
ex_to_vector(s.doc_mut().unwrap(), different_source);
|
||||
let source_fills = s.doc().unwrap().get(different_source).unwrap().fills.clone();
|
||||
if let NodeKind::Vector { network } = &mut s.doc_mut().unwrap().get_mut(different_source).unwrap().kind {
|
||||
network.regions[0].fills = None;
|
||||
}
|
||||
let group = id_of(&ex(&mut s, "object.group", json!({"ids": [different_source]}))["id"]).unwrap();
|
||||
s.doc_mut().unwrap().get_mut(group).unwrap().fills.clear();
|
||||
let flattened = id_of(&ex(&mut s, "vector.flatten", json!({"ids": [group]}))["id"]).unwrap();
|
||||
assert_eq!(net(&s, flattened).regions[0].fills, Some(source_fills), "flatten retains paints that differ from output node fills");
|
||||
|
||||
let legacy_gradient = square(&mut s, false);
|
||||
let mut gradient = figcraft_color::Gradient::default_for(figcraft_color::GradientKind::Linear, figcraft_color::Color::BLACK);
|
||||
gradient.from = (0.2, 0.1);
|
||||
gradient.to = (0.8, 0.9);
|
||||
s.doc_mut().unwrap().get_mut(legacy_gradient).unwrap().fills = vec![figcraft_color::Paint::gradient(gradient)];
|
||||
ex_to_vector(s.doc_mut().unwrap(), legacy_gradient);
|
||||
if let NodeKind::Vector { network } = &mut s.doc_mut().unwrap().get_mut(legacy_gradient).unwrap().kind {
|
||||
network.regions[0].fills = None;
|
||||
}
|
||||
let before = net(&s, legacy_gradient).regions[0].paint_frame(100.0, 100.0).2;
|
||||
let before_world = s.doc().unwrap().world_transform(legacy_gradient) * before;
|
||||
ex(&mut s, "vector.offset", json!({"ids": [legacy_gradient], "offset": 2}));
|
||||
let after_net = net(&s, legacy_gradient);
|
||||
let after = after_net.regions[0]
|
||||
.paint_frame(s.doc().unwrap().get(legacy_gradient).unwrap().width, s.doc().unwrap().get(legacy_gradient).unwrap().height)
|
||||
.2;
|
||||
let after_world = s.doc().unwrap().world_transform(legacy_gradient) * after;
|
||||
assert!(
|
||||
before_world.as_coeffs().iter().zip(after_world.as_coeffs()).all(|(before, after)| (before - after).abs() < 1e-8),
|
||||
"legacy offset preserves the gradient's world paint frame"
|
||||
);
|
||||
|
||||
let empty_faces =
|
||||
id_of(&ex(&mut s, "vector.create", json!({"path": "M0 0 L100 0 L100 100 L0 100 Z M200 0 L300 0 L300 100 L200 100 Z"}))["id"]).unwrap();
|
||||
@@ -133,6 +186,45 @@ fn region_paints_survive_simplify_offset_and_bucket_normalization() {
|
||||
assert!(!s.doc().unwrap().get(empty_faces).unwrap().fills.is_empty(), "the painted target gets the layer default fill");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_simplify_combines_mixed_winding_regions() {
|
||||
let mut s = Session::new();
|
||||
let id = id_of(&ex(&mut s, "vector.create", json!({"path": "M0 0 L100 0 L100 100 L0 100 Z M25 25 L75 25 L75 75 L25 75 Z"}))["id"]).unwrap();
|
||||
let mut second_path = BezPath::new();
|
||||
second_path.move_to((50.0, 0.0));
|
||||
second_path.line_to((150.0, 0.0));
|
||||
second_path.line_to((150.0, 100.0));
|
||||
second_path.line_to((50.0, 100.0));
|
||||
second_path.close_path();
|
||||
let second = VectorNetwork::from_path(&second_path);
|
||||
{
|
||||
let doc = s.doc_mut().unwrap();
|
||||
let NodeKind::Vector { network } = &mut doc.get_mut(id).unwrap().kind else { panic!("expected vector fixture") };
|
||||
network.regions[0].winding = WindingRule::EvenOdd;
|
||||
let vertex_offset = network.vertices.len();
|
||||
let segment_offset = network.segments.len();
|
||||
network.vertices.extend(second.vertices);
|
||||
network.segments.extend(second.segments.into_iter().map(|segment| Segment {
|
||||
a: segment.a + vertex_offset,
|
||||
b: segment.b + vertex_offset,
|
||||
..segment
|
||||
}));
|
||||
network.regions.extend(second.regions.into_iter().map(|region| Region {
|
||||
loops: region.loops.into_iter().map(|lp| lp.into_iter().map(|segment| segment + segment_offset).collect()).collect(),
|
||||
..region
|
||||
}));
|
||||
network.regions[1].winding = WindingRule::NonZero;
|
||||
}
|
||||
ex(&mut s, "vector.simplify", json!({"ids": [id]}));
|
||||
let simplified = net(&s, id);
|
||||
assert_eq!(simplified.regions.len(), 1, "legacy regions simplify as one combined fill");
|
||||
assert!(!simplified.fill_contains(Point::new(60.0, 10.0)), "overlap cancels under combined even-odd winding");
|
||||
assert!(!simplified.fill_contains(Point::new(40.0, 40.0)), "nested contour remains a hole");
|
||||
assert!(simplified.fill_contains(Point::new(120.0, 50.0)), "non-overlapping area remains filled");
|
||||
assert!(simplified.regions[0].fills.is_none());
|
||||
assert!(simplified.regions[0].paint_space.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_evenodd_holes_survive_bucket_offset_and_flatten() {
|
||||
let mut s = Session::new();
|
||||
|
||||
@@ -1776,7 +1776,7 @@ impl<'d> Exporter<'d> {
|
||||
/// The editable network (with per-point corner radii, handle mirroring and end caps) and,
|
||||
/// when it has filled areas, their outline.
|
||||
fn vector(&mut self, n: &Node, net: &VectorNetwork, f: &mut Fields) {
|
||||
if net.regions.iter().any(|region| region.fills.is_some()) {
|
||||
if net.regions.iter().any(|region| region.fills.is_some() || region.paint_space.is_some()) {
|
||||
self.warn("vector region paints were left out of the .fig export");
|
||||
}
|
||||
let entries: usize = net.regions.iter().flat_map(|r| &r.loops).map(Vec::len).sum();
|
||||
|
||||
@@ -842,6 +842,33 @@ fn fig_export_warns_when_region_paints_are_left_out() {
|
||||
assert!(warnings.iter().any(|warning| warning == "vector region paints were left out of the .fig export"), "{warnings:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fig_export_warns_when_inherited_region_paints_have_a_paint_frame() {
|
||||
let mut doc = generate(3);
|
||||
let id = doc
|
||||
.nodes()
|
||||
.find_map(|node| match &node.kind {
|
||||
NodeKind::Vector { network } if !network.regions.is_empty() => Some(node.id),
|
||||
_ => None,
|
||||
})
|
||||
.unwrap();
|
||||
let node = doc.node_mut(id).unwrap();
|
||||
let NodeKind::Vector { network } = &mut node.kind else { panic!("expected vector fixture") };
|
||||
let region = network.regions.first_mut().unwrap();
|
||||
region.fills = None;
|
||||
region.paint_space = Some(RegionPaintSpace { width: 20.0, height: 10.0, transform: Affine::translate((4.0, 8.0)) });
|
||||
|
||||
let (_, warnings) = export(&doc, None).unwrap();
|
||||
assert!(warnings.iter().any(|warning| warning == "vector region paints were left out of the .fig export"), "{warnings:?}");
|
||||
|
||||
let mut plain = doc;
|
||||
let node = plain.node_mut(id).unwrap();
|
||||
let NodeKind::Vector { network } = &mut node.kind else { panic!("expected vector fixture") };
|
||||
network.regions[0].paint_space = None;
|
||||
let (_, warnings) = export(&plain, None).unwrap();
|
||||
assert!(!warnings.iter().any(|warning| warning == "vector region paints were left out of the .fig export"), "{warnings:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn written_files_decode_and_list_parents_first() {
|
||||
for seed in 0..20 {
|
||||
|
||||
@@ -1820,7 +1820,19 @@ impl<'a> Mapper<'a> {
|
||||
if r.style != 0 {
|
||||
match style(r.style) {
|
||||
Some(s) => {
|
||||
if let Some(fills) = s.get("fillPaints") {
|
||||
let fills = s.get("fillPaints").and_then(|own_fills| {
|
||||
if own_fills.list().iter().any(paint_bound) {
|
||||
return Some(own_fills);
|
||||
}
|
||||
let linked_style = style_link(&View::of(s), "styleIdForFill", "inheritFillStyleID")
|
||||
.and_then(|id| self.nodes.get(&id).copied())
|
||||
.filter(|style| style.get("styleType").and_then(Kv::str).is_some());
|
||||
match linked_style {
|
||||
Some(style) => style.get("fillPaints"),
|
||||
None => Some(own_fills),
|
||||
}
|
||||
});
|
||||
if let Some(fills) = fills {
|
||||
if let Kv::Array(source_paints) = fills {
|
||||
let paint_cost = source_paints.iter().fold(source_paints.len(), |cost, paint| {
|
||||
cost.saturating_add(paint.get("stops").map(Kv::list).unwrap_or_default().len())
|
||||
|
||||
@@ -282,6 +282,27 @@ fn region_canvas(fill_paints: Option<Vec<Kv<'static>>>, style_id: u32) -> Vec<u8
|
||||
canvas(&schema(), &message)
|
||||
}
|
||||
|
||||
fn region_canvas_with_link(own: Option<Kv<'static>>, link_target: Option<u64>, linked_fill: Kv<'static>) -> Vec<u8> {
|
||||
let mut message = tail_message();
|
||||
let Kv::Array(nodes) = field_mut(&mut message, "nodeChanges").unwrap() else { panic!() };
|
||||
let wave = nodes.iter_mut().find(|node| node.get("name").and_then(Kv::str) == Some("Wave")).unwrap();
|
||||
let vector_data = field_mut(wave, "vectorData").unwrap();
|
||||
let Kv::Array(styles) = field_mut(vector_data, "styleOverrideTable").unwrap() else { panic!() };
|
||||
let Kv::Object(fields) = styles.first_mut().unwrap() else { panic!() };
|
||||
if let Some(own) = own {
|
||||
fields.push(("fillPaints", arr(vec![own])));
|
||||
}
|
||||
if let Some(target) = link_target {
|
||||
fields.push(("styleIdForFill", o(vec![("guid", g(target))])));
|
||||
}
|
||||
nodes.push(node(50, None, "RECTANGLE", "Linked Fill", vec![("styleType", e("FILL")), ("fillPaints", arr(vec![linked_fill]))]));
|
||||
let mut network = network::decode(&square_blob()).unwrap();
|
||||
network.regions[0].style = 1;
|
||||
let Kv::Array(blobs) = field_mut(&mut message, "blobs").unwrap() else { panic!() };
|
||||
*field_mut(blobs.get_mut(1).unwrap(), "bytes").unwrap() = Kv::Bytes(network::encode(&network));
|
||||
canvas(&schema(), &message)
|
||||
}
|
||||
|
||||
fn field_mut<'a, 's>(value: &'a mut Kv<'s>, name: &str) -> Option<&'a mut Kv<'s>> {
|
||||
if let Kv::Object(fields) = value {
|
||||
return fields.iter_mut().find(|(key, _)| *key == name).map(|(_, field)| field);
|
||||
@@ -334,6 +355,45 @@ fn vector_region_missing_and_empty_paints_remain_distinct() {
|
||||
assert_eq!(network_of(&empty, "Wave").regions[0].fills, Some(Vec::new()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vector_region_linked_fill_uses_style_paints_without_recording_definition() {
|
||||
let linked = solid(0.9, 0.1, 0.2);
|
||||
let own = solid(0.1, 0.2, 0.9);
|
||||
let wire = region_canvas_with_link(Some(own), Some(50), linked);
|
||||
let (doc, _) = import(&wire, "Linked region style").unwrap();
|
||||
let fills = network_of(&doc, "Wave").regions[0].fills.as_ref().unwrap();
|
||||
let figcraft_doc::color::PaintKind::Solid { color } = &fills[0].kind else { panic!("{fills:?}") };
|
||||
assert_eq!((color.r, color.g, color.b), (0.9, 0.1, 0.2));
|
||||
assert!(doc.nodes().all(|node| node.name != "Linked Fill"), "style lookup must not add a style as a layer");
|
||||
let archive = Archive::open(&wire).unwrap();
|
||||
let (schema, message) = decode_canvas(&archive.canvas).unwrap();
|
||||
let message = schema.decode_message(schema.find("Message").unwrap(), &message).unwrap();
|
||||
let (rest, _, _) = map::to_rest(&message, "Linked region style", crate::rest::MAX_NODES);
|
||||
assert!(rest.get("styles").is_none(), "regional link must not populate the layer styles map");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vector_region_variable_bound_own_fill_wins_over_linked_style() {
|
||||
let own =
|
||||
o(vec![("type", e("SOLID")), ("color", o(vec![("r", f(0.1)), ("g", f(0.2)), ("b", f(0.9)), ("a", f(1.0))])), ("colorVar", alias_data(41))]);
|
||||
let (doc, _) = import(®ion_canvas_with_link(Some(own), Some(50), solid(0.9, 0.1, 0.2)), "Bound region style").unwrap();
|
||||
let fills = network_of(&doc, "Wave").regions[0].fills.as_ref().unwrap();
|
||||
let figcraft_doc::color::PaintKind::Solid { color } = &fills[0].kind else { panic!("{fills:?}") };
|
||||
assert_eq!((color.r, color.g, color.b), (0.1, 0.2, 0.9));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vector_region_missing_fills_inherit_and_unresolved_link_keeps_own_fill() {
|
||||
let (inherited, _) = import(®ion_canvas_with_link(None, Some(50), solid(0.9, 0.1, 0.2)), "Inherited linked region").unwrap();
|
||||
assert_eq!(network_of(&inherited, "Wave").regions[0].fills, None);
|
||||
|
||||
let own = solid(0.1, 0.2, 0.9);
|
||||
let (fallback, _) = import(®ion_canvas_with_link(Some(own), Some(999), solid(0.9, 0.1, 0.2)), "Unresolved linked region").unwrap();
|
||||
let fills = network_of(&fallback, "Wave").regions[0].fills.as_ref().unwrap();
|
||||
let figcraft_doc::color::PaintKind::Solid { color } = &fills[0].kind else { panic!("{fills:?}") };
|
||||
assert_eq!((color.r, color.g, color.b), (0.1, 0.2, 0.9));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vector_regions_keep_distinct_solid_paints() {
|
||||
let mut message = tail_message();
|
||||
|
||||
@@ -326,7 +326,7 @@ fn node_outline_at(doc: &Document, n: &Node, depth: usize) -> Option<(BezPath, F
|
||||
Some((p, FillRule::Winding))
|
||||
}
|
||||
NodeKind::Vector { network } => {
|
||||
if network.regions.iter().any(|r| r.fills.is_some()) {
|
||||
if network.regions.iter().any(|r| r.fills.is_some() || r.paint_space.is_some()) {
|
||||
let rounded = network.rounded();
|
||||
let net = rounded.as_ref().unwrap_or(network);
|
||||
let mut areas = BezPath::new();
|
||||
|
||||
@@ -162,6 +162,43 @@ fn styled_vector_mask_unions_region_windings_and_excludes_unpainted_regions() {
|
||||
assert_eq!(px(&pix, 55, 10), [255, 255, 255, 255], "explicit-empty region excluded");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inherited_paint_frames_keep_overlapping_regions_in_vector_masks() {
|
||||
use figcraft_geom::{Affine, Point, Shape};
|
||||
|
||||
let mut d = Document::new("inherited region mask");
|
||||
let page = d.first_page().unwrap();
|
||||
let group = d.create(page, NodeKind::Group, "Mask group", 0.0, 0.0, 40.0, 20.0).unwrap();
|
||||
let mut network = VectorNetwork::default();
|
||||
for x in [0.0, 10.0] {
|
||||
let v = network.vertices.len();
|
||||
let s = network.segments.len();
|
||||
network.vertices.extend([(x, 0.0), (x + 20.0, 0.0), (x + 20.0, 20.0), (x, 20.0)].map(|(x, y)| Vertex::new(x, y)));
|
||||
network.segments.extend((0..4).map(|i| Segment::line(v + i, v + (i + 1) % 4)));
|
||||
network.regions.push(Region {
|
||||
loops: vec![(s..s + 4).collect()],
|
||||
winding: WindingRule::EvenOdd,
|
||||
fills: None,
|
||||
paint_space: Some(figcraft_doc::RegionPaintSpace { width: 20.0, height: 20.0, transform: Affine::translate((x, 0.0)) }),
|
||||
});
|
||||
}
|
||||
let mask = d.create(group, NodeKind::Vector { network }, "Mask", 0.0, 0.0, 30.0, 20.0).unwrap();
|
||||
let n = d.get_mut(mask).unwrap();
|
||||
n.fills = vec![Paint::solid(Color::BLACK)];
|
||||
n.is_mask = true;
|
||||
n.mask_type = figcraft_doc::MaskType::Vector;
|
||||
let (outline, rule) = node_outline(&d, d.node(mask).unwrap()).unwrap();
|
||||
assert_eq!(rule, FillRule::Winding);
|
||||
assert_ne!(outline.winding(Point::new(15.0, 10.0)), 0, "independent inherited regions retain their overlap");
|
||||
let content = d.create(group, NodeKind::Rectangle, "Content", 0.0, 0.0, 40.0, 20.0).unwrap();
|
||||
d.get_mut(content).unwrap().fills = vec![Paint::solid(Color::BLACK)];
|
||||
let pix = render_view(&d, page, &view(40, 20), &Options { background: Some(Color::WHITE), ..Default::default() }).unwrap();
|
||||
for x in [5, 15, 25] {
|
||||
assert_eq!(px(&pix, x, 10), [0, 0, 0, 255]);
|
||||
}
|
||||
assert_eq!(px(&pix, 35, 10), [255, 255, 255, 255]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zoom_scales() {
|
||||
let (d, p, _) = doc_with_rect(Color::BLACK);
|
||||
|
||||
Reference in New Issue
Block a user