DWG: keep true colours, transparency, table styles, layouts and viewport ids (fixes #223) (#225)

DWG saves convert our R2000 DXF with acadrust, which wrote an R2000 DWG:
no true colours, no entity transparency, no table styles, and its R2000
reader puts every layout's paper-space entities into the first layout.
The bridge now writes at least AutoCAD 2004 (AC1018) DWG; the DXF itself
stays R2000. acadrust goes to 0.6.3 (explicit transparency encoding).

DWG doesn't store viewport ids: readers number a layout's viewports in
order, and id 1 is the paper-space viewport (the sheet). CADCraft keeps
that one implicit, so its first viewport came back as id 1 and wasn't
drawn. The DXF writer now puts a paper-space viewport (id 1, marked with
CADCraft xdata) ahead of a layout's viewports when it has none, and the
reader drops it again. Viewports also get status 68 and the always-set
bit 32768 in group 90, so other readers don't take them as "off".

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Valentin Klein
2026-10-10 14:40:28 -04:00
committed by GitHub
co-authored by Claude Opus 5.5
parent 7f84f9c874
commit 27d6bf2548
7 changed files with 226 additions and 8 deletions
Generated
+2 -2
View File
@@ -4,9 +4,9 @@ version = 4
[[package]]
name = "acadrust"
version = "0.6.1"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1db2bd3da02ed787f3f5c8afba8779dd104b4459b78bb1431b1a0211cc3719f0"
checksum = "7df99ae4f5a6d0147907f9976fe1dd166d01e72ab1a93820b50242937c5797e6"
dependencies = [
"anyhow",
"bitflags 2.13.2",
+1 -1
View File
@@ -10,4 +10,4 @@ workspace = true
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
# MPL-2.0: used as an unmodified dependency only (see plan/adr/0001 and NOTICE).
acadrust = "0.6.1"
acadrust = "0.6.3"
+12 -2
View File
@@ -104,12 +104,22 @@ mod native {
}
}
/// Convert DXF bytes into a DWG file.
/// Oldest DWG version written; older DXF input is written as this version.
const MIN_DWG_VERSION: acadrust::DxfVersion = acadrust::DxfVersion::AC1018;
/// Convert DXF bytes into a DWG file of the DXF's version, and at least AutoCAD 2004
/// (AC1018).
pub fn dxf_to_dwg(dxf: &[u8]) -> Result<Vec<u8>, String> {
let data = dxf.to_vec();
let doc = std::panic::catch_unwind(move || acadrust::DxfReader::from_reader(Cursor::new(data)).and_then(|r| r.read()))
let mut doc = std::panic::catch_unwind(move || acadrust::DxfReader::from_reader(Cursor::new(data)).and_then(|r| r.read()))
.map_err(|_| "the DXF→DWG conversion failed".to_string())?
.map_err(|e| format!("DXF: {e}"))?;
// Write at least AutoCAD 2004 (AC1018) DWG: R2000 DWG has no true colours, transparency,
// table styles or gradients, and readers can't tell its layouts apart reliably. The DXF
// stays R2000; only the DWG is newer.
if doc.version < MIN_DWG_VERSION {
doc.version = MIN_DWG_VERSION;
}
acadrust::DwgWriter::write_to_vec(&doc).map_err(|e| format!("DWG write: {e}"))
}
}
+9
View File
@@ -306,6 +306,15 @@ pub(crate) fn read_dstyle(tags: &[Tag], styles: &HashMap<String, String>, blocks
out
}
/// Xdata marker of the paper-space viewport (id 1) the writer adds to layouts that have none;
/// CADCraft keeps that viewport implicit, so the reader drops it.
pub(crate) const PAPER_VIEW: &str = "PAPERVIEW";
/// True for a VIEWPORT record carrying the [`PAPER_VIEW`] marker.
pub(crate) fn is_paper_view(tags: &[Tag]) -> bool {
xdata(tags, APP).iter().any(|t| t.code == 1000 && t.str().trim().eq_ignore_ascii_case(PAPER_VIEW))
}
/// Snap kinds in CADCraft's ASSOC xdata.
fn snap_code(s: &AssocSnap) -> i64 {
match s {
+4
View File
@@ -122,6 +122,10 @@ fn text_from(t: &T) -> Text {
/// Parse one entity record. POLYLINE/INSERT followers are handled by the caller.
fn entity(kind: &str, tags: &[Tag]) -> Option<(Common, EntityKind)> {
// The paper-space viewport our writer adds (dxf_write::paper_view) stays implicit.
if kind == "VIEWPORT" && crate::dxf_ext::is_paper_view(tags) {
return None;
}
let t = T(tags);
let c = common(&t);
let k = match kind {
+59 -3
View File
@@ -92,6 +92,9 @@ struct OsnapRef {
/// Handles and names the entity writer needs from the rest of the file.
#[derive(Default)]
struct Ctx {
/// First viewport of each layout that has no paper-space viewport (id 1) → the layout's index;
/// [`paper_view`] writes that layout's paper-space viewport ahead of it.
paper_views: HashMap<Handle, usize>,
/// Dimension → anonymous `*D` block name.
dim_blocks: HashMap<Handle, String>,
/// Upper-case text style name → STYLE record handle.
@@ -808,16 +811,19 @@ fn entity(w: &mut W, d: &Drawing, e: &Entity, owner: &str, paper: bool, cx: &Ctx
hatch_xdata(w, h);
}
EntityKind::Viewport(v) => {
paper_view(w, d, e, owner, cx);
w.s(0, "VIEWPORT");
common(w, e, owner, paper, "AcDbViewport");
w.p(10, v.center);
w.f(40, v.width);
w.f(41, v.height);
// Status 68: a positive value is "on" (the stacking order; 1 is the active viewport).
w.i(68, i64::from(v.id.clamp(1, 32767)));
w.i(69, i64::from(v.id));
w.p2(12, v.view_center);
w.f(45, v.view_height);
// Status flags: 16384 = display locked.
w.i(90, if v.locked { 16384 } else { 0 });
// Status flags: 16384 = display locked, 32768 = always set (readers take it as "on").
w.i(90, 32768 | if v.locked { 16384 } else { 0 });
// 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);
@@ -1091,6 +1097,56 @@ fn std_assoc_refs(d: &Drawing, dm: &Dimension) -> (i64, Vec<OsnapRef>) {
(flags, refs)
}
/// Layouts with viewports but without the paper-space viewport (id 1, the sheet itself), keyed
/// by their first viewport. AutoCAD and DWG take a layout's first viewport as its paper-space
/// viewport (DWG does not store viewport ids; readers number them in order), so a layout made in
/// CADCraft, whose paper-space viewport is implicit, gets one written ahead of its viewports.
fn paper_views(d: &Drawing) -> HashMap<Handle, usize> {
let mut out = HashMap::new();
'layouts: for (i, l) in d.layouts.iter().enumerate() {
let mut first = None;
for e in l.entities.iter() {
if let EntityKind::Viewport(v) = &e.kind {
if v.id == 1 {
continue 'layouts;
}
first.get_or_insert(e.handle);
}
}
if let Some(h) = first {
out.insert(h, i);
}
}
out
}
/// The paper-space viewport (id 1) of the layout whose first viewport is `e`, if it needs one
/// ([`paper_views`]): the whole sheet, viewed 1:1. Marked with CADCraft xdata so that our reader
/// leaves it implicit again.
fn paper_view(w: &mut W, d: &Drawing, e: &Entity, owner: &str, cx: &Ctx) {
let Some(l) = cx.paper_views.get(&e.handle).and_then(|i| d.layouts.get(*i)) else { return };
let size = cadcraft_render::paper::sheet(d, &l.name).map(|s| s.size).unwrap_or(Vec2::new(12.0, 9.0));
let center = size * 0.5;
let h = w.h();
w.s(0, "VIEWPORT");
w.s(5, h);
w.s(330, owner);
w.s(100, "AcDbEntity");
w.i(67, 1);
w.s(8, "0");
w.s(100, "AcDbViewport");
w.p(10, center.to3(0.0));
w.f(40, size.x);
w.f(41, size.y);
w.i(68, 1);
w.i(69, 1);
w.p2(12, center);
w.f(45, size.y);
w.i(90, 32768);
w.s(1001, dxf_ext::APP);
w.s(1000, dxf_ext::PAPER_VIEW);
}
fn table_head(w: &mut W, name: &str, count: usize) -> String {
let h = w.h();
w.s(0, "TABLE");
@@ -1129,7 +1185,7 @@ pub fn write(d: &Drawing) -> String {
let mut user_blocks: Vec<(String, String, &Block)> = d.blocks.values().map(|b| (b.name.clone(), String::new(), b.as_ref())).collect();
user_blocks.iter_mut().for_each(|b| b.1 = format!("{:X}", 0));
let user_blocks: Vec<(String, String, &Block)> = user_blocks.into_iter().map(|(n, _, b)| (n, w.h(), b)).collect();
let mut cx = Ctx::default();
let mut cx = Ctx { paper_views: paper_views(d), ..Ctx::default() };
// Dimension blocks.
let mut dim_defs: Vec<(String, String, Vec<Entity>)> = Vec::new(); // filled below
let mut n = 1;
+139
View File
@@ -0,0 +1,139 @@
//! Data that used to be lost only when a drawing is saved as DWG and reopened (the DWG bridge
//! converts our DXF with acadrust), and the paper-space viewport the DXF writer adds for it.
#![cfg(not(target_arch = "wasm32"))]
use cadcraft_color::{Color, Rgb};
use cadcraft_doc::*;
use cadcraft_geom::{Vec2, Vec3};
fn through_dwg(d: &Drawing) -> Drawing {
let dwg = cadcraft_io::write(d, "x.dwg").unwrap();
cadcraft_io::read(&dwg, "x.dwg").unwrap()
}
fn line(d: &mut Drawing, space: &Space, common: Common) -> Handle {
d.add(space, common, EntityKind::Line(Line { a: Vec3::ZERO, b: Vec3::new(10.0, 5.0, 0.0) })).unwrap()
}
fn viewport(id: u32) -> EntityKind {
EntityKind::Viewport(Viewport {
center: Vec3::new(50.0, 40.0, 0.0),
width: 80.0,
height: 60.0,
view_center: Vec2::new(10.0, 10.0),
view_height: 50.0,
id,
locked: false,
frozen_layers: vec![],
layer_colors: vec![],
})
}
/// The (code, value) pairs of every `kind` record in DXF text.
fn records(text: &str, kind: &str) -> Vec<Vec<(String, String)>> {
let lines: Vec<&str> = text.lines().map(str::trim).collect();
let mut out: Vec<Vec<(String, String)>> = Vec::new();
let mut inside = false;
for pair in lines.chunks(2) {
let [code, value] = pair else { break };
if *code == "0" {
inside = *value == kind;
if inside {
out.push(Vec::new());
}
} else if inside && let Some(rec) = out.last_mut() {
rec.push((code.to_string(), value.to_string()));
}
}
out
}
fn group<'a>(rec: &'a [(String, String)], code: &str) -> Option<&'a str> {
rec.iter().find(|(c, _)| c == code).map(|(_, v)| v.as_str())
}
fn viewport_ids(l: &Layout) -> Vec<u32> {
l.entities.iter().filter_map(|e| if let EntityKind::Viewport(v) = &e.kind { Some(v.id) } else { None }).collect()
}
/// True colours (entity and layer) and entity transparency: the DWG is written as AutoCAD 2004
/// (R2000 DWG has neither), while the DXF stays R2000.
#[test]
fn dwg_keeps_true_colours_and_transparency() {
let mut d = Drawing::new_metric();
d.layers.push(Layer { name: "Fancy".into(), color: Color::True(Rgb(12, 200, 99)), ..Layer::default() });
let h = line(
&mut d,
&Space::Model,
Common { layer: "Fancy".into(), color: Color::True(Rgb(1, 2, 3)), transparency: Transparency::Percent(40), ..Common::default() },
);
let dwg = cadcraft_io::write(&d, "x.dwg").unwrap();
assert_eq!(cadcraft_dwg::version(&dwg).as_deref(), Some("AC1018"));
let header = records(&cadcraft_io::write_dxf(&d), "SECTION");
assert_eq!(header.first().and_then(|h| group(h, "1")), Some("AC1015"), "the DXF stays R2000");
let back = cadcraft_io::read(&dwg, "x.dwg").unwrap();
let e = back.entity(h).unwrap();
assert_eq!(e.common.color, Color::True(Rgb(1, 2, 3)));
assert_eq!(e.common.transparency, Transparency::Percent(40));
assert_eq!(back.layers.iter().find(|l| l.name == "Fancy").map(|l| l.color), Some(Color::True(Rgb(12, 200, 99))));
}
/// Entities of a layout other than the first stay in that layout.
#[test]
fn dwg_keeps_paper_space_entities_in_their_layout() {
let mut d = Drawing::new_metric();
let second = Space::Paper(d.layouts[1].name.clone());
let h = d.add(&second, Common::default(), EntityKind::Circle(Circle { center: Vec3::new(1.0, 1.0, 0.0), radius: 0.5 })).unwrap();
let back = through_dwg(&d);
assert!(back.layouts[1].entities.contains(h), "{:?}", back.layouts.iter().map(|l| l.entities.len()).collect::<Vec<_>>());
assert!(!back.layouts[0].entities.contains(h));
}
/// Table styles (R2000 DWG has no TABLESTYLE objects).
#[test]
fn dwg_keeps_table_styles() {
let mut d = Drawing::new_metric();
let sched = TableStyle { name: "Sched".into(), text_height: 2.5, margin: 1.0, title: false, header: true };
d.table_styles.push(sched.clone());
let back = through_dwg(&d);
assert_eq!(back.table_styles.iter().find(|s| s.name == "Sched"), Some(&sched));
}
/// DWG doesn't store viewport ids; readers number a layout's viewports in order, and id 1 is the
/// paper-space viewport (the sheet, not drawn). The writer puts that viewport ahead of a layout's
/// own viewports, so a viewport made in CADCraft (id 2) keeps its id instead of becoming the
/// sheet; our reader drops the added viewport again, so a DXF round trip is unchanged.
#[test]
fn viewport_ids_survive_dwg_and_the_paper_viewport_stays_implicit() {
let mut d = Drawing::new_metric();
let first = Space::Paper(d.layouts[0].name.clone());
let h = d.add(&first, Common::default(), viewport(2)).unwrap();
d.add(&first, Common::default(), viewport(3)).unwrap();
let text = cadcraft_io::write_dxf(&d);
let vps = records(&text, "VIEWPORT");
assert_eq!(vps.len(), 3, "{text}");
assert_eq!(group(&vps[0], "69"), Some("1"));
assert_eq!(group(&vps[0], "1000"), Some("PAPERVIEW"));
// Viewports are written "on": status 68 positive, and 90 with the always-set bit 32768.
assert_eq!(group(&vps[1], "68"), Some("2"));
assert_eq!(group(&vps[1], "90"), Some("32768"));
let back = cadcraft_io::read_dxf(text.as_bytes()).unwrap();
assert_eq!(viewport_ids(&back.layouts[0]), [2, 3]);
assert_eq!(back.layouts[0].entities.len(), 2);
let back = through_dwg(&d);
assert_eq!(viewport_ids(&back.layouts[0]), [2, 3]);
assert!(back.layouts[0].entities.contains(h));
// A layout that has its paper-space viewport (from another program) gets no second one.
let mut d = Drawing::new_metric();
d.add(&first, Common::default(), viewport(1)).unwrap();
d.add(&first, Common::default(), viewport(2)).unwrap();
let text = cadcraft_io::write_dxf(&d);
assert!(!text.contains("PAPERVIEW"));
assert_eq!(viewport_ids(&cadcraft_io::read_dxf(text.as_bytes()).unwrap().layouts[0]), [1, 2]);
}