M7: DWG read/write through the acadrust bridge (crates/dwg)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Brandon ThomasandClaude Opus 5.5 committed 2026-10-05 23:43:22 -04:00
1 parent cb68bed225
commit 3e2155e587
9 files changed
+236 -6

No files matched your search

Generated
+130
View File
@@ -2,6 +2,29 @@
# It is not intended for manual editing. # It is not intended for manual editing.
version = 4 version = 4
[[package]]
name = "acadrust"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1db2bd3da02ed787f3f5c8afba8779dd104b4459b78bb1431b1a0211cc3719f0"
dependencies = [
"anyhow",
"bitflags 2.13.2",
"byteorder",
"encoding_rs",
"flate2",
"foldhash",
"indexmap",
"itoa",
"memmap2",
"nom",
"once_cell",
"rayon",
"ryu",
"thiserror 2.0.21",
"web-time",
]
[[package]] [[package]]
name = "accesskit" name = "accesskit"
version = "0.24.1" version = "0.24.1"
@@ -174,6 +197,12 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "anyhow"
version = "1.0.104"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
[[package]] [[package]]
name = "arboard" name = "arboard"
version = "3.6.1" version = "3.6.1"
@@ -485,6 +514,12 @@ dependencies = [
"syn 3.0.6", "syn 3.0.6",
] ]
[[package]]
name = "byteorder"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]] [[package]]
name = "byteorder-lite" name = "byteorder-lite"
version = "0.1.0" version = "0.1.0"
@@ -542,6 +577,13 @@ dependencies = [
"thiserror 2.0.21", "thiserror 2.0.21",
] ]
[[package]]
name = "cadcraft-dwg"
version = "0.1.0"
dependencies = [
"acadrust",
]
[[package]] [[package]]
name = "cadcraft-dxf" name = "cadcraft-dxf"
version = "0.1.0" version = "0.1.0"
@@ -584,6 +626,7 @@ version = "0.1.0"
dependencies = [ dependencies = [
"cadcraft-color", "cadcraft-color",
"cadcraft-doc", "cadcraft-doc",
"cadcraft-dwg",
"cadcraft-dxf", "cadcraft-dxf",
"cadcraft-geom", "cadcraft-geom",
"cadcraft-render", "cadcraft-render",
@@ -815,6 +858,12 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "core_detect"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f8f80099a98041a3d1622845c271458a2d73e688351bf3cb999266764b81d48"
[[package]] [[package]]
name = "crc32fast" name = "crc32fast"
version = "1.5.2" version = "1.5.2"
@@ -833,6 +882,25 @@ dependencies = [
"crossbeam-utils", "crossbeam-utils",
] ]
[[package]]
name = "crossbeam-deque"
version = "0.8.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "622f3fc73690be383c7214310406f28a90e6edeadc3cea882f9d71e495b9711a"
dependencies = [
"crossbeam-epoch",
"crossbeam-utils",
]
[[package]]
name = "crossbeam-epoch"
version = "0.9.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc74980687109a3b14c72fd458107bf0baa1da1a1a805e178d15501ba9b86d9d"
dependencies = [
"crossbeam-utils",
]
[[package]] [[package]]
name = "crossbeam-utils" name = "crossbeam-utils"
version = "0.8.23" version = "0.8.23"
@@ -1046,6 +1114,20 @@ dependencies = [
"bytemuck", "bytemuck",
] ]
[[package]]
name = "encoding_rs"
version = "0.8.42"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e985e0451871ad22fb8d2b6b076e2028a502a0d3950998c2c5c0a4f9b5d9679"
dependencies = [
"cfg-if",
"core_detect",
"multiversion_no_op",
"rustversion",
"scopeguard",
"simdutf8",
]
[[package]] [[package]]
name = "endi" name = "endi"
version = "1.1.1" version = "1.1.1"
@@ -1888,6 +1970,12 @@ dependencies = [
"autocfg", "autocfg",
] ]
[[package]]
name = "minimal-lexical"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]] [[package]]
name = "miniz_oxide" name = "miniz_oxide"
version = "0.8.9" version = "0.8.9"
@@ -1937,6 +2025,12 @@ dependencies = [
"windows-sys 0.61.2", "windows-sys 0.61.2",
] ]
[[package]]
name = "multiversion_no_op"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "743fb55ba31b18fb1ecef6bdc9aa2743314978ac084044301a7eee33fb99a20d"
[[package]] [[package]]
name = "naga" name = "naga"
version = "30.0.1" version = "30.0.1"
@@ -2011,6 +2105,16 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451"
[[package]]
name = "nom"
version = "7.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
dependencies = [
"memchr",
"minimal-lexical",
]
[[package]] [[package]]
name = "num-traits" name = "num-traits"
version = "0.2.19" version = "0.2.19"
@@ -2710,6 +2814,26 @@ dependencies = [
"objc2-quartz-core 0.3.2", "objc2-quartz-core 0.3.2",
] ]
[[package]]
name = "rayon"
version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d"
dependencies = [
"either",
"rayon-core",
]
[[package]]
name = "rayon-core"
version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91"
dependencies = [
"crossbeam-deque",
"crossbeam-utils",
]
[[package]] [[package]]
name = "read-fonts" name = "read-fonts"
version = "0.41.0" version = "0.41.0"
@@ -2834,6 +2958,12 @@ version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
[[package]]
name = "ryu"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
[[package]] [[package]]
name = "same-file" name = "same-file"
version = "1.0.6" version = "1.0.6"
+1
View File
@@ -14,6 +14,7 @@ repository = "https://github.com/storytold/cadcraft"
# internal # internal
cadcraft-geom = { path = "crates/geom" } cadcraft-geom = { path = "crates/geom" }
cadcraft-dxf = { path = "crates/dxf" } cadcraft-dxf = { path = "crates/dxf" }
cadcraft-dwg = { path = "crates/dwg" }
cadcraft-color = { path = "crates/color" } cadcraft-color = { path = "crates/color" }
cadcraft-doc = { path = "crates/doc" } cadcraft-doc = { path = "crates/doc" }
cadcraft-fonts = { path = "crates/fonts" } cadcraft-fonts = { path = "crates/fonts" }
+6
View File
@@ -9,6 +9,12 @@ docs/brand/LICENSE-brand.txt.
Third-party material Third-party material
Software dependencies are listed in Cargo.lock. One is under a weak-copyleft licence: the acadrust
crate (DWG reading and writing, https://github.com/hakanaktt/acadrust) is licensed under the
Mozilla Public License 2.0 and is used unmodified as a separate library (crates/dwg). Its source
is available from crates.io and the repository above.
None: every bundled asset is original to CADCraft (icons, drafting font, hatch patterns and None: every bundled asset is original to CADCraft (icons, drafting font, hatch patterns and
linetypes are drawn or defined in code). The per-file list is in ATTRIBUTION.md. Builds made with linetypes are drawn or defined in code). The per-file list is in ATTRIBUTION.md. Builds made with
the optional craft-fonts input (CRAFT_FONTS_DIR) embed the fonts listed in its ATTRIBUTION.md, the optional craft-fonts input (CRAFT_FONTS_DIR) embed the fonts listed in its ATTRIBUTION.md,
+4 -3
View File
@@ -67,8 +67,9 @@
line, prompts with clickable `[Keywords]`, AutoComplete, object snaps, polar tracking, ortho, line, prompts with clickable `[Keywords]`, AutoComplete, object snaps, polar tracking, ortho,
direct distance entry, window and crossing selection, grips, and right-click-to-repeat behave direct distance entry, window and crossing selection, grips, and right-click-to-repeat behave
the way decades of drafting habit expect. the way decades of drafting habit expect.
- **Open files.** DXF is read and written natively (ASCII and binary, R12 through 2018); DWG is - **Open files.** DXF is read and written natively (ASCII and binary, R12 through 2018), and DWG
on the roadmap. Export to SVG and PNG today. files (R13 through 2018) open and save through the open-source acadrust library. Export to SVG
and PNG today.
- **Fast and native.** Pure Rust and egui, no Electron, no web view. One binary on macOS - **Fast and native.** Pure Rust and egui, no Electron, no web view. One binary on macOS
(universal), Windows, Linux and FreeBSD, plus a WebAssembly build for the browser. (universal), Windows, Linux and FreeBSD, plus a WebAssembly build for the browser.
- **Built for agents.** Every menu item, tool and prompt is a command. Agents can type at the - **Built for agents.** Every menu item, tool and prompt is a command. Agents can type at the
@@ -91,7 +92,7 @@ numbers):
| Layers & properties | Layers palette and Layer Properties Manager (on/off, freeze, lock, plot, colour, linetype), layer tools (isolate, freeze, off, lock, match, previous), Properties palette with per-object editing, linetypes, lineweights, colour index and true colour | | Layers & properties | Layers palette and Layer Properties Manager (on/off, freeze, lock, plot, colour, linetype), layer tools (isolate, freeze, off, lock, match, previous), Properties palette with per-object editing, linetypes, lineweights, colour index and true colour |
| Annotation | Dimensions (linear, aligned, radius, diameter, angular, ordinate, arc length) rendered from DIMSTYLE settings, our own single-stroke drafting font, `%%d %%p %%c` codes, MTEXT wrapping and attachment | | Annotation | Dimensions (linear, aligned, radius, diameter, angular, ordinate, arc length) rendered from DIMSTYLE settings, our own single-stroke drafting font, `%%d %%p %%c` codes, MTEXT wrapping and attachment |
| Hatch & blocks | Pattern and solid hatches with our own pattern library, block references with attributes and nested blocks | | Hatch & blocks | Pattern and solid hatches with our own pattern library, block references with attributes and nested blocks |
| Files | DXF read/write, SVG and PNG export | | Files | DXF read/write (R12–2018), DWG read/write (R13–2018, via the acadrust library), SVG and PNG export |
| Automation | MCP server, JSON control channel, `cadcraft-cli` (info, convert, run, commands, mcp) | | Automation | MCP server, JSON control channel, `cadcraft-cli` (info, convert, run, commands, mcp) |
## Quick start ## Quick start
+13
View File
@@ -0,0 +1,13 @@
[package]
name = "cadcraft-dwg"
description = "CADCraft DWG bridge: converts DWG to and from DXF bytes using the acadrust crate (MPL-2.0, used unmodified)"
version.workspace = true
edition.workspace = true
license.workspace = true
[lints]
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"
+75
View File
@@ -0,0 +1,75 @@
//! DWG support through a DXF bridge.
//!
//! DWG files are read with the `acadrust` crate (MPL-2.0, used unmodified as a dependency) and
//! converted to DXF bytes, which CADCraft's own DXF reader maps to its document model; saving
//! goes the other way. Keeping the dependency behind this byte-level API isolates it: nothing
//! else in CADCraft depends on its types. Native targets only (it memory-maps files).
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
#![forbid(unsafe_code)]
/// DWG version codes we can write.
pub const VERSIONS: &[(&str, &str)] =
&[("AC1015", "2000"), ("AC1018", "2004"), ("AC1021", "2007"), ("AC1024", "2010"), ("AC1027", "2013"), ("AC1032", "2018")];
/// True when the bytes look like a DWG file (`AC10xx` magic).
pub fn is_dwg(bytes: &[u8]) -> bool {
bytes.len() > 6 && bytes.starts_with(b"AC10") && bytes.get(4..6).is_some_and(|v| v.iter().all(u8::is_ascii_digit))
}
/// The DWG version code of a file (e.g. "AC1032").
pub fn version(bytes: &[u8]) -> Option<String> {
is_dwg(bytes).then(|| String::from_utf8_lossy(bytes.get(0..6).unwrap_or_default()).to_string())
}
#[cfg(not(target_arch = "wasm32"))]
mod native {
use std::io::Cursor;
/// Read DWG bytes and return an ASCII DXF rendition.
pub fn dwg_to_dxf(bytes: &[u8]) -> Result<Vec<u8>, String> {
let data = bytes.to_vec();
let doc = std::panic::catch_unwind(move || acadrust::DwgReader::from_stream(Cursor::new(data)).read())
.map_err(|_| "the DWG reader failed on this file".to_string())?
.map_err(|e| format!("DWG: {e}"))?;
acadrust::DxfWriter::new(&doc).write_to_vec().map_err(|e| format!("DWG→DXF: {e}"))
}
/// Convert DXF bytes into a DWG file.
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()))
.map_err(|_| "the DXF→DWG conversion failed".to_string())?
.map_err(|e| format!("DXF: {e}"))?;
acadrust::DwgWriter::write_to_vec(&doc).map_err(|e| format!("DWG write: {e}"))
}
}
#[cfg(not(target_arch = "wasm32"))]
pub use native::{dwg_to_dxf, dxf_to_dwg};
#[cfg(target_arch = "wasm32")]
pub fn dwg_to_dxf(_bytes: &[u8]) -> Result<Vec<u8>, String> {
Err("DWG files can't be opened in the web build yet; save as DXF".into())
}
#[cfg(target_arch = "wasm32")]
pub fn dxf_to_dwg(_dxf: &[u8]) -> Result<Vec<u8>, String> {
Err("DWG files can't be written in the web build yet; save as DXF".into())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn detects_magic() {
assert!(is_dwg(b"AC1032\0\0\0\0"));
assert!(!is_dwg(b" 0\r\nSECTION"));
assert_eq!(version(b"AC1018xxxx").as_deref(), Some("AC1018"));
}
#[test]
fn garbage_is_an_error_not_a_crash() {
assert!(dwg_to_dxf(b"AC1032 this is not a dwg file at all").is_err());
assert!(dwg_to_dxf(b"").is_err());
}
}
+1
View File
@@ -13,5 +13,6 @@ cadcraft-geom = { workspace = true }
cadcraft-color = { workspace = true } cadcraft-color = { workspace = true }
cadcraft-doc = { workspace = true } cadcraft-doc = { workspace = true }
cadcraft-dxf = { workspace = true } cadcraft-dxf = { workspace = true }
cadcraft-dwg = { workspace = true }
cadcraft-render = { workspace = true } cadcraft-render = { workspace = true }
thiserror = { workspace = true } thiserror = { workspace = true }
+5 -3
View File
@@ -24,8 +24,9 @@ fn ext(name: &str) -> String {
/// Read a drawing; the format comes from the content (DXF ASCII/binary) or the name. /// Read a drawing; the format comes from the content (DXF ASCII/binary) or the name.
pub fn read(bytes: &[u8], name: &str) -> Result<Drawing> { pub fn read(bytes: &[u8], name: &str) -> Result<Drawing> {
if bytes.starts_with(b"AC10") { if cadcraft_dwg::is_dwg(bytes) {
return Err(IoError::Unsupported("DWG (reading .dwg is on the roadmap; save as DXF for now)".into())); let dxf = cadcraft_dwg::dwg_to_dxf(bytes).map_err(IoError::Format)?;
return dxf_read::read(&dxf);
} }
match ext(name).as_str() { match ext(name).as_str() {
"dxf" | "" => dxf_read::read(bytes), "dxf" | "" => dxf_read::read(bytes),
@@ -37,7 +38,8 @@ pub fn read(bytes: &[u8], name: &str) -> Result<Drawing> {
/// Write a drawing in the format chosen by the name's extension. /// Write a drawing in the format chosen by the name's extension.
pub fn write(d: &Drawing, name: &str) -> Result<Vec<u8>> { pub fn write(d: &Drawing, name: &str) -> Result<Vec<u8>> {
match ext(name).as_str() { match ext(name).as_str() {
"dxf" | "dwg" | "" => Ok(dxf_write::write(d).into_bytes()), "dxf" | "" => Ok(dxf_write::write(d).into_bytes()),
"dwg" => cadcraft_dwg::dxf_to_dwg(dxf_write::write(d).as_bytes()).map_err(IoError::Format),
"svg" => Ok(svg::export(d, &Space::Model).into_bytes()), "svg" => Ok(svg::export(d, &Space::Model).into_bytes()),
"png" => png(d, &Space::Model, 2400, 1600), "png" => png(d, &Space::Model, 2400, 1600),
e => Err(IoError::Unsupported(e.to_string())), e => Err(IoError::Unsupported(e.to_string())),
+1
View File
@@ -35,6 +35,7 @@ impl Class {
pub const TABLE: &[(&str, Class)] = &[ pub const TABLE: &[(&str, Class)] = &[
("geom", Class::Layer(0)), ("geom", Class::Layer(0)),
("dxf", Class::Standalone), ("dxf", Class::Standalone),
("dwg", Class::Standalone),
("color", Class::Layer(0)), ("color", Class::Layer(0)),
("doc", Class::Layer(1)), ("doc", Class::Layer(1)),
("fonts", Class::Layer(2)), ("fonts", Class::Layer(2)),