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.
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]]
name = "accesskit"
version = "0.24.1"
@@ -174,6 +197,12 @@ dependencies = [
"libc",
]
[[package]]
name = "anyhow"
version = "1.0.104"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
[[package]]
name = "arboard"
version = "3.6.1"
@@ -485,6 +514,12 @@ dependencies = [
"syn 3.0.6",
]
[[package]]
name = "byteorder"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "byteorder-lite"
version = "0.1.0"
@@ -542,6 +577,13 @@ dependencies = [
"thiserror 2.0.21",
]
[[package]]
name = "cadcraft-dwg"
version = "0.1.0"
dependencies = [
"acadrust",
]
[[package]]
name = "cadcraft-dxf"
version = "0.1.0"
@@ -584,6 +626,7 @@ version = "0.1.0"
dependencies = [
"cadcraft-color",
"cadcraft-doc",
"cadcraft-dwg",
"cadcraft-dxf",
"cadcraft-geom",
"cadcraft-render",
@@ -815,6 +858,12 @@ dependencies = [
"libc",
]
[[package]]
name = "core_detect"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f8f80099a98041a3d1622845c271458a2d73e688351bf3cb999266764b81d48"
[[package]]
name = "crc32fast"
version = "1.5.2"
@@ -833,6 +882,25 @@ dependencies = [
"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]]
name = "crossbeam-utils"
version = "0.8.23"
@@ -1046,6 +1114,20 @@ dependencies = [
"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]]
name = "endi"
version = "1.1.1"
@@ -1888,6 +1970,12 @@ dependencies = [
"autocfg",
]
[[package]]
name = "minimal-lexical"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]]
name = "miniz_oxide"
version = "0.8.9"
@@ -1937,6 +2025,12 @@ dependencies = [
"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]]
name = "naga"
version = "30.0.1"
@@ -2011,6 +2105,16 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "num-traits"
version = "0.2.19"
@@ -2710,6 +2814,26 @@ dependencies = [
"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]]
name = "read-fonts"
version = "0.41.0"
@@ -2834,6 +2958,12 @@ version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
[[package]]
name = "ryu"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
[[package]]
name = "same-file"
version = "1.0.6"
+1
View File
@@ -14,6 +14,7 @@ repository = "https://github.com/storytold/cadcraft"
# internal
cadcraft-geom = { path = "crates/geom" }
cadcraft-dxf = { path = "crates/dxf" }
cadcraft-dwg = { path = "crates/dwg" }
cadcraft-color = { path = "crates/color" }
cadcraft-doc = { path = "crates/doc" }
cadcraft-fonts = { path = "crates/fonts" }
+6
View File
@@ -9,6 +9,12 @@ docs/brand/LICENSE-brand.txt.
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
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,
+4 -3
View File
@@ -67,8 +67,9 @@
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
the way decades of drafting habit expect.
- **Open files.** DXF is read and written natively (ASCII and binary, R12 through 2018); DWG is
on the roadmap. Export to SVG and PNG today.
- **Open files.** DXF is read and written natively (ASCII and binary, R12 through 2018), and DWG
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
(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
@@ -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 |
| 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 |
| 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) |
## 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-doc = { workspace = true }
cadcraft-dxf = { workspace = true }
cadcraft-dwg = { workspace = true }
cadcraft-render = { 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.
pub fn read(bytes: &[u8], name: &str) -> Result<Drawing> {
if bytes.starts_with(b"AC10") {
return Err(IoError::Unsupported("DWG (reading .dwg is on the roadmap; save as DXF for now)".into()));
if cadcraft_dwg::is_dwg(bytes) {
let dxf = cadcraft_dwg::dwg_to_dxf(bytes).map_err(IoError::Format)?;
return dxf_read::read(&dxf);
}
match ext(name).as_str() {
"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.
pub fn write(d: &Drawing, name: &str) -> Result<Vec<u8>> {
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()),
"png" => png(d, &Space::Model, 2400, 1600),
e => Err(IoError::Unsupported(e.to_string())),
+1
View File
@@ -35,6 +35,7 @@ impl Class {
pub const TABLE: &[(&str, Class)] = &[
("geom", Class::Layer(0)),
("dxf", Class::Standalone),
("dwg", Class::Standalone),
("color", Class::Layer(0)),
("doc", Class::Layer(1)),
("fonts", Class::Layer(2)),