mirror of
https://github.com/storytold/cadcraft.git
synced 2026-10-14 14:26:45 -04:00
DXF: write viewport frozen layers as group 341 layer handles too (#827)
A layout VIEWPORT's frozen layers were saved only as CADCraft's own 1001 CADCRAFT / 1000 VPFROZEN xdata. Other programs ignore that, so in any third-party viewer every viewport showed every layer. The writer now also emits the standard group 341: one soft pointer per frozen layer to its LAYER table record handle, after 90/340 and before the xdata. A frozen name with no LAYER record is skipped for 341 only (the xdata still carries it). The reader keeps preferring the xdata; a VIEWPORT without it (a file from another program) resolves its 341 handles through the LAYER records' group-5 handles, ignoring unknown handles and keeping the 341 order. Not covered: the DXF-to-DWG converter does not carry group 341 yet, so a DWG keeps per-viewport freezing only through the CADCraft xdata. Co-authored-by: Claude Opus 5.5 <[email protected]> Co-authored-by: Brandon Thomas <[email protected]>
This commit is contained in:
4 files changed
+155
-6
No files matched your search
@@ -926,6 +926,8 @@ struct Rx {
|
||||
pstyles: HashMap<String, String>,
|
||||
/// Upper-case block name → associative array parameters from its BLOCK_RECORD.
|
||||
block_arrays: HashMap<String, cadcraft_doc::ArrayParams>,
|
||||
/// LAYER record handle (upper case) → layer name (VIEWPORT group 341 frozen layers).
|
||||
layers: HashMap<String, String>,
|
||||
}
|
||||
|
||||
/// The plot style names of a file: the entries of the `ACAD_PLOTSTYLENAME` dictionary of the
|
||||
@@ -1024,6 +1026,15 @@ fn entity_extras(kind: &str, tags: &[Tag], h: Handle, k: &mut EntityKind, rx: &m
|
||||
}
|
||||
rx.mleader_fix.push((h, pending));
|
||||
}
|
||||
// CADCraft's VPFROZEN xdata wins; otherwise AutoCAD's group 341 layer handles.
|
||||
("VIEWPORT", EntityKind::Viewport(v)) if v.frozen_layers.is_empty() => {
|
||||
v.frozen_layers = tags
|
||||
.iter()
|
||||
.take_while(|t| t.code < 1000)
|
||||
.filter(|t| t.code == 341)
|
||||
.filter_map(|t| rx.layers.get(&t.str().trim().to_ascii_uppercase()).cloned())
|
||||
.collect();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -1311,6 +1322,9 @@ fn tables(tags: &[Tag], d: &mut Drawing, rx: &mut Rx) {
|
||||
"LTYPE" => {
|
||||
rx.ltypes.insert(h.trim().to_ascii_uppercase(), n);
|
||||
}
|
||||
"LAYER" => {
|
||||
rx.layers.insert(h.trim().to_ascii_uppercase(), n);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,8 @@ struct W {
|
||||
next: u64,
|
||||
/// Upper-case plot style name → its `ACDBPLACEHOLDER` handle (group 390 of layers and objects).
|
||||
pstyles: HashMap<String, String>,
|
||||
/// Upper-case layer name → its LAYER table record handle (group 341 of viewports).
|
||||
layers: HashMap<String, String>,
|
||||
}
|
||||
|
||||
impl W {
|
||||
@@ -170,7 +172,7 @@ fn annotative_xdata(w: &mut W) {
|
||||
|
||||
/// Override (`ACAD` DSTYLE) and associativity (`CADCRAFT` ASSOC) xdata of a dimension.
|
||||
fn dim_xdata(w: &mut W, dm: &Dimension, cx: &Ctx) {
|
||||
let mut ov = W { t: Vec::new(), next: 0, pstyles: HashMap::new() };
|
||||
let mut ov = W { t: Vec::new(), next: 0, pstyles: HashMap::new(), layers: HashMap::new() };
|
||||
let mut sah = false;
|
||||
for (k, v) in &dm.overrides {
|
||||
let Some(field) = DimStyle::field_name(k) else { continue };
|
||||
@@ -972,6 +974,13 @@ fn entity(w: &mut W, d: &Drawing, e: &Entity, owner: &str, paper: bool, cx: &Ctx
|
||||
if let Some(h) = clip {
|
||||
w.s(340, h.hex());
|
||||
}
|
||||
// AutoCAD's frozen-layer list: one soft pointer per layer, to its LAYER record. Other
|
||||
// programs read only this; CADCraft prefers its own xdata below when present.
|
||||
for name in &v.frozen_layers {
|
||||
if let Some(h) = w.layers.get(&name.to_ascii_uppercase()).cloned() {
|
||||
w.s(341, h);
|
||||
}
|
||||
}
|
||||
// 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);
|
||||
@@ -1465,7 +1474,7 @@ fn tags_for(d: &Drawing, version: Version, target: Version) -> Vec<Tag> {
|
||||
let committed = d.committed();
|
||||
let d: &Drawing = &committed;
|
||||
// Structural handles start above every entity handle.
|
||||
let mut w = W { t: Vec::new(), next: d.handseed.max(0x100).saturating_add(0x1000), pstyles: HashMap::new() };
|
||||
let mut w = W { t: Vec::new(), next: d.handseed.max(0x100).saturating_add(0x1000), pstyles: HashMap::new(), layers: HashMap::new() };
|
||||
// Pre-allocate handles for block records.
|
||||
let mut paper_layouts: Vec<&Layout> = d.layouts.iter().collect();
|
||||
paper_layouts.sort_by_key(|l| l.tab_order);
|
||||
@@ -1768,7 +1777,8 @@ fn tags_for(d: &Drawing, version: Version, target: Version) -> Vec<Tag> {
|
||||
// LAYER
|
||||
let th = table_head(&mut w, "LAYER", d.layers.len());
|
||||
for l in &d.layers {
|
||||
record_head(&mut w, "LAYER", &th, "AcDbLayerTableRecord");
|
||||
let lh = record_head(&mut w, "LAYER", &th, "AcDbLayerTableRecord");
|
||||
w.layers.insert(l.name.to_ascii_uppercase(), lh);
|
||||
w.s(2, &l.name);
|
||||
w.i(70, i64::from(l.frozen) | if l.vp_freeze_new { 2 } else { 0 } | if l.locked { 4 } else { 0 });
|
||||
let aci = match l.color {
|
||||
@@ -1929,7 +1939,7 @@ fn tags_for(d: &Drawing, version: Version, target: Version) -> Vec<Tag> {
|
||||
w.i(70, 0);
|
||||
let v = serde_json::to_value(s).unwrap_or_default();
|
||||
let defaults = serde_json::to_value(DimStyle::default()).unwrap_or_default();
|
||||
let mut newer = W { t: Vec::new(), next: 0, pstyles: HashMap::new() };
|
||||
let mut newer = W { t: Vec::new(), next: 0, pstyles: HashMap::new(), layers: HashMap::new() };
|
||||
for (field, code, kind) in dxf_ext::DIM_CODES {
|
||||
let Some(enc) = v.get(*field).and_then(|x| cx.encode(*kind, x)) else { continue };
|
||||
if !dxf_ext::NEWER_CODES.contains(code) {
|
||||
|
||||
@@ -625,6 +625,130 @@ fn viewport_frozen_layers_roundtrip() {
|
||||
assert!(frozen.contains(&(3, Vec::new())), "{frozen:?}");
|
||||
}
|
||||
|
||||
/// A layout with viewport 2 frozen on `Walls` and `A B` (both real layers), and its written DXF as
|
||||
/// (code, value) pairs.
|
||||
fn frozen_vp_dxf() -> (Drawing, Vec<(String, String)>) {
|
||||
let mut d = sample();
|
||||
d.layers.push(Layer { name: "A B".into(), ..Layer::default() });
|
||||
let paper = Space::Paper("Layout1".into());
|
||||
let vp = 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: 2,
|
||||
locked: false,
|
||||
frozen_layers: vec!["Walls".into(), "A B".into()],
|
||||
layer_colors: Vec::new(),
|
||||
clip: None,
|
||||
on: true,
|
||||
});
|
||||
d.add(&paper, Common::default(), vp).unwrap();
|
||||
let text = write_dxf(&d);
|
||||
let lines: Vec<&str> = text.lines().collect();
|
||||
let pairs = lines.chunks(2).map(|c| (c[0].trim().to_string(), c.get(1).copied().unwrap_or("").to_string())).collect();
|
||||
(d, pairs)
|
||||
}
|
||||
|
||||
/// The pair range of the VIEWPORT record whose group 69 is `id`.
|
||||
fn vp_record(p: &[(String, String)], id: &str) -> std::ops::Range<usize> {
|
||||
let starts: Vec<usize> = (0..p.len()).filter(|&i| p[i].0 == "0").collect();
|
||||
for w in starts.windows(2) {
|
||||
let r = w[0]..w[1];
|
||||
if p[w[0]].1 == "VIEWPORT" && p[r.clone()].iter().any(|(c, v)| c == "69" && v.trim() == id) {
|
||||
return r;
|
||||
}
|
||||
}
|
||||
panic!("no VIEWPORT {id}");
|
||||
}
|
||||
|
||||
fn layer_handle(p: &[(String, String)], name: &str) -> String {
|
||||
let starts: Vec<usize> = (0..p.len()).filter(|&i| p[i].0 == "0").collect();
|
||||
for w in starts.windows(2) {
|
||||
let rec = &p[w[0]..w[1]];
|
||||
if rec[0].1 == "LAYER" && rec.iter().any(|(c, v)| c == "2" && v == name) {
|
||||
return rec.iter().find(|(c, _)| c == "5").unwrap().1.clone();
|
||||
}
|
||||
}
|
||||
panic!("no LAYER {name}");
|
||||
}
|
||||
|
||||
fn viewport_frozen(d: &Drawing, id: u32) -> Vec<String> {
|
||||
d.layouts
|
||||
.iter()
|
||||
.flat_map(|l| l.entities.iter())
|
||||
.find_map(|e| match &e.kind {
|
||||
EntityKind::Viewport(v) if v.id == id => Some(v.frozen_layers.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.expect("viewport")
|
||||
}
|
||||
|
||||
fn join(p: &[(String, String)]) -> String {
|
||||
p.iter()
|
||||
.map(|(c, v)| {
|
||||
format!(
|
||||
"{c}
|
||||
{v}
|
||||
"
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn viewport_frozen_layers_written_as_341_handles() {
|
||||
let (_, p) = frozen_vp_dxf();
|
||||
let r = vp_record(&p, "2");
|
||||
let rec = &p[r];
|
||||
let h341: Vec<&str> = rec.iter().filter(|(c, _)| c == "341").map(|(_, v)| v.as_str()).collect();
|
||||
assert_eq!(h341, vec![layer_handle(&p, "Walls").as_str(), layer_handle(&p, "A B").as_str()]);
|
||||
let last341 = rec.iter().rposition(|(c, _)| c == "341").unwrap();
|
||||
let x = rec.iter().position(|(c, _)| c == "1001").expect("xdata");
|
||||
assert!(last341 < x, "341 must precede the xdata");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn viewport_frozen_layers_read_from_341_without_xdata() {
|
||||
let (_, p) = frozen_vp_dxf();
|
||||
let r = vp_record(&p, "2");
|
||||
// Strip the CADCraft xdata, and add a 341 to a handle that names no layer.
|
||||
let x = p[r.clone()].iter().position(|(c, _)| c == "1001").unwrap() + r.start;
|
||||
let mut q: Vec<(String, String)> = p[..r.start].to_vec();
|
||||
let mut head = p[r.start..x].to_vec();
|
||||
head.push(("341".into(), "FFFFF".into()));
|
||||
q.extend(head);
|
||||
q.extend(p[r.end..].iter().cloned());
|
||||
let back = read_dxf(join(&q).as_bytes()).unwrap();
|
||||
assert_eq!(viewport_frozen(&back, 2), vec!["Walls".to_string(), "A B".to_string()]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn viewport_frozen_xdata_wins_over_341() {
|
||||
let (_, p) = frozen_vp_dxf();
|
||||
let r = vp_record(&p, "2");
|
||||
// Point every 341 at layer 0: the xdata list must still be what is read.
|
||||
let zero = layer_handle(&p, "0");
|
||||
let mut q = p.clone();
|
||||
for t in &mut q[r] {
|
||||
if t.0 == "341" {
|
||||
t.1 = zero.clone();
|
||||
}
|
||||
}
|
||||
let back = read_dxf(join(&q).as_bytes()).unwrap();
|
||||
assert_eq!(viewport_frozen(&back, 2), vec!["Walls".to_string(), "A B".to_string()]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn viewport_frozen_layers_survive_dwg() {
|
||||
let (d, _) = frozen_vp_dxf();
|
||||
let dwg = write(&d, "t.dwg").unwrap();
|
||||
let back = read(&dwg, "t.dwg").unwrap();
|
||||
assert_eq!(viewport_frozen(&back, 2), vec!["Walls".to_string(), "A B".to_string()]);
|
||||
// Survives via the CADCraft xdata: the DWG converter does not yet carry group 341 itself.
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn viewport_layer_colors_roundtrip() {
|
||||
let mut d = sample();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# File-format parity
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-12 · **Change:** minor (the Model layout's plot type is written as extents, not "Layout information") · **Target:** Autodesk AutoCAD 2027
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-12 · **Change:** minor (VIEWPORT frozen layers as group 341; code pages, #505; native MULTILEADER, #459; foreign data kept, #528; PDF text, layers and sheets, #567, #571; templates and user files, #552; plotter devices, #564) · **Target:** Autodesk AutoCAD 2027
|
||||
|
||||
Every format AutoCAD 2027 reads or writes, with CADCraft's support, fidelity and tests. Work
|
||||
items are in [gaps.md](gaps.md#file-formats); the weighted score feeds
|
||||
@@ -80,7 +80,7 @@ early (#175), but the throughput problem remains. **DWG ≈ 30% read, ≈ 15% wr
|
||||
| HATCH gradient | yes | yes (#162, fixes #80) | |
|
||||
| SOLID, TRACE, 3DFACE | yes | yes | |
|
||||
| ACAD_TABLE | yes | yes (entity + block) | |
|
||||
| VIEWPORT | yes | yes; frozen-layer lists kept (#150) | VP layer colour overrides lost (#99, PR #197) |
|
||||
| VIEWPORT | yes | yes; frozen-layer lists kept (#150), also as standard group 341 layer handles that other programs read | DWG: the DXF→DWG converter does not yet write group 341 (frozen layers survive a CADCraft round trip via xdata); VP layer colour overrides lost (#99, PR #197) |
|
||||
| IMAGE | yes, with IMAGEDEF | yes, with IMAGEDEF and IMAGEDEF_REACTOR | no attach command or display of the picture yet (FF10) |
|
||||
| WIPEOUT | yes | yes (#147, fixes #79) | |
|
||||
| MLINE (+ MLINESTYLE), HELIX, TOLERANCE | yes (#510, #420) | yes | |
|
||||
@@ -135,6 +135,7 @@ our output in AutoCAD black-box.
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-12 | minor | DXF/DWG save: the Model layout's plot settings are written as CADCraft plots model space, drawing extents scaled to fit (PLOTSETTINGS 74 = 1, 75 = 0), instead of plot type 5 "Layout information", which other programs' Plot dialogs can't use for model space |
|
||||
| 2026-10-12 | minor | VIEWPORT frozen layers also written and read as AutoCAD's group 341 layer handles, so third-party viewers honour per-viewport freezing in DXF (DWG converter gap noted) |
|
||||
| 2026-10-11 | minor | Rows refreshed for merged work: code pages and `\M+` (#505), MULTILEADER (#459), IMAGE, MLINE/HELIX/REGION/TOLERANCE, VPORT/VIEW/UCS tables, modelled and raw objects, foreign data (#528), DWT templates and user `.lin`/`.pat`/`.pgp` files (#552), PDF output (#567, #571), plotter devices (#564), data extraction (#433, #534); weighted percentages not re-measured |
|
||||
| 2026-10-11 | minor | DXF (ASCII and binary) and DWG saved at 2000–2018, 2018 by default (#421) |
|
||||
| 2026-10-10 | minor | WIPEOUT and gradients written since #147/#162; other save losses remain (#201, #204, #208, #209, #213, #223) |
|
||||
|
||||
Reference in new issue
Block a user