M8: .fig import: a routed connector takes its line style from the generated record (#1807) (#1813)

* M8: .fig import: a routed connector takes its line style from the generated record (#1807)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* M8: .fig import: a routed connector keeps its own empty paints and dash pattern

CodeRabbit review on #1813: an explicit empty strokePaints (no stroke) or
dashPattern (solid) on the connector no longer takes the generated record's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
This commit is contained in:
Trevor Brown
2026-11-01 14:12:31 -05:00
committed by GitHub
co-authored by Claude Opus 5.5 Brandon Thomas
parent 264bcb1c03
commit bd920fc7de
3 changed files with 87 additions and 2 deletions
@@ -218,6 +218,64 @@ fn connector_mut<'a, 's>(message: &'a mut Kv<'s>, local_id: u64) -> Option<&'a m
})
}
fn set_connector_field(message: &mut Kv<'_>, key: &'static str, value: Kv<'static>) {
let connector = connector_mut(message, 123).expect("synthetic connector");
let Kv::Object(fields) = connector else { panic!("connector object") };
if let Some((_, current)) = fields.iter_mut().find(|(name, _)| *name == key) {
*current = value;
} else {
fields.push((key, value));
}
}
/// A connector without a saved snapshot (a curved or elbowed one is routed here) still draws its
/// line as the generated record says: the paint, weight, cap and dashes live there, not on the
/// connector (#1807).
#[test]
fn a_routed_connector_takes_its_line_style_from_the_generated_record() {
let mut message = synthetic_connector(1.0, false, 0.0);
set_connector_field(&mut message, "connectorLineStyle", Kv::Enum("CURVED"));
set_connector_field(&mut message, "connectorStart", obj(vec![("position", point(10.0, 10.0))]));
set_connector_field(&mut message, "connectorEnd", obj(vec![("position", point(110.0, 60.0))]));
set_override_field(&mut message, "nodeGenerationData", 0, "strokeWeight", scalar(4.0));
set_override_field(&mut message, "nodeGenerationData", 0, "strokeCap", Kv::Enum("ROUND"));
set_override_field(&mut message, "nodeGenerationData", 0, "dashPattern", Kv::Array(vec![scalar(16.0), scalar(10.0)]));
let (rest, warnings, _) = map::to_rest(&message, "synthetic", crate::rest::MAX_NODES);
let connector = &rest["document"]["children"][0]["children"][0];
assert_eq!(connector["type"], "VECTOR");
assert!(connector.get("vectorNetwork").is_some(), "routed here: {connector}");
assert_eq!(connector["strokes"].as_array().map(Vec::len), Some(1), "{connector}");
assert!((connector["strokes"][0]["color"]["b"].as_f64().unwrap() - 0.8).abs() < 1e-6);
assert_eq!(connector["strokeWeight"], 4.0);
assert_eq!(connector["strokeDashes"], serde_json::json!([16.0, 10.0]));
assert_eq!(connector["strokeCapEnd"], "ARROW");
assert!(warnings.iter().any(|warning| warning.contains("follow FigCraft's own routing")), "{warnings:?}");
let (native, _) = crate::rest::import_fig(&rest, &|_| None).unwrap();
let node = find_node(&native, "synthetic connector");
assert_eq!((node.strokes.len(), node.stroke.weight, node.stroke.dashes.clone()), (1, 4.0, vec![16.0, 10.0]));
}
/// A routed connector that writes its own empty paints and dash pattern is meant to have no stroke
/// and a solid line: the generated record's paint and dashes don't replace them.
#[test]
fn a_routed_connector_keeps_its_own_empty_paints_and_dashes() {
let mut message = synthetic_connector(1.0, false, 0.0);
set_connector_field(&mut message, "connectorLineStyle", Kv::Enum("CURVED"));
set_connector_field(&mut message, "connectorStart", obj(vec![("position", point(10.0, 10.0))]));
set_connector_field(&mut message, "connectorEnd", obj(vec![("position", point(110.0, 60.0))]));
set_connector_field(&mut message, "strokePaints", Kv::Array(Vec::new()));
set_connector_field(&mut message, "dashPattern", Kv::Array(Vec::new()));
set_override_field(&mut message, "nodeGenerationData", 0, "strokeWeight", scalar(4.0));
set_override_field(&mut message, "nodeGenerationData", 0, "dashPattern", Kv::Array(vec![scalar(16.0), scalar(10.0)]));
let (rest, _, _) = map::to_rest(&message, "synthetic", crate::rest::MAX_NODES);
let connector = &rest["document"]["children"][0]["children"][0];
assert!(connector.get("vectorNetwork").is_some(), "routed here: {connector}");
assert!(connector["strokes"].as_array().is_none_or(Vec::is_empty), "{connector}");
assert!(connector.get("strokeDashes").is_none(), "{connector}");
assert_eq!(connector["strokeWeight"], 4.0);
}
fn set_override_field(message: &mut Kv<'_>, field: &str, local_id: u64, key: &'static str, value: Kv<'static>) {
let connector = connector_mut(message, 123).expect("synthetic connector");
let section = field_mut(connector, field).expect("connector override section");
+26
View File
@@ -1,4 +1,5 @@
use super::layer::{cap_name, own_text, svg_path, valid_size};
use super::paint::{paint, stroke};
use super::{Affine, Kv, Mapper, NO_STYLE, Obj, Point, Value, View, generated_record, guid, json, matrix, network, num, rows};
use figcraft_doc::geom::kurbo::{BezPath, PathEl, Rect, Shape};
use figcraft_doc::{RouteEnd, Routing, Side, route_points};
@@ -29,6 +30,7 @@ impl<'a> Mapper<'a> {
return Vec::new();
}
let mid = self.connector(o, v, space);
self.connector_line_style(o, v);
if shape.label {
return self.connector_label(o, v, rest_id, mid);
}
@@ -122,6 +124,30 @@ impl<'a> Mapper<'a> {
middle(&path).map(|m| m - b.origin().to_vec2())
}
/// The line's paint, weight, cap and dashes, which a connector keeps in its generated line
/// record rather than on itself: without a saved snapshot the routed line took none of them and
/// drew nothing (#1807). The connector's own values, when it has any, win.
fn connector_line_style(&mut self, o: &mut Obj, v: &View<'a>) {
let Some(line_style) = generated_record(v, "nodeGenerationData", 0) else { return };
let style = View::of(line_style);
// A connector that writes its own (even empty) paints or dash pattern keeps them: an empty
// list is no stroke, or a solid line, not a missing value.
if v.get("strokePaints").is_none() && o.get("strokes").and_then(Value::as_array).is_none_or(Vec::is_empty) {
let strokes: Vec<Value> = style.list("strokePaints").iter().filter_map(|p| paint(p, &mut |w| self.warn(w))).collect();
if !strokes.is_empty() {
o.insert("strokes".into(), Value::Array(strokes));
}
}
let mut line = Obj::new();
stroke(&mut line, &style);
if v.get("dashPattern").is_some() {
line.remove("strokeDashes");
}
for (key, value) in line {
o.entry(key).or_insert(value);
}
}
/// A connector with a label but no saved snapshot of it, as the frame a saved one imports as:
/// `o` keeps the connector's place and size and gives its line to a vector child, and the
/// label is a text child centred on `mid` (the line's middle; the frame's centre when the line