mirror of
https://github.com/storytold/figcraft.git
synced 2026-11-08 16:30:07 +00:00
Merge pull request #2123 from FredApps/fix/scaled-vector-corners-upstream-2097
M6: scale per-point vector radii in scaled instances
This commit is contained in:
@@ -58,6 +58,8 @@ mod text_style_tests;
|
||||
mod text_wrap_tests;
|
||||
#[cfg(test)]
|
||||
mod value_budget_tests;
|
||||
#[cfg(test)]
|
||||
mod vector_scale_tests;
|
||||
|
||||
use std::cell::Cell;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
@@ -356,8 +356,8 @@ 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 what a network is made from: its blob, its scale and its style table.
|
||||
network_ids: HashMap<(usize, u64, u64, usize), usize>,
|
||||
/// Index in `networks` by its blob, position scale, style table and point-radius scale.
|
||||
network_ids: HashMap<(usize, u64, u64, usize, 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,
|
||||
|
||||
@@ -628,7 +628,7 @@ impl<'a> Mapper<'a> {
|
||||
/// scaled to the layer's size, with per-point corner radii, handle mirroring and end caps.
|
||||
/// False when there's none or it's damaged (the outlines stand in).
|
||||
fn network(&mut self, o: &mut Obj, v: &View<'a>) -> bool {
|
||||
let Some(vd) = v.get("vectorData") else { return false };
|
||||
let Some((vd, radius_scale)) = v.find("vectorData") else { return false };
|
||||
let Some(blob_id) = num(vd, "vectorNetworkBlob").map(|i| i as usize) else { return false };
|
||||
let Some(blob) = self.blobs.get(blob_id).copied() else { return false };
|
||||
let Some(net) = network::decode(blob) else {
|
||||
@@ -674,7 +674,9 @@ impl<'a> Mapper<'a> {
|
||||
.map(|p| {
|
||||
let mut out = json!({"x": p.x * sx, "y": p.y * sy});
|
||||
let st = style(p.style);
|
||||
if let Some(r) = st.and_then(|s| num(s, "cornerRadius")).filter(|r| *r > 0.0) {
|
||||
// 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) {
|
||||
out["radius"] = json!(r);
|
||||
}
|
||||
match st.and_then(|s| s.get("handleMirroring")).and_then(Kv::str) {
|
||||
@@ -790,7 +792,8 @@ impl<'a> Mapper<'a> {
|
||||
o.insert("vectorNetwork".into(), entry);
|
||||
return true;
|
||||
}
|
||||
let key = (blob_id, sx.to_bits(), sy.to_bits(), std::ptr::from_ref(vd) as usize);
|
||||
// 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 id = match self.network_ids.get(&key) {
|
||||
Some(id) => *id,
|
||||
None => {
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
//! Per-point radii follow the scale of their vector-data row, not the resized layer's box.
|
||||
|
||||
use figcraft_doc::{Document, Node, NodeKind, VectorNetwork};
|
||||
|
||||
use super::kiwi::Kv;
|
||||
use super::network::{self, Network, Region, Segment, Vertex};
|
||||
use super::tests::{arr, at, canvas, e, f, g, node, o, schema, u, xy};
|
||||
|
||||
fn vector_data(radius: f64) -> Kv<'static> {
|
||||
o(vec![
|
||||
("vectorNetworkBlob", u(0)),
|
||||
("normalizedSize", xy(100.0, 100.0)),
|
||||
(
|
||||
"styleOverrideTable",
|
||||
arr(vec![o(vec![("styleID", u(1)), ("cornerRadius", f(radius))]), o(vec![("styleID", u(2)), ("cornerRadius", f(0.0))])]),
|
||||
),
|
||||
])
|
||||
}
|
||||
|
||||
fn source() -> Vec<Kv<'static>> {
|
||||
vec![
|
||||
node(0, None, "DOCUMENT", "D", vec![]),
|
||||
node(1, Some((0, "!")), "CANVAS", "P", vec![]),
|
||||
node(5, Some((1, "!")), "SYMBOL", "Main", vec![("size", xy(100.0, 100.0))]),
|
||||
node(
|
||||
6,
|
||||
Some((5, "!")),
|
||||
"VECTOR",
|
||||
"Triangle",
|
||||
vec![
|
||||
("size", xy(100.0, 100.0)),
|
||||
("vectorData", vector_data(8.0)),
|
||||
("horizontalConstraint", e("SCALE")),
|
||||
("verticalConstraint", e("SCALE")),
|
||||
],
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
fn instance(id: u64, parent: u64, main: u64, name: &str, size: f64, factor: Option<f64>, rows: Vec<Kv<'static>>) -> Kv<'static> {
|
||||
let mut symbol = vec![("symbolID", g(main)), ("symbolOverrides", arr(rows))];
|
||||
if let Some(factor) = factor {
|
||||
symbol.push(("uniformScaleFactor", f(factor)));
|
||||
}
|
||||
node(id, Some((parent, "!")), "INSTANCE", name, vec![("size", xy(size, size)), ("transform", at(200.0, 0.0)), ("symbolData", o(symbol))])
|
||||
}
|
||||
|
||||
fn row(fields: Vec<(&'static str, Kv<'static>)>) -> Kv<'static> {
|
||||
let mut values = vec![("guidPath", o(vec![("guids", arr(vec![g(6)]))]))];
|
||||
values.extend(fields);
|
||||
o(values)
|
||||
}
|
||||
|
||||
fn import(nodes: Vec<Kv<'static>>) -> Document {
|
||||
let network = Network {
|
||||
vertices: vec![Vertex { style: 1, x: 0.0, y: 0.0 }, Vertex { style: 1, x: 100.0, y: 0.0 }, Vertex { style: 2, x: 0.0, y: 100.0 }],
|
||||
segments: (0..3).map(|a| Segment { style: 0, a, b: (a + 1) % 3, ta: (0.0, 0.0), tb: (0.0, 0.0) }).collect(),
|
||||
regions: vec![Region { style: 0, non_zero: false, loops: vec![vec![0, 1, 2]] }],
|
||||
};
|
||||
let message = o(vec![("nodeChanges", arr(nodes)), ("blobs", arr(vec![o(vec![("bytes", Kv::Bytes(network::encode(&network)))])]))]);
|
||||
super::import(&canvas(&schema(), &message), "Scaled vectors").unwrap().0
|
||||
}
|
||||
|
||||
fn child<'a>(doc: &'a Document, name: &str) -> &'a Node {
|
||||
let parent = doc.nodes().find(|node| node.name == name).unwrap();
|
||||
doc.node(parent.children[0]).unwrap()
|
||||
}
|
||||
|
||||
fn network(node: &Node) -> &VectorNetwork {
|
||||
let NodeKind::Vector { network } = &node.kind else { panic!("not a vector") };
|
||||
network
|
||||
}
|
||||
|
||||
fn assert_vector(node: &Node, size: f64, radius: f64) {
|
||||
assert_eq!((node.width, node.height), (size, size));
|
||||
let vector = network(node);
|
||||
assert_eq!((vector.vertices[1].x, vector.vertices[2].y), (size, size));
|
||||
assert_eq!(vector.vertices.iter().map(|point| point.radius).collect::<Vec<_>>(), [radius, radius, 0.0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vector_corner_radii_follow_uniform_scale_without_changing_the_main() {
|
||||
for (factor, size, radius) in [(None, 100.0, 8.0), (Some(1.0), 100.0, 8.0), (Some(0.5), 50.0, 4.0), (Some(2.0), 200.0, 16.0)] {
|
||||
let mut nodes = source();
|
||||
nodes.push(instance(9, 1, 5, "Copy", size, factor, vec![]));
|
||||
let doc = import(nodes);
|
||||
assert_vector(child(&doc, "Main"), 100.0, 8.0);
|
||||
assert_vector(child(&doc, "Copy"), size, radius);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vector_corner_radii_from_override_and_derived_rows_are_not_scaled_twice() {
|
||||
for derived in [false, true] {
|
||||
let mut nodes = source();
|
||||
let rows = vec![row(vec![("vectorData", vector_data(3.0))])];
|
||||
let mut copy = instance(9, 1, 5, "Copy", 50.0, Some(0.5), if derived { vec![] } else { rows.clone() });
|
||||
if derived {
|
||||
let Kv::Object(fields) = &mut copy else { panic!() };
|
||||
fields.push(("derivedSymbolData", arr(rows)));
|
||||
}
|
||||
nodes.push(copy);
|
||||
let doc = import(nodes);
|
||||
assert_vector(child(&doc, "Copy"), 50.0, 3.0);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vector_corner_radii_still_scale_when_only_the_size_is_derived() {
|
||||
let mut nodes = source();
|
||||
let mut copy = instance(9, 1, 5, "Copy", 50.0, Some(0.5), vec![]);
|
||||
let Kv::Object(fields) = &mut copy else { panic!() };
|
||||
fields.push(("derivedSymbolData", arr(vec![row(vec![("size", xy(50.0, 50.0))])])));
|
||||
nodes.push(copy);
|
||||
let doc = import(nodes);
|
||||
assert_vector(child(&doc, "Copy"), 50.0, 4.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nested_vector_corner_radii_take_the_scale_of_their_own_row() {
|
||||
for own_row in [false, true] {
|
||||
let mut nodes = source();
|
||||
nodes.push(node(20, Some((1, "!")), "SYMBOL", "Wrapper", vec![("size", xy(50.0, 50.0))]));
|
||||
let rows = if own_row { vec![row(vec![("vectorData", vector_data(6.0))])] } else { vec![] };
|
||||
nodes.push(instance(21, 20, 5, "Inner", 50.0, Some(0.5), rows));
|
||||
nodes.push(instance(30, 1, 20, "Outer", 25.0, Some(0.5), vec![]));
|
||||
let doc = import(nodes);
|
||||
let inner = child(&doc, "Outer");
|
||||
assert_vector(doc.node(inner.children[0]).unwrap(), 25.0, if own_row { 3.0 } else { 2.0 });
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn equally_sized_vectors_with_different_scale_factors_do_not_share_radii() {
|
||||
for scaled_first in [false, true] {
|
||||
let mut nodes = source();
|
||||
// Equal sort positions are ordered by GUID, so vary IDs to exercise both cache orders.
|
||||
nodes.push(instance(if scaled_first { 8 } else { 9 }, 1, 5, "Scaled", 50.0, Some(0.5), vec![]));
|
||||
nodes.push(instance(if scaled_first { 9 } else { 8 }, 1, 5, "Resized", 50.0, None, vec![]));
|
||||
let doc = import(nodes);
|
||||
assert_vector(child(&doc, "Scaled"), 50.0, 4.0);
|
||||
assert_vector(child(&doc, "Resized"), 50.0, 8.0);
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user