From d9e79d604b8d34fbea7eff367e90afdc67868c51 Mon Sep 17 00:00:00 2001 From: Skyfuller1303 Date: Fri, 16 Oct 2026 02:34:42 +0200 Subject: [PATCH] M1: distinguish unresolved vector segment style metadata in .fig import The vector network decoder now keeps each segment's style id. Import reports a segment style that names a row of the layer's style table as metadata that was not interpreted, and one that names no row as a reference to a missing style entry, each at most once per file. The earlier note claimed that stroke styles were left out, which overstated what is known about the metadata. Segment geometry, paint and width are unchanged, and the style ids are not kept in the native vector model. --- crates/format/src/fig/export.rs | 8 +++- crates/format/src/fig/map.rs | 25 ++++++++--- crates/format/src/fig/network.rs | 11 ++--- crates/format/src/fig/tail_tests.rs | 64 ++++++++++++++++++++++------- 4 files changed, 83 insertions(+), 25 deletions(-) diff --git a/crates/format/src/fig/export.rs b/crates/format/src/fig/export.rs index bfd0b615..7443c8de 100644 --- a/crates/format/src/fig/export.rs +++ b/crates/format/src/fig/export.rs @@ -1669,7 +1669,13 @@ impl<'d> Exporter<'d> { let segments = net .segments .iter() - .map(|sg| network::Segment { a: sg.a, ta: (finite(sg.ta.0), finite(sg.ta.1)), b: sg.b, tb: (finite(sg.tb.0), finite(sg.tb.1)) }) + .map(|sg| network::Segment { + style: 0, + a: sg.a, + ta: (finite(sg.ta.0), finite(sg.ta.1)), + b: sg.b, + tb: (finite(sg.tb.0), finite(sg.tb.1)), + }) .collect(); let regions = net .regions diff --git a/crates/format/src/fig/map.rs b/crates/format/src/fig/map.rs index 0fe44662..36f45f31 100644 --- a/crates/format/src/fig/map.rs +++ b/crates/format/src/fig/map.rs @@ -1192,12 +1192,29 @@ impl<'a> Mapper<'a> { fn network(&mut self, o: &mut Obj, v: &View<'a>) -> bool { let Some(vd) = v.get("vectorData") else { return false }; let Some(blob) = num(vd, "vectorNetworkBlob").and_then(|i| self.blobs.get(i as usize)).copied() else { return false }; - let Some((net, styled_segments)) = network::decode_styled(blob) else { + let Some((net, _)) = network::decode_styled(blob) else { self.warn("damaged vector networks import as outlines"); return false; }; - if styled_segments > 0 { - self.warn("stroke styles of single vector network segments were left out"); + let styles: BTreeMap = + vd.get("styleOverrideTable").map(Kv::list).unwrap_or_default().iter().filter_map(|s| Some((num(s, "styleID")? as u64, s))).collect(); + let mut has_uninterpreted = false; + let mut has_missing = false; + for segment in &net.segments { + if segment.style == 0 { + continue; + } + if styles.contains_key(&u64::from(segment.style)) { + has_uninterpreted = true; + } else { + has_missing = true; + } + } + if has_uninterpreted { + self.warn("style settings of some vector segments aren't supported; those segments use the layer's stroke"); + } + if has_missing { + self.warn("some vector segments refer to style settings missing from the file; they use the layer's stroke"); } // An open path shows its fill, which regions can't hold: the outline keeps the look. let fills_shown = @@ -1205,8 +1222,6 @@ impl<'a> Mapper<'a> { if net.segments.is_empty() || (net.regions.is_empty() && fills_shown && o.contains_key("fillGeometry")) { return false; } - let styles: BTreeMap = - vd.get("styleOverrideTable").map(Kv::list).unwrap_or_default().iter().filter_map(|s| Some((num(s, "styleID")? as u64, s))).collect(); let style = |id: u32| styles.get(&u64::from(id)).copied().filter(|_| id != 0); let scale = |k: &str| { let size = v.size().map(|s| if k == "x" { s.0 } else { s.1 }); diff --git a/crates/format/src/fig/network.rs b/crates/format/src/fig/network.rs index d251dff1..4e8c3ad7 100644 --- a/crates/format/src/fig/network.rs +++ b/crates/format/src/fig/network.rs @@ -17,6 +17,7 @@ pub(super) struct Vertex { } pub(super) struct Segment { + pub style: u32, pub a: usize, pub ta: (f64, f64), pub b: usize, @@ -70,8 +71,7 @@ pub(super) fn decode(b: &[u8]) -> Option { decode_styled(b).map(|(net, _)| net) } -/// The network in blob `b` and how many of its segments have a style id of their own (per-segment -/// stroke overrides, which the document model can't hold); `None` as for `decode`. +/// The network in blob `b` and how many of its segments have a nonzero style id; `None` as for `decode`. pub(super) fn decode_styled(b: &[u8]) -> Option<(Network, usize)> { let mut r = Reader { b, pos: 0 }; let (nv, ns, nr) = (r.u32()? as usize, r.u32()? as usize, r.u32()? as usize); @@ -84,10 +84,11 @@ pub(super) fn decode_styled(b: &[u8]) -> Option<(Network, usize)> { } let mut styled = 0usize; for _ in 0..ns { - if r.u32()? != 0 { + let style = r.u32()?; + if style != 0 { styled += 1; } - net.segments.push(Segment { a: r.index(nv)?, ta: (r.f32()?, r.f32()?), b: r.index(nv)?, tb: (r.f32()?, r.f32()?) }); + net.segments.push(Segment { style, a: r.index(nv)?, ta: (r.f32()?, r.f32()?), b: r.index(nv)?, tb: (r.f32()?, r.f32()?) }); } let mut entries = 0usize; for _ in 0..nr { @@ -120,7 +121,7 @@ pub(super) fn encode(net: &Network) -> Vec { fl(v.y, &mut out); } for s in &net.segments { - u(0, &mut out); + u(s.style, &mut out); u(s.a as u32, &mut out); fl(s.ta.0, &mut out); fl(s.ta.1, &mut out); diff --git a/crates/format/src/fig/tail_tests.rs b/crates/format/src/fig/tail_tests.rs index 5d34f23b..bc4f31cc 100644 --- a/crates/format/src/fig/tail_tests.rs +++ b/crates/format/src/fig/tail_tests.rs @@ -9,7 +9,7 @@ use super::super::network::{self, Network, Region, Segment, Vertex}; use super::*; fn seg(a: usize, b: usize, ta: (f64, f64), tb: (f64, f64)) -> Segment { - Segment { a, ta, b, tb } + Segment { style: 0, a, ta, b, tb } } fn vtx(style: u32, x: f64, y: f64) -> Vertex { @@ -264,21 +264,33 @@ fn vectors_import_as_editable_networks() { #[test] fn per_segment_stroke_styles_are_reported() { - let mut blob = square_blob(); - // The first segment's style id follows the counts and the vertices (12 bytes each). - blob[12 + 4 * 12..12 + 4 * 12 + 4].copy_from_slice(&7u32.to_le_bytes()); - let (net, styled) = network::decode_styled(&blob).unwrap(); - assert_eq!((net.segments.len(), styled), (4, 1)); + let styled_import = |segment_style: u32| { + let mut blob = square_blob(); + // The first segment's style id follows the counts and the vertices (12 bytes each). + blob[12 + 4 * 12..12 + 4 * 12 + 4].copy_from_slice(&segment_style.to_le_bytes()); + let (net, styled) = network::decode_styled(&blob).unwrap(); + assert_eq!((net.segments.len(), net.segments[0].style, styled), (4, segment_style, 1)); + let mut message = tail_message(); + let Kv::Object(fields) = &mut message else { panic!() }; + let Kv::Array(blobs) = &mut fields.iter_mut().find(|(key, _)| *key == "blobs").unwrap().1 else { panic!() }; + blobs[1] = o(vec![("bytes", Kv::Bytes(blob))]); + let (doc, warnings) = import(&canvas(&schema(), &message), "Tail").unwrap(); + assert_eq!(network_of(&doc, "Wave").segments.len(), 4); + warnings + }; assert_eq!(network::decode_styled(&square_blob()).unwrap().1, 0); let plain = import(&tail_canvas(), "Tail").unwrap().1; - assert!(!plain.iter().any(|w| w.contains("stroke styles of single")), "{plain:?}"); - let mut message = tail_message(); - let Kv::Object(fields) = &mut message else { panic!() }; - let Kv::Array(blobs) = &mut fields.iter_mut().find(|(key, _)| *key == "blobs").unwrap().1 else { panic!() }; - blobs[1] = o(vec![("bytes", Kv::Bytes(blob))]); - let (doc, warnings) = import(&canvas(&schema(), &message), "Tail").unwrap(); - assert!(warnings.iter().any(|w| w.contains("stroke styles of single vector network segments were left out")), "{warnings:?}"); - assert_eq!(network_of(&doc, "Wave").segments.len(), 4); + assert!(!plain.iter().any(|w| w.contains("vector segments")), "{plain:?}"); + let uninterpreted = |w: &String| w.contains("style settings of some vector segments aren't supported; those segments use the layer's stroke"); + let missing = |w: &String| w.contains("some vector segments refer to style settings missing from the file; they use the layer's stroke"); + // Style 1 has a row in the layer's override table; its contents are not applied. + let known = styled_import(1); + assert_eq!(known.iter().filter(|w| uninterpreted(w)).count(), 1, "{known:?}"); + assert!(!known.iter().any(missing), "{known:?}"); + // Style 7 names no row at all. + let unknown = styled_import(7); + assert_eq!(unknown.iter().filter(|w| missing(w)).count(), 1, "{unknown:?}"); + assert!(!unknown.iter().any(uninterpreted), "{unknown:?}"); } #[test] @@ -385,6 +397,30 @@ fn damaged_network_blobs_never_panic() { } } +#[test] +fn segment_styles_round_trip_without_changing_geometry() { + let original = Network { + vertices: vec![vtx(0, 2.0, 3.0), vtx(0, 10.0, 20.0), vtx(0, 40.0, 50.0), vtx(0, 60.0, 70.0)], + segments: vec![ + seg(0, 1, (1.0, 2.0), (3.0, 4.0)), + Segment { style: 17, a: 1, ta: (5.0, 6.0), b: 2, tb: (7.0, 8.0) }, + Segment { style: 27, a: 2, ta: (9.0, 10.0), b: 3, tb: (11.0, 12.0) }, + ], + regions: vec![], + }; + let bytes = network::encode(&original); + let (decoded, styled) = network::decode_styled(&bytes).unwrap(); + assert_eq!(styled, 2); + assert_eq!(decoded.segments.len(), 3); + for (got, want) in decoded.segments.iter().zip(&original.segments) { + assert_eq!(got.style, want.style); + assert_eq!((got.a, got.ta, got.b, got.tb), (want.a, want.ta, want.b, want.tb)); + } + for cut in 0..bytes.len() { + assert!(network::decode_styled(&bytes[..cut]).is_none(), "truncated at {cut}"); + } +} + #[test] fn mangled_tail_files_never_panic() { let c = tail_canvas();