diff --git a/crates/io/src/dxf_ext.rs b/crates/io/src/dxf_ext.rs index 3adafabe9..162d3e37e 100644 --- a/crates/io/src/dxf_ext.rs +++ b/crates/io/src/dxf_ext.rs @@ -365,6 +365,25 @@ pub(crate) fn read_frozen(tags: &[Tag]) -> Vec { list.iter().take(MAX_XDATA_ITEMS).filter(|t| t.code == 1000).map(Tag::str).collect() } +/// A viewport's layer colour overrides as CADCraft xdata: `1000 VPCOLORS`, `1002 {`, then per override +/// `1000 layer` and `1000 colour` (the [`Color::name`] text, which keeps true colours), `1002 }`. +pub(crate) fn layer_colors_xdata(colors: &[(String, Color)]) -> Vec { + let mut v = vec![Tag::s(1000, "VPCOLORS"), Tag::s(1002, "{")]; + for (layer, color) in colors { + v.push(Tag::s(1000, layer.clone())); + v.push(Tag::s(1000, color.name())); + } + v.push(Tag::s(1002, "}")); + v +} + +/// The overrides written by [`layer_colors_xdata`]; empty for files from other writers, bad pairs are skipped. +pub(crate) fn read_layer_colors(tags: &[Tag]) -> Vec<(String, Color)> { + let list = xdata_list(xdata(tags, APP), "VPCOLORS"); + let names: Vec = list.iter().take(MAX_XDATA_ITEMS * 2).filter(|t| t.code == 1000).map(Tag::str).collect(); + names.chunks_exact(2).filter_map(|p| Some((p.first()?.clone(), Color::parse(p.get(1)?)?))).collect() +} + const POINT_NAMES: [&str; 5] = ["defpt", "p13", "p14", "p15", "p16"]; /// Associativity links from CADCraft xdata; malformed links are skipped. diff --git a/crates/io/src/dxf_read.rs b/crates/io/src/dxf_read.rs index f4a06915d..f87381910 100644 --- a/crates/io/src/dxf_read.rs +++ b/crates/io/src/dxf_read.rs @@ -307,7 +307,7 @@ fn entity(kind: &str, tags: &[Tag]) -> Option<(Common, EntityKind)> { id: t.i(69).unwrap_or(0) as u32, locked: t.i(90).unwrap_or(0) & 16384 != 0, frozen_layers: crate::dxf_ext::read_frozen(tags), - layer_colors: Vec::new(), + layer_colors: crate::dxf_ext::read_layer_colors(tags), }), "WIPEOUT" => { let o = t.p(10); diff --git a/crates/io/src/dxf_write.rs b/crates/io/src/dxf_write.rs index f24f063f3..5f999e23d 100644 --- a/crates/io/src/dxf_write.rs +++ b/crates/io/src/dxf_write.rs @@ -792,9 +792,15 @@ fn entity(w: &mut W, d: &Drawing, e: &Entity, owner: &str, paper: bool, cx: &Ctx w.f(45, v.view_height); // Status flags: 16384 = display locked. w.i(90, if v.locked { 16384 } else { 0 }); - if !v.frozen_layers.is_empty() { + // One CADCraft xdata group holds both lists: readers stop at the first group of an app. + if !v.frozen_layers.is_empty() || !v.layer_colors.is_empty() { w.s(1001, dxf_ext::APP); - w.xdata(dxf_ext::frozen_xdata(&v.frozen_layers)); + if !v.frozen_layers.is_empty() { + w.xdata(dxf_ext::frozen_xdata(&v.frozen_layers)); + } + if !v.layer_colors.is_empty() { + w.xdata(dxf_ext::layer_colors_xdata(&v.layer_colors)); + } } } EntityKind::Wipeout(wo) => { diff --git a/crates/io/src/tests.rs b/crates/io/src/tests.rs index a473503d5..e2da8fa0d 100644 --- a/crates/io/src/tests.rs +++ b/crates/io/src/tests.rs @@ -571,6 +571,43 @@ fn viewport_frozen_layers_roundtrip() { assert!(frozen.contains(&(3, Vec::new())), "{frozen:?}"); } +#[test] +fn viewport_layer_colors_roundtrip() { + let mut d = sample(); + let paper = Space::Paper("Layout1".into()); + let vp = |id: u32, frozen: Vec, colors: Vec<(String, Color)>| { + EntityKind::Viewport(Viewport { + center: Vec3::new(5.0, 4.0, 0.0), + width: 8.0, + height: 6.0, + view_center: Vec2::new(5.0, 2.5), + view_height: 12.0, + id, + locked: false, + frozen_layers: frozen, + layer_colors: colors, + }) + }; + let colors = vec![("Walls".to_string(), Color::Index(5)), ("A B".to_string(), Color::True(cadcraft_color::Rgb(255, 128, 0)))]; + d.add(&paper, Common::default(), vp(2, Vec::new(), colors.clone())).unwrap(); + d.add(&paper, Common::default(), vp(3, Vec::new(), Vec::new())).unwrap(); + // Frozen layers and colour overrides share one CADCraft xdata group. + d.add(&paper, Common::default(), vp(4, vec!["Walls".into()], colors.clone())).unwrap(); + let back = roundtrip(&d); + let found: Vec<(u32, Vec, Vec<(String, Color)>)> = back + .layouts + .iter() + .flat_map(|l| l.entities.iter()) + .filter_map(|e| match &e.kind { + EntityKind::Viewport(v) => Some((v.id, v.frozen_layers.clone(), v.layer_colors.clone())), + _ => None, + }) + .collect(); + assert!(found.contains(&(2, Vec::new(), colors.clone())), "{found:?}"); + assert!(found.contains(&(3, Vec::new(), Vec::new())), "{found:?}"); + assert!(found.contains(&(4, vec!["Walls".to_string()], colors)), "{found:?}"); +} + fn full_dim_style() -> DimStyle { DimStyle { name: "Mech".into(),