M4: preserve inherited region paints and linked fill styles

This commit is contained in:
Skyfuller1303
2026-10-26 21:33:56 +01:00
parent a4076caaf4
commit 7f707ecca3
10 changed files with 306 additions and 42 deletions
+16
View File
@@ -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");
+1 -1
View File
@@ -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 {
+48 -28
View File
@@ -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()))?;
+102 -10
View File
@@ -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();
+1 -1
View File
@@ -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();
+27
View File
@@ -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 {
+13 -1
View File
@@ -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())
+60
View File
@@ -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(&region_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(&region_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(&region_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();
+1 -1
View File
@@ -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();
+37
View File
@@ -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);