From b7f83a66871d6f0c2e7b1fc0fa23572a8afbadfe Mon Sep 17 00:00:00 2001 From: MarekSpodBiedry <73122968+MarekSpodBiedry@users.noreply.github.com> Date: Sat, 10 Oct 2026 18:14:21 +0200 Subject: [PATCH] Keep viewport frozen-layer lists through DXF save and reopen (#98) (#150) --- crates/io/src/dxf_ext.rs | 14 ++++++++++++++ crates/io/src/dxf_read.rs | 2 +- crates/io/src/dxf_write.rs | 4 ++++ crates/io/src/tests.rs | 33 +++++++++++++++++++++++++++++++++ 4 files changed, 52 insertions(+), 1 deletion(-) diff --git a/crates/io/src/dxf_ext.rs b/crates/io/src/dxf_ext.rs index 88328de4..e1641954 100644 --- a/crates/io/src/dxf_ext.rs +++ b/crates/io/src/dxf_ext.rs @@ -306,6 +306,20 @@ pub(crate) fn assoc_xdata(assoc: &[DimAssoc]) -> Vec { v } +/// A viewport's frozen layer names as CADCraft xdata: `1000 VPFROZEN`, `1002 {`, one `1000` per layer, `1002 }`. +pub(crate) fn frozen_xdata(layers: &[String]) -> Vec { + let mut v = vec![Tag::s(1000, "VPFROZEN"), Tag::s(1002, "{")]; + v.extend(layers.iter().map(|l| Tag::s(1000, l.clone()))); + v.push(Tag::s(1002, "}")); + v +} + +/// The frozen layer names written by [`frozen_xdata`]; empty for files from other writers. +pub(crate) fn read_frozen(tags: &[Tag]) -> Vec { + let list = xdata_list(xdata(tags, APP), "VPFROZEN"); + list.iter().take(MAX_XDATA_ITEMS).filter(|t| t.code == 1000).map(Tag::str).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 68e58e60..54c7f238 100644 --- a/crates/io/src/dxf_read.rs +++ b/crates/io/src/dxf_read.rs @@ -288,7 +288,7 @@ fn entity(kind: &str, tags: &[Tag]) -> Option<(Common, EntityKind)> { view_height: t.fd(45, 1.0), id: t.i(69).unwrap_or(0) as u32, locked: t.i(90).unwrap_or(0) & 16384 != 0, - frozen_layers: Vec::new(), + frozen_layers: crate::dxf_ext::read_frozen(tags), layer_colors: Vec::new(), }), "WIPEOUT" => { diff --git a/crates/io/src/dxf_write.rs b/crates/io/src/dxf_write.rs index dd24bf2f..98edc6c7 100644 --- a/crates/io/src/dxf_write.rs +++ b/crates/io/src/dxf_write.rs @@ -765,6 +765,10 @@ 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() { + w.s(1001, dxf_ext::APP); + w.xdata(dxf_ext::frozen_xdata(&v.frozen_layers)); + } } // Not yet written: images, wipeouts, tables, multileaders, unknown objects. _ => {} diff --git a/crates/io/src/tests.rs b/crates/io/src/tests.rs index de7dd2d0..e9699c41 100644 --- a/crates/io/src/tests.rs +++ b/crates/io/src/tests.rs @@ -419,6 +419,39 @@ fn roundtrip(d: &Drawing) -> Drawing { read_dxf(write_dxf(d).as_bytes()).unwrap() } +#[test] +fn viewport_frozen_layers_roundtrip() { + let mut d = sample(); + let paper = Space::Paper("Layout1".into()); + let vp = |id: u32, frozen: Vec| { + 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: true, + frozen_layers: frozen, + layer_colors: Vec::new(), + }) + }; + d.add(&paper, Common::default(), vp(2, vec!["Walls".into(), "A B".into()])).unwrap(); + d.add(&paper, Common::default(), vp(3, Vec::new())).unwrap(); + let back = roundtrip(&d); + let frozen: Vec<(u32, Vec)> = 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())), + _ => None, + }) + .collect(); + assert!(frozen.contains(&(2, vec!["Walls".to_string(), "A B".to_string()])), "{frozen:?}"); + assert!(frozen.contains(&(3, Vec::new())), "{frozen:?}"); +} + fn full_dim_style() -> DimStyle { DimStyle { name: "Mech".into(),