mirror of
https://github.com/storytold/figcraft.git
synced 2026-11-12 13:25:29 +00:00
M6: a vector point without a corner radius of its own takes the layer's on .fig import (#2114)
Figma rounds every point whose style doesn't set a corner radius with the layer's cornerRadius. The import read only the point's style, so those vectors came in sharp. Points now fall back to the layer's radius, at the scale of the layer that carries it (#2091), and the network cache keys on it. The export writes an explicit 0 for a sharp point on a rounded vector so it stays sharp.
This commit is contained in:
1 parent
8a7ebc2484
commit
a6be897d31
6 files changed
+123
-5
No files matched your search
@@ -115,3 +115,6 @@ display_name = "Hamza0101"
|
||||
|
||||
[people.ashokram-si]
|
||||
real_name = "Ashok Ram"
|
||||
|
||||
[people.GiovanniBot]
|
||||
sync_github_name = true
|
||||
@@ -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 {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in new issue
Block a user