mirror of
https://github.com/storytold/cadcraft.git
synced 2026-11-03 00:42:46 -06:00
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:
co-authored by
Claude Opus 5.5
parent
7f84f9c874
commit
27d6bf2548
Generated
+2
-2
@@ -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",
|
||||
|
||||
@@ -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
@@ -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}"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
Reference in New Issue
Block a user