Merge pull request #2115 from GiovanniBot/fix/vector-node-corner-radius

M6: a vector point without a corner radius of its own takes the layer's on .fig import (#2114)
This commit is contained in:
BFlat authored and GitHub committed 2026-11-12 09:33:43 +07:00
commit 0dddfe1a65
6 files changed
+123 -5

No files matched your search

+3
View File
@@ -115,3 +115,6 @@ display_name = "Hamza0101"
[people.ashokram-si]
real_name = "Ashok Ram"
[people.GiovanniBot]
sync_github_name = true
+3 -1
View File
@@ -207,7 +207,9 @@ impl<'d> Exporter<'d> {
.enumerate()
.map(|(i, v)| {
let mut st: Fields = Vec::new();
if v.radius > 0.0 {
// A point without a radius of its own is drawn with the layer's, so a sharp
// point on a rounded layer says so.
if v.radius > 0.0 || n.corners.radii[0] > 0.0 {
st.push(("cornerRadius", fl(v.radius)));
}
if let Some(m) = v.mirror {
+19
View File
@@ -955,6 +955,25 @@ fn closed_vector_geometry_is_written_with_its_corner_smoothing() {
assert_eq!(radii, vec![20.0, 0.0, 10.0, 0.0], "each point keeps its own radius");
}
#[test]
fn sharp_points_on_a_rounded_vector_stay_sharp() {
// A point without a radius takes the layer's when read, so a sharp point on a vector with a
// layer radius is written with its own zero.
let mut doc = Document::new("sharp point");
let page = doc.first_page().unwrap();
let mut network = VectorNetwork::from_points(&[(0.0, 0.0), (100.0, 0.0), (100.0, 100.0), (0.0, 100.0)].map(|(x, y)| Point::new(x, y)), true);
for (v, r) in network.vertices.iter_mut().zip([8.0, 0.0, 8.0, 0.0]) {
v.radius = r;
}
let id = doc.create(page, NodeKind::Vector { network }, "V", 0.0, 0.0, 100.0, 100.0).unwrap();
doc.node_mut(id).unwrap().corners.radii = [8.0; 4];
let (back, _) = round_trip(&doc, "sharp point");
let v = back.nodes().find(|n| matches!(n.kind, NodeKind::Vector { .. })).unwrap();
let NodeKind::Vector { network } = &v.kind else { panic!("vector") };
let radii: Vec<f64> = network.vertices.iter().map(|x| x.radius).collect();
assert_eq!(radii, vec![8.0, 0.0, 8.0, 0.0]);
}
#[test]
fn the_written_schema_parses_and_round_trips() {
let schema = Schema::parse(export::SCHEMA).unwrap();
+3 -2
View File
@@ -356,8 +356,9 @@ struct Mapper<'a> {
/// holding it) name it by `vectorNetworkRef`. Kept out of the JSON, where a large file's
/// networks took gigabytes.
networks: Vec<Option<VectorNetwork>>,
/// Index in `networks` by its blob, position scale, style table and point-radius scale.
network_ids: HashMap<(usize, u64, u64, usize, u64), usize>,
/// Index in `networks` by its blob, position scale, style table, point-radius scale and the
/// layer's corner radius (which points without one of their own take).
network_ids: HashMap<(usize, u64, u64, usize, u64, u64), usize>,
/// Region paint elements (paints and gradient stops) read so far: one budget for the whole
/// file, since layers with region paints don't share their network.
region_paint_elements: usize,
+5 -2
View File
@@ -668,6 +668,8 @@ impl<'a> Mapper<'a> {
size.zip(norm).filter(|(s, n)| s.is_finite() && *n > 1e-9).map_or(1.0, |(s, n)| s / n)
};
let (sx, sy) = (scale("x"), scale("y"));
// A point whose style doesn't set a radius takes the layer's, as the editor draws it.
let layer_radius = v.len("cornerRadius");
let vertices: Vec<Value> = net
.vertices
.iter()
@@ -676,7 +678,7 @@ impl<'a> Mapper<'a> {
let st = style(p.style);
// Point radii are lengths in this row's space, unlike the normalized positions.
// An override/derived row already carries the instance's scale.
if let Some(r) = st.and_then(|s| num(s, "cornerRadius")).and_then(|r| scaled(r, radius_scale)).filter(|r| *r > 0.0) {
if let Some(r) = st.and_then(|s| num(s, "cornerRadius")).and_then(|r| scaled(r, radius_scale)).or(layer_radius).filter(|r| *r > 0.0) {
out["radius"] = json!(r);
}
match st.and_then(|s| s.get("handleMirroring")).and_then(Kv::str) {
@@ -793,7 +795,8 @@ impl<'a> Mapper<'a> {
return true;
}
// A resized instance and a K-scaled one can have the same box but different radii.
let key = (blob_id, sx.to_bits(), sy.to_bits(), std::ptr::from_ref(vd) as usize, radius_scale.to_bits());
let key =
(blob_id, sx.to_bits(), sy.to_bits(), std::ptr::from_ref(vd) as usize, radius_scale.to_bits(), layer_radius.unwrap_or(0.0).to_bits());
let id = match self.network_ids.get(&key) {
Some(id) => *id,
None => {
+90
View File
@@ -521,6 +521,96 @@ fn network_of<'d>(doc: &'d figcraft_doc::Document, name: &str) -> &'d figcraft_d
}
}
#[test]
fn vector_points_without_a_radius_of_their_own_take_the_layers() {
// A vector rounded from the layer's corner radius: the editor rounds every point whose
// style doesn't set a radius (no style, or one without `cornerRadius`) with the layer's,
// while a point's own radius, zero included, wins.
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 Kv::Object(fields) = wave else { panic!() };
fields.push(("cornerRadius", f(6.0)));
let vector_data = field_mut(wave, "vectorData").unwrap();
let Kv::Array(styles) = field_mut(vector_data, "styleOverrideTable").unwrap() else { panic!() };
let Kv::Object(sharp) = styles.get_mut(2).unwrap() else { panic!() };
sharp.push(("cornerRadius", f(0.0)));
let (doc, _) = import(&canvas(&schema(), &message), "Rounded").unwrap();
let radii: Vec<f64> = network_of(&doc, "Wave").vertices.iter().map(|v| v.radius).collect();
assert_eq!(radii, [6.0, 4.0, 6.0, 0.0], "no style, own radius, style without one, own zero");
// Without a layer radius, the points without one of their own stay sharp.
let (doc, _) = import(&tail_canvas(), "Tail").unwrap();
let radii: Vec<f64> = network_of(&doc, "Wave").vertices.iter().map(|v| v.radius).collect();
assert_eq!(radii, [0.0, 4.0, 0.0, 0.0]);
}
#[test]
fn an_instance_that_rounds_a_vector_keeps_its_own_points() {
// An instance's copy shares its main's vector data; overriding the layer's radius must not
// reuse the main's network, nor hand the instance's to the main.
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();
*field_mut(wave, "parentIndex").unwrap() = o(vec![("guid", g(50)), ("position", s("!"))]);
*field_mut(wave, "transform").unwrap() = at(0.0, 0.0);
let rounded = o(vec![("guidPath", o(vec![("guids", arr(vec![g(30)]))])), ("cornerRadius", f(6.0))]);
nodes.push(node(50, Some((1, "~")), "SYMBOL", "Mark", vec![("size", xy(200.0, 100.0)), ("transform", at(700.0, 0.0))]));
nodes.push(node(
51,
Some((1, "~!")),
"INSTANCE",
"Rounded mark",
vec![
("size", xy(200.0, 100.0)),
("transform", at(700.0, 400.0)),
("symbolData", o(vec![("symbolID", g(50)), ("symbolOverrides", arr(vec![rounded]))])),
],
));
let (doc, _) = import(&canvas(&schema(), &message), "Instance").unwrap();
let radii_under = |parent: &str| -> Vec<f64> {
let p = named(&doc, parent);
let wave = p.children.iter().map(|c| doc.node(*c).unwrap()).find(|n| n.name == "Wave").unwrap();
let NodeKind::Vector { network } = &wave.kind else { panic!() };
network.vertices.iter().map(|v| v.radius).collect()
};
assert_eq!(radii_under("Mark"), [0.0, 4.0, 0.0, 0.0]);
assert_eq!(radii_under("Rounded mark"), [6.0, 4.0, 6.0, 6.0]);
}
#[test]
fn a_scaled_instance_scales_its_vectors_point_radii() {
// Point radii are lengths: an instance scaled with the Scale tool draws its main's, from a
// point's own style or from the layer, times the factor (#2091).
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();
*field_mut(wave, "parentIndex").unwrap() = o(vec![("guid", g(50)), ("position", s("!"))]);
*field_mut(wave, "transform").unwrap() = at(0.0, 0.0);
let Kv::Object(fields) = wave else { panic!() };
fields.push(("cornerRadius", f(3.0)));
nodes.push(node(50, Some((1, "~")), "SYMBOL", "Mark", vec![("size", xy(200.0, 100.0)), ("transform", at(700.0, 0.0))]));
nodes.push(node(
51,
Some((1, "~!")),
"INSTANCE",
"Big mark",
vec![
("size", xy(400.0, 200.0)),
("transform", at(700.0, 400.0)),
("symbolData", o(vec![("symbolID", g(50)), ("uniformScaleFactor", f(2.0))])),
],
));
let (doc, _) = import(&canvas(&schema(), &message), "Scaled").unwrap();
let radii_under = |parent: &str| -> Vec<f64> {
let p = named(&doc, parent);
let wave = p.children.iter().map(|c| doc.node(*c).unwrap()).find(|n| n.name == "Wave").unwrap();
let NodeKind::Vector { network } = &wave.kind else { panic!() };
network.vertices.iter().map(|v| v.radius).collect()
};
assert_eq!(radii_under("Mark"), [3.0, 4.0, 3.0, 3.0]);
assert_eq!(radii_under("Big mark"), [6.0, 8.0, 6.0, 6.0]);
}
#[test]
fn vectors_import_as_editable_networks() {
let (doc, warnings) = import(&tail_canvas(), "Tail").unwrap();