Metadata-Version: 2.4
Name: dwgforge
Version: 0.1.0
Summary: Author AutoCAD / Civil 3D geometry in Python by generating AutoLISP and executing it against real DWG files.
Project-URL: Homepage, https://github.com/NOVA-XO/dwgforge
Project-URL: Source, https://github.com/NOVA-XO/dwgforge
Project-URL: Issues, https://github.com/NOVA-XO/dwgforge/issues
Author-email: NOVA-XO <superiornova068@gmail.com>
License-Expression: MIT
License-File: LICENSE
Keywords: autocad,autolisp,cad,civil3d,codegen,dwg,dxf,lisp
Classifier: Development Status :: 3 - Alpha
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: Natural Language :: English
Classifier: Operating System :: Microsoft :: Windows
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Multimedia :: Graphics :: Editors :: Vector-Based
Classifier: Topic :: Scientific/Engineering :: GIS
Classifier: Typing :: Typed
Requires-Python: >=3.12
Provides-Extra: all
Requires-Dist: ezdxf>=1.3; extra == 'all'
Requires-Dist: pywin32>=306; (sys_platform == 'win32') and extra == 'all'
Provides-Extra: com
Requires-Dist: pywin32>=306; (sys_platform == 'win32') and extra == 'com'
Provides-Extra: dev
Requires-Dist: mypy>=1.14; extra == 'dev'
Requires-Dist: pytest-cov>=6.0; extra == 'dev'
Requires-Dist: pytest>=8.3; extra == 'dev'
Requires-Dist: ruff>=0.12; extra == 'dev'
Provides-Extra: dxf
Requires-Dist: ezdxf>=1.3; extra == 'dxf'
Description-Content-Type: text/markdown

# dwgforge

**Write Python. Get a real `.dwg`.** dwgforge generates AutoLISP from a small, typed Python API and
has AutoCAD / Civil 3D itself execute it headlessly, so the geometry lands in a genuine DWG authored
by Autodesk's own writer — no DXF round-trip, no file-format reverse engineering, no `pip install`.

Status: **starter / 0.1.0.** It draws today; the seams are deliberately obvious so it can grow.

Applications built on it live in a repository of their own:
[**dwgforge-apps**](https://github.com/NOVA-XO/dwgforge-apps) — parametric 3D piping parts with a
browser viewport. This repository is the library and nothing else.

---

## Quickstart

Windows PowerShell 5.1 has **no `&&`** — it is a parser error. Use `;` between commands.

```powershell
cd C:\Users\User\Python\dwgforge
python -m venv .venv
.\.venv\Scripts\python.exe -m pip install -e ".[dev]"
.\.venv\Scripts\dwgforge.exe doctor
.\.venv\Scripts\dwgforge.exe demo out\plan.dwg
```

`doctor` tells you whether `accoreconsole.exe` was found and what AutoCAD reports about itself.
`demo` draws the example below into `out\plan.dwg` and is the project's acceptance test.
Add `--dry-run` to print the generated AutoLISP to stdout and run nothing at all.

### Pointing dwgforge at a different AutoCAD

The backend resolves `accoreconsole.exe` in this order, and stops at the first hit:

1. `$env:DWGFORGE_ACCORECONSOLE` — if this is set but does not point at a real file, resolution
   **fails rather than silently falling through**, so a typo never sends your job to the wrong release.
2. `accoreconsole` on `PATH`.
3. `C:\Program Files\Autodesk\AutoCAD 20*`, newest first.

```powershell
$env:DWGFORGE_ACCORECONSOLE = "C:\Program Files\Autodesk\AutoCAD 2024\accoreconsole.exe"
dwgforge doctor
```

Or pin it per-call, which is what you want when a script must target one specific release:

```python
from pathlib import Path

from dwgforge import AccoreConsoleBackend, write_dwg

backend = AccoreConsoleBackend(
    exe=Path(r"C:\Program Files\Autodesk\AutoCAD 2024\accoreconsole.exe")
)
write_dwg(dwg, "out/plan.dwg", backend=backend)
```

`AccoreConsoleBackend(template=...)` opens an existing `.dwg` and edits it instead of starting from
your profile's default template. It must be a `.dwg`: a `.dwt` makes accoreconsole exit 255 with no
message, so dwgforge refuses one at the Python boundary.

---

## How it works

Five strictly stacked layers. Dependencies flow **downward only**, and `tests/test_layering.py`
enforces that mechanically by AST-scanning every import — you do not have to take it on trust.

| Layer | Module | Responsibility |
| --- | --- | --- |
| **L1** | `errors.py`, `lisp.py`, `protocol.py` | The s-expression AST and the *only* code allowed to build LISP text: float formatting, string escaping, the 32-bit integer clamp, Cyrillic transport, the line-length cap, and the two-way sentinel contract with AutoCAD. |
| **L2** | `geometry.py`, `entities.py` | `Pt` plus frozen entity dataclasses that lower themselves to L1 nodes via `.to_lisp()`. They never build strings and never touch a path. |
| **L3** | `document.py` | `Drawing` accumulates layers and entities, tags each one, and renders a complete validated program. It executes nothing and imports no backend. |
| **L4** | `backends/` | A `Backend` protocol plus `AccoreConsoleBackend` (the zero-dependency default) and the optional, experimental `ComBackend`. Backends receive rendered text and a target path; they know nothing about geometry. |
| **L5** | `api.py`, `cli.py` | The only place L3 and L4 are composed. `write_dwg()` lives here. |

```text
  your_script.py
        |   dwg.line(...) / .circle(...) / .text("Улаанбаатар 2026")
        v
  frozen entity dataclasses                             L2  entities.py
        |   .to_lisp()
        v
  s-expression AST  --> render --> .scr text            L1  lisp.py + protocol.py
        |                          UTF-8 BOM, CRLF, <=1900 chars per line
        v
  accoreconsole.exe /s job.scr /l en-US                 L4  backends/accore.py
        |   (DF:em "0:LINE" (list (cons 0 "LINE") ...))  x N, then SAVEAS
        v
     out\plan.dwg      +      out\.dwgforge\plan.{scr,lsp,log}
```

Geometry is created with **`entmake`**, never with `(command ...)`. `entmake` is immune to `OSMODE`,
`ORTHOMODE`, `CLAYER` and `CECOLOR`, needs no localized command names, and DXF group codes have not
moved in twenty years — unlike prompt sequences. `(command ...)` appears at exactly one site in the
whole system: the save.

### The artifact trio

Every run drops three files in `.dwgforge/` beside the output DWG, on success **and** on failure:

- **`<stem>.scr`** — the program that actually ran. UTF-8 **with a BOM** (without one, AutoCAD
  decodes it as CP1252 and Cyrillic turns to mojibake) and CRLF with a mandatory trailing newline
  (without it the last line is silently never executed and the process still exits 0).
- **`<stem>.lsp`** — the same forms, unwrapped, for humans. `APPLOAD` it in the GUI and hand it to a
  colleague who has no Python. **dwgforge never `(load)`s this file** — see Security notes.
- **`<stem>.log`** — the decoded transcript, including every per-entity result.

Success is never guessed from the exit code. `accoreconsole` returns **0** after
`; error: divide by zero`, after a failed `LOAD`, and after an unknown command. dwgforge instead
requires a runtime-assembled sentinel, a zero failure counter, a clean error scan, *and* a non-empty
file on disk. The exit code is recorded in `RunResult.returncode` and never consulted.

---

## Example

```python
from pathlib import Path

from dwgforge import Drawing, Layer, write_dwg

dwg = Drawing()
dwg.add_layer(Layer("ЗАМ-ТЭНХЛЭГ", color=3))
dwg.add_layer(Layer("BORDER", color=7))

dwg.line((0, 0), (100, 50), layer="ЗАМ-ТЭНХЛЭГ")
dwg.circle((50, 25), 12.5, layer="ЗАМ-ТЭНХЛЭГ")
dwg.polyline([(0, 0), (100, 0), (100, 60), (0, 60)], closed=True, layer="BORDER")
dwg.text((0, 65), "Улаанбаатар 2026", height=2.5, layer="ЗАМ-ТЭНХЛЭГ")
dwg.mtext((0, 75), "Мөр 1\\PМөр 2", height=2.5, width=60.0, layer="ЗАМ-ТЭНХЛЭГ")

result = write_dwg(dwg, Path("out/plan.dwg"))
print(result.summary())
# -> accore OK  entities=5/5  layers=2  saved=out\plan.dwg  1.9s
print(result.artifacts["scr"])  # the generated .scr, kept for debugging
print(result.artifacts["lsp"])  # same program as a hand-loadable .lsp (APPLOAD in the GUI)
```

Runnable versions live in [`examples/`](examples/):
`01_hello_dwg.py` is the above; `02_mongolian_labels.py` writes the same drawing three times, once
per Cyrillic transport mode, and shows a bulged polyline.

---

## Why Python? An honest verdict

The brief asked whether something would beat Python here. It was worth asking, and the answer is not
a uniform yes for Python. Here is where each option actually wins.

| Approach | Native DWG out | Civil 3D objects | Setup cost | Honest verdict |
| --- | --- | --- | --- | --- |
| **Python -> AutoLISP -> accoreconsole** (this project) | **Yes** — written by Civil 3D itself | No (headless limit) | Zero packages | **Wins here.** Best authoring ergonomics that still produces a real DWG. |
| **ezdxf** (Python) | **No** — DXF only | No | `pip install ezdxf` | **The better library**, and it loses anyway. See below. |
| **C# / .NET ObjectARX** | Yes | **Yes — the only supported route** | Compile + deploy cycle | **The better API.** The right answer the day you need Civil 3D objects. |
| **Pure AutoLISP** | Yes | No | None | Wins for tiny interactive utilities. No types, no tests, no libraries. |
| **Hy / IronPython** | — | — | — | Dead ends. Do not spend a week finding out. |

**ezdxf** is a genuinely better-designed library than anything in this repo, with real entity
objects and no subprocess. It writes **DXF only**. Converting DXF to DWG needs the proprietary ODA
File Converter, which is free for **non-commercial use only** and is not installed here — a licence
problem, not a technical one, and the wrong thing to build a civil-engineering workflow on. The
open-source DWG writers do not close the gap: GNU LibreDWG 0.13.4 writes R2000 and older; ezdwg
0.11.0 writes AC1015 only. Neither writes a modern DWG. dwgforge sidesteps the entire question by
never producing a foreign file: Civil 3D writes the DWG.

**C#/.NET ObjectARX is the better API**, and this is where Python genuinely loses. It is typed
against the real object model, it runs in-process, and it is the **only supported way to create
Civil 3D objects** — alignments, surfaces, corridors, COGO points. The costs are real: a
compile/deploy cycle instead of editing a script, and a forced retarget every release (2026 targets
`net8.0-windows`; 2027 moves to .NET 10). Choose it when the deliverable *is* Civil 3D objects.

**Where Python wins for this project, concretely:** zero pip installs on a bare interpreter, a real
DWG authored by Civil 3D, roughly 2–8 seconds per drawing, and an emitted `.lsp` a colleague can
`APPLOAD` with no Python installed at all.

### The dividing line, numerically

The cost is a **fixed process start of ~2–8 s**, then entity creation is nearly free — 5000 inline
`entmake` calls ran in **3.1 s**. So:

- **Batch generation: yes.** One drawing or ten thousand entities, the fixed cost amortizes away.
- **Interactive editing: no.** Anything that must answer inside a single human interaction
  (< 1 s), or react to what the user has selected in a live GUI, is the wrong shape for this design.
  Write that in AutoLISP or .NET, inside the session.

### Switch triggers

Move off this design when any of these becomes true:

1. **You need Civil 3D objects** (alignment, surface, corridor, COGO point) -> C#/.NET ObjectARX.
2. **You need to respond to a live selection or a running command** -> AutoLISP or .NET in-session.
3. **DXF becomes an acceptable deliverable** -> ezdxf, immediately; it is the nicer library.
4. **Per-drawing latency dominates your batch** -> keep one COM session warm, or move to .NET.

---

## Known boundaries

**Civil 3D objects cannot be created headlessly, and the failure mode is seductive.**
`accoreconsole` loads 23 AECC modules and a Civil 3D metric template, so it *looks* like Civil 3D is
right there. Those are **object enablers only**: every `Aecc` command is undefined, and
`(vlax-get-acad-object)` returns `nil`, which means every `vla-*` call dies with
`bad argument type: VLA-OBJECT nil` — and because those functions are all *defined*, naive code
reads as correct until the `nil` propagates somewhere far from the cause. Alignments, surfaces,
corridors and COGO points need C#/.NET ObjectARX in a full session. This is not a dwgforge bug and
no amount of work in this repo will fix it.

Not in the starter, with where each one goes:

| Missing | Where it would be added |
| --- | --- |
| More entity types (ellipse, spline, hatch, dimension) | One frozen dataclass in **L2** `entities.py`. Nothing else changes. |
| Block definitions and `INSERT` | **L2** entity + a `Drawing.block()` registry in **L3**. |
| Dynamic blocks, xrefs | Need a live session: **L4** `ComBackend`, or a future .NET backend. |
| Civil 3D AECC objects | A new **L4** .NET backend. Not reachable from AutoLISP at all. |
| A DXF / ezdxf output path | Implement the **L4** `Backend` protocol. Geometry above it is unchanged. |
| Batch pipelines | Many `Drawing`s, one `Backend`. Already supported by the seam; no new code needed. |

`ComBackend` is **experimental** and refuses to run unless constructed with
`allow_active_document=True`, because it draws into whatever drawing you currently have open. It
cannot capture a transcript, so it infers success from the output file alone. It exists mainly to
prove the `Backend` seam with two implementations. Prefer the headless default.

---

## Troubleshooting

**Run `dwgforge doctor` first.** It reports the resolved `accoreconsole.exe`, the environment
override, whether `pywin32` and `ezdxf` are present, and — via a live probe — AutoCAD's version,
product string, `SECURELOAD` value and `DWGCODEPAGE`.

| Symptom | Cause and fix |
| --- | --- |
| **Run times out** | A physical `.scr` line reached ~2048 characters, or the parentheses are unbalanced. Both make accoreconsole spin at 100% CPU forever. dwgforge caps lines at 1900 and validates balance *before writing*, so this should surface as a Python exception instead — if it ever times out anyway, read `.dwgforge/<stem>.scr` and `.log`. Never remove the subprocess timeout; it is the last line of defence. |
| **No sentinel in the transcript / "script never started"** | The `.scr` lost its UTF-8 BOM, or AutoCAD is not licensed on this machine. A licence failure is nearly silent: exit 0, truncated transcript, no `BEGIN` token. Run `dwgforge doctor`. |
| **Exit 255, no output at all** | A `.dwt` was passed as the template. Pass a `.dwg`, or omit the template and let accoreconsole use your profile default. dwgforge normally refuses this before spawning anything. |
| **`.dwl` lock / save fails** | The drawing is open in the Civil 3D GUI. accoreconsole opens it read-only and `SAVEAS` fails after several seconds of apparently normal work. Close it. dwgforge pre-checks for a sibling `.dwl`, but a lock taken *after* the check still fails. |
| **"Do you want to replace it?"** | Cannot happen. That prompt blocks a headless process forever even with `FILEDIA 0`, so `DF:saveas` deletes the target first and then drains up to 8 residual prompts. |
| **An entity is missing but the run passed** | It cannot pass — a failed `entmake` increments the failure counter and forces a `FAIL` verdict. Check `RunResult.failures`; each tag maps straight back to `drawing.entities[i]`. |
| **Cyrillic is garbled in the drawing** | Try `DrawingOptions(ascii_mode="chr")`. Layer and style *names* on a non-Unicode toolchain need `chr`; `uplus` is for text content only. |

---

## Security notes

- dwgforge **never writes `SECURELOAD` or `TRUSTEDPATHS`.** Both persist in your user profile
  registry across sessions, so a tool that "temporarily" relaxes them is permanently weakening your
  machine. `doctor` only *reads* `SECURELOAD` and reports it.
- dwgforge **never calls `(load)`.** Every form is inlined into the `.scr`, which is exempt from
  `SECURELOAD` entirely. The companion `.lsp` is a debug artifact you may load by hand; the tool
  never does.
- **User strings can only ever become AutoLISP string literals, never code.** All escaping happens in
  one place (`lisp.py`), control characters are rejected outright, and a payload like
  `a"); (command "_.ERASE"` comes out as a quoted literal that cannot terminate itself.
- **Sentinel forgery is structurally impossible.** Because `.scr` lines echo their own source, a
  token written literally anywhere in the file would be a guaranteed false positive. Every token is
  assembled at runtime by `(DF:tok k)`, and a Python-side validator refuses to write any file
  containing the contiguous token — even if it arrived via a layer name you chose.
- **Subprocess is always invoked with a list `argv`, never `shell=True`,** with `stdin` connected to
  the null device.

---

## Монгол

### Юу хийдэг вэ?

dwgforge бол **Python дээр бичээд жинхэнэ `.dwg` файл гаргаж авдаг** сан юм. Таны Python код
AutoLISP эх кодыг үүсгэнэ, түүнийг AutoCAD / Civil 3D өөрөө дэлгэц харагдахгүйгээр (headless)
ажиллуулж, геометр нь DWG файл дотор бууна. Ингэснээр DXF рүү хөрвүүлэх шат байхгүй, файлын
форматыг задлах шаардлагагүй, гуравдагч сан суулгах ч хэрэггүй — DWG-г Civil 3D өөрөө бичиж байгаа.

### Суулгах

PowerShell 5.1 дээр **`&&` тэмдэг ажиллахгүй** (parser error гарна). Оронд нь `;` хэрэглэнэ.

```powershell
cd C:\Users\User\Python\dwgforge
python -m venv .venv
.\.venv\Scripts\python.exe -m pip install -e ".[dev]"
.\.venv\Scripts\dwgforge.exe doctor
.\.venv\Scripts\dwgforge.exe demo out\plan.dwg
```

`doctor` нь `accoreconsole.exe` олдсон эсэх, AutoCAD-ын хувилбар зэргийг шалгаж хэлнэ. Ямар нэг
асуудал гарвал **хамгийн түрүүнд үүнийг ажиллуул.** `demo` нь жишээ зургийг `out\plan.dwg` рүү
зурна. `--dry-run` нэмбэл AutoCAD-ыг огт ажиллуулахгүйгээр үүсгэсэн AutoLISP кодыг дэлгэцэнд хэвлэнэ.

### Өөр хувилбарын AutoCAD руу заах

```powershell
$env:DWGFORGE_ACCORECONSOLE = "C:\Program Files\Autodesk\AutoCAD 2024\accoreconsole.exe"
dwgforge doctor
```

Хэрэв энэ хувьсагч буруу зам заасан бол dwgforge **чимээгүй өөр хувилбар руу шилжихгүй**, шууд
алдаа өгнө — ингэснээр таны ажил санамсаргүйгээр буруу release дээр очихгүй.

### Жишээ код

```python
from pathlib import Path

from dwgforge import Drawing, Layer, write_dwg

dwg = Drawing()
dwg.add_layer(Layer("ЗАМ-ТЭНХЛЭГ", color=3))
dwg.add_layer(Layer("BORDER", color=7))

dwg.line((0, 0), (100, 50), layer="ЗАМ-ТЭНХЛЭГ")
dwg.circle((50, 25), 12.5, layer="ЗАМ-ТЭНХЛЭГ")
dwg.polyline([(0, 0), (100, 0), (100, 60), (0, 60)], closed=True, layer="BORDER")
dwg.text((0, 65), "Улаанбаатар 2026", height=2.5, layer="ЗАМ-ТЭНХЛЭГ")
dwg.mtext((0, 75), "Мөр 1\\PМөр 2", height=2.5, width=60.0, layer="ЗАМ-ТЭНХЛЭГ")

result = write_dwg(dwg, Path("out/plan.dwg"))
print(result.summary())
```

Давхарга (layer) болон бичээсийн нэрийг кирилл үсгээр шууд бичиж болно. MTEXT дотор мөр таслахад
`\P` кодыг ашиглана — Python дотор `"\\P"` гэж бичихийг анхаар.

### Кирилл үсгийн горим

`DrawingOptions(ascii_mode=...)` гурван утга авна:

| Горим | Хэзээ хэрэглэх |
| --- | --- |
| **`off`** (үндсэн) | Ихэнх тохиолдолд **үүнийг хэрэглэ.** `.scr` файл нь UTF-8 BOM-той тул кирилл үсэг гажихгүй, эргэж уншихад яг адилхан гарна. |
| **`chr`** | Үсэг бүрийг `(chr 1052)` болгож хувиргана, үүссэн файл нь цэвэр ASCII. Хуучин буюу Unicode дэмждэггүй орчинд **давхаргын нэр**, **style-ийн нэр** бичихэд зөвхөн энэ горим ажиллана. |
| **`uplus`** | Зөвхөн TEXT/MTEXT-ийн **агуулгыг** `\U+XXXX` болгоно. Давхаргын нэрэнд ажиллахгүй тул dwgforge автоматаар `chr` руу шилжүүлнэ (`\U+` нь текст зурагчийн код болохоос хүснэгтийн нэрийн код биш). Текстийн урт ~7 дахин нэмэгддэг. |

Эргэлзвэл `off` дээр үлдээ. `chr` нь давхаргын нэр гажсан үед хэрэглэх шийдэл.

### Алдаа олох

Юуны өмнө `dwgforge doctor`. Дараа нь:

- **Хөтөлбөр царцаж, timeout болов** — `.scr` файлын нэг мөр хэт урт (~2048 тэмдэгт) эсвэл хаалт
  тэнцэхгүй байна. dwgforge үүнийг файл бичихээс өмнө шалгадаг тул ихэвчлэн Python алдаа болж
  гарна. `.dwgforge\<нэр>.scr` ба `.log` файлыг үз.
- **Sentinel алга, "script never started"** — `.scr` файлын BOM алдагдсан, эсвэл энэ компьютер дээр
  AutoCAD-ын лиценз ажиллахгүй байна. Лицензийн алдаа бараг чимээгүй өнгөрдөг (exit код 0 гарна),
  тиймээс `doctor`-оор шалга.
- **`.dwl` түгжээ / хадгалж чадсангүй** — тухайн зураг Civil 3D дээр нээлттэй байна. Хаа.
- **"Do you want to replace it?"** — гарахгүй. dwgforge хадгалахын өмнө хуучин файлыг устгадаг.
- **Нэг обьект зурагдаагүй** — `RunResult.failures` дотроос шалга. Тэнд байгаа tag нь
  `drawing.entities[i]` рүү шууд заана.

### Хязгаарлалт

**Civil 3D-ийн обьектуудыг (alignment, surface, corridor, COGO point) headless горимд үүсгэх
боломжгүй.** `accoreconsole` нь AECC модулиудыг ачаалдаг тул Civil 3D бэлэн байгаа мэт **харагддаг**,
гэвч тэдгээр нь зөвхөн object enabler юм: `Aecc` командууд ажиллахгүй, `(vlax-get-acad-object)` нь
`nil` буцаана. Эдгээр обьектыг үүсгэхийн тулд C#/.NET ObjectARX ашиглах шаардлагатай. Энэ бол
dwgforge-ийн алдаа биш, харин AutoCAD-ын headless горимын хязгаар юм.

Мөн энэ анхны хувилбарт **блок, xref, dynamic block** ороогүй. Шинэ обьектын төрөл нэмэх нь
`entities.py` дотор нэг dataclass бичихтэй тэнцэнэ — үүнээс өөр юу ч өөрчлөгдөхгүй.

### Python мөн үү, өөр хэл дээрүү?

Товчхондоо: **энэ ажилд Python тохирсон.** Сан суулгах шаардлагагүй, DWG-г Civil 3D өөрөө бичнэ,
нэг зураг ~2–8 секунд, гаргаж авсан `.lsp` файлыг Python байхгүй хамт ажиллагсад `APPLOAD` хийж
ашиглаж болно. Гэхдээ Python хожигдох хоёр тохиолдол бий: **Civil 3D-ийн обьект үүсгэх** (C#/.NET
хэрэгтэй) болон **хэрэглэгчийн сонголтод шууд хариу үзүүлэх интерактив ажил** (AutoLISP эсвэл .NET
шууд session дотор). Багц (batch) байдлаар олон зураг үүсгэх ажилд Python хамгийн тохиромжтой.
Дэлгэрэнгүйг дээрх "Why Python?" хэсгээс уншина уу.

---

## Roadmap

The extension points, in the order they are likely to be wanted:

1. More L2 entity types — ellipse, spline, hatch, dimension, leader.
2. Block definitions and `INSERT`.
3. Batch pipelines: many `Drawing`s over one warm `Backend`.
4. A DXF backend behind the existing `Backend` protocol (ezdxf, optional extra).
5. A .NET backend — the only route to Civil 3D AECC objects.

## License

MIT. See [LICENSE](LICENSE). Copyright (c) 2026 NOVA-XO.
