uncad-cli 0.10.0

CLI for uncad (CAD file summary, JSON export of the parsed model, SVG/PNG rendering, read-only verbs as JSON and as MCP tools).
# uncad

[![CI](https://github.com/iyulab/uncad/actions/workflows/ci.yml/badge.svg)](https://github.com/iyulab/uncad/actions/workflows/ci.yml)
[![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](LICENSE)

An open-source Rust library that parses CAD files (DWG/DXF) into a model and
exports that model as JSON, SVG or PNG. Read-only: it does not write DWG or DXF,
and does not convert between them.

## Quick start

```bash
git submodule update --init   # test fixtures only -- not needed to build (see "Platform")
cargo build --workspace
cargo test --workspace
```

```rust
let db = uncad::parse("drawing.dwg")?;   // same model for DWG and DXF (entities + tables)
println!("{} entities", db.entities.len());

// The header variables that give the numbers a meaning come beside the model:
let (db, header) = uncad::parse_with_header("drawing.dxf")?;
println!("units: {:?}", header.units());   // $INSUNITS; None when the file does not say

// Bytes already in memory (from the network, an archive, ...):
let bytes = std::fs::read("drawing.dwg")?;
let db = uncad::parse_bytes(&bytes, uncad::Format::Dwg)?;

let json = db.to_json(uncad::ToJsonOptions { pretty: true })?;   // the model, serialized as-is
std::fs::write("drawing.json", json)?;

// Rendering is the iron-render-cad crate's (uncad-cli uses it):
let result = iron_render_cad::to_svg(&db, iron_render_cad::ToSvgOptions::default());
std::fs::write("drawing.svg", result.svg)?;
```

## CLI

The binary is `uncad`. From a clone, `cargo install --path crates/uncad-cli`
puts it on your `PATH`; the lines below run it through `cargo run` instead.

```bash
cargo run -p uncad-cli -- drawing.dwg                            # summary: entity count per type
cargo run -p uncad-cli -- drawing.dwg -o drawing.json --pretty   # export the parsed model
cargo run -p uncad-cli -- drawing.dwg -o drawing.svg             # render to SVG (model space)
cargo run -p uncad-cli -- drawing.dwg -o drawing.png             # render to PNG (via SVG)
cargo run -p uncad-cli -- drawing.dwg -o drawing.png --scale 2   # rasterize at twice the size
cargo run -p uncad-cli -- drawing.dwg -o drawing.png --fit 4000  # the longer side 4000 px
cargo run -p uncad-cli -- drawing.dwg -o drawing.png --fit 4000 --stroke 2 --paper dark  # 2 px lines, black page
cargo run -p uncad-cli -- drawing.dwg -o detail.svg --window 0,0,500,300  # just this rectangle (drawing units)
cargo run -p uncad-cli -- drawing.dwg -o drawing.svg --no-trim   # draw far-away outliers too
cargo run -p uncad-cli -- drawing.dwg -o sheet.svg --space paper # sheet borders / title blocks
cargo run -p uncad-cli -- drawing.dwg -o all.svg --space all     # every space in one document
cargo run -p uncad-cli -- export drawing.dwg -o pkg/             # images + JSON an LLM or a vision model reads
```

A render is drawn on a white page by default, where pure white (ACI 7) is drawn
black. `--paper dark` draws it on a black page instead: pure white stays white,
pure black is drawn white, and every other color is the file's -- the light ones
a white page washes out included. Lines are about 1/6000 of the picture's
diagonal unless `--stroke <px>` sets their width in pixels (PNG only).

Questions about a drawing, each answered as one line of JSON -- the result
structure of [`iron-scout-cad`](https://github.com/iyulab/iron-scout-cad) or
[`iron-diff-cad`](https://github.com/iyulab/iron-diff-cad), serialized as it is:

```bash
uncad summarize drawing.dwg                                   # what the drawing contains
uncad summarize drawing.dwg --type circle --layer pipes       # ... and the circles on a layer, each with its box
uncad summarize drawing.dwg --id 812 --detail                 # ... and one entity's model record (the fields `set` takes)
uncad hit-test drawing.dwg --x 120 --y 45 --tolerance 0.5     # the entities at a point
uncad diff rev-a.dwg rev-b.dwg                                # exact numeric difference (by reference ID when the files show one drawing)
uncad diff a.dxf b.dxf --matching geometry --length-tolerance 0.01
uncad diff a.dxf b.dxf --matching geometry --min-similarity 1.5  # ... pairing only equal shapes
uncad diff rev-a.dwg rev-b.dwg --omit within,unstated         # only what changed beyond tolerance
```

A change set lists every field that differs, including those that moved within
tolerance and those one drawing does not state (a drawing saved again in a newer
format states values the older format had no place for). `--omit` leaves either
out -- for an agent reading the answer into a limited context -- and the answer
counts what it left out in `omitted`, so nothing left out reads as unchanged.

One verb edits: `set` changes one field of one entity and writes the result as a
new model JSON file -- never over an existing file, never into its input -- and
answers with the difference it made, as `diff` reports it. The field and the
value are named as the model JSON names them (the value is JSON: a number, a
string in quotes, an object). Every verb reads model JSON as a drawing, so edits
chain, and the last state can be compared with the first -- or rendered, since
every command but `export` (which reads the drawing's header) takes model JSON:

```bash
uncad set part.dwg --id 42 --path radius --value 6.5 -o step1.json
uncad set step1.json --id 42 --path center.x --value 10 -o step2.json
uncad diff part.dwg step2.json                                # both edits, and nothing else
uncad step2.json -o step2.svg                                 # what the edited state looks like
```

`redline` draws that difference on top of the first drawing: the first drawing
exactly as it renders alone, and over it, in red (or `--proposal-color`), what
each changed entity became, what was removed (dashed), and a revision cloud
around every change. A change whose counterpart is uncertain gets a dashed
cloud and no geometry. When the drawing itself uses colors close to the one the
changes are drawn in, the answer lists them and a warning suggests another. It
writes an SVG or PNG file -- never over an existing one -- and answers with what
it marked and what it could not show, and why. `--paper` and `--stroke` mean what
they mean for a render; a stroke is in pixels of the picture, so it takes a PNG --
an SVG, which has none, refuses it:

```bash
uncad redline part.dwg step2.json -o proposal.svg
uncad redline part.dwg step2.json -o proposal.png --fit 2000 --omit within  # 2000 px, beyond tolerance only
uncad redline plan.dwg step2.json -o detail.png --fit 1200 --frame changes  # just the changes, and around them
uncad redline plan.dwg step2.json -o proposal.svg --proposal-color '#0057b8'  # a drawing already in red
uncad redline plan.dwg step2.json -o review.png --fit 1200 --stroke 3 --paper dark  # 3 px lines, black page
```

`uncad mcp` serves `summarize`, `hit-test`, `diff`, `set` and `redline` as [Model Context
Protocol](https://modelcontextprotocol.io) tools over stdio, for an agent to
call. A tool's answer is byte for byte what the command prints; a warning the
command writes to stderr is a further content block. Each call reads its files
again -- the server keeps no state -- and calls run one at a time.

```json
{ "mcpServers": { "uncad": { "command": "uncad", "args": ["mcp"] } } }
```

## Scope

1. **DWG** — read through [LibreDWG]https://www.gnu.org/software/libredwg/
   (GPLv3+), bound directly via Rust FFI (`bindgen`). All versions.
2. **DXF** — ASCII and binary, R2007 and later included, read by the pure-Rust
   `undxf` crate (MIT) rather than by LibreDWG; chosen by file extension
   (`parse`) or by the caller (`parse_bytes`). A drawing's DXF and DWG read to
   the same model except where the two formats state different things
   ([`docs/CAVEATS.md`]./docs/CAVEATS.md, "DXF").
3. **Output** — the parsed model as JSON (`CadDatabase::to_json`, from
   `uncad-model`). SVG and PNG come from the
   [`iron-render-cad`]https://github.com/iyulab/iron-render-cad crate (MIT), which
   `uncad-cli` uses. Writing DWG/DXF is not offered; the write API that existed in
   0.1.0 was removed (see [`CHANGELOG.md`]./CHANGELOG.md).

The model itself -- `CadDatabase`, `Entity`, `Tables` and their JSON form -- is
the [`uncad-model`](https://github.com/iyulab/uncad-model) crate (MIT), re-exported
here as `uncad::model` / `uncad::tables` / `uncad::json`. This crate is one backend
that fills it; anything that only needs to *read* a drawing depends on the model
crate alone and inherits nothing from this crate's license.

## Platform

Pure Rust plus native FFI. WebAssembly and the browser are not targets — the
intended consumers are libraries and binaries (CLI, server, desktop app).

`bindgen` needs `libclang`, so LLVM/Clang has to be installed (Windows:
`winget install LLVM.LLVM`, Ubuntu: `apt install libclang-dev`). The LibreDWG C
sources are vendored into `crates/libredwg-sys/vendor/libredwg/`, so **building
does not need the `lib/libredwg` submodule**. Running `cargo test --workspace`
does: the real-file tests under `crates/uncad/tests/` and `uncad-cli`'s
`tests/documented_invocations.rs` read fixtures from that submodule's
`test/test-data/`. Clone with
`git clone --recurse-submodules`, or run `git submodule update --init` in an
existing clone. Builds and tests pass on Linux (`x86_64-unknown-linux-gnu`) as
well as Windows.

## License

**GPLv3-or-later**. LibreDWG (GPLv3+) is the only third-party component linked
in, and its license carries over. The vendored copy carries local patches;
`crates/libredwg-sys/NOTICE.md` lists each one and is their modification
notice, inside the crate so that it reaches the published tarball. Copyright and license details for
third-party components are in
[`docs/THIRD_PARTY_NOTICES.md`](./docs/THIRD_PARTY_NOTICES.md).

## Repository layout

```
lib/libredwg/            LibreDWG upstream, as a git submodule. Not used by the build --
                         it is the source vendor/ is regenerated from, and where the
                         real-file test fixtures (test/test-data/) come from
crates/
  libredwg-sys/          raw FFI (cc + bindgen). vendor/libredwg/ holds the subset of C
                         sources actually compiled (for publishing to crates.io), with
                         local patches marked "uncad local patch" and listed in
                         NOTICE.md; shim/ holds the C accessors for opaque types, and
                         vendor-config/config.h stands in for autotools
  uncad/                 the safe API: parse() / parse_bytes() -> uncad_model::CadDatabase,
                         and the drawing's Header beside it from parse_with_header()
  uncad-cli/             the CLI binary (uncad)
crates/*/tests/          integration tests against the public API. crates/*/examples/ are
                         manual-check tools, and #[cfg(test)] blocks inside src/*.rs are
                         unit tests -- docs/ARCHITECTURE.md's "Test layout" says which
                         belongs where
scripts/                 sync-libredwg-vendor.sh -- regenerates vendor/ after a submodule
                         update
samples/                 gitignored except its README -- drop any DWG/DXF in here for
                         manual testing, no license clearance needed. No automated tests
                         read from it
docs/                    architecture, known limitations, third-party notices
```

Further reading:

- [`docs/ARCHITECTURE.md`]./docs/ARCHITECTURE.md — crate layout, build system,
  test layout, the FFI/bindgen boundary, thread safety, the entity model
- [`docs/CAVEATS.md`]./docs/CAVEATS.md — entity type coverage, known
  limitations and bugs, cross-platform notes