Metadata-Version: 2.4
Name: dwgforge
Version: 0.2.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

[![PyPI](https://img.shields.io/pypi/v/dwgforge.svg)](https://pypi.org/project/dwgforge/)
[![Python](https://img.shields.io/pypi/pyversions/dwgforge.svg)](https://pypi.org/project/dwgforge/)
[![CI](https://github.com/NOVA-XO/dwgforge/actions/workflows/ci.yml/badge.svg)](https://github.com/NOVA-XO/dwgforge/actions/workflows/ci.yml)
[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/NOVA-XO/dwgforge/blob/main/LICENSE)

**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, and not one
third-party runtime dependency.

> 🇲🇳 **Монголоор бүрэн тайлбар** доор байна: [**Монгол**](#монгол).

```powershell
pip install dwgforge
dwgforge doctor
```

Status: **0.2.0.** It draws and it models solids 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.

**Contents** · [Requirements](#requirements) · [Install](#install) · [Quickstart](#quickstart) ·
[The API in one screen](#the-api-in-one-screen) · [How it works](#how-it-works) ·
[Example](#example) · [3D solids](#3d-solids) · [Why Python?](#why-python-an-honest-verdict) ·
[Known boundaries](#known-boundaries) · [Troubleshooting](#troubleshooting) ·
[Security notes](#security-notes) · [Монгол](#монгол)

---

## Requirements

| | |
| --- | --- |
| **OS** | Windows. `accoreconsole.exe` is Windows-only, so the default backend is too. |
| **AutoCAD** | AutoCAD 2026 or Civil 3D 2026 (older releases work; point dwgforge at them explicitly). `accoreconsole.exe` ships with the install — there is nothing extra to buy or download. |
| **Python** | 3.12 or 3.13. |
| **Packages** | None. `dependencies = []` is a design constraint, not an accident: the default backend must install on a bare interpreter. |

Optional extras: `dwgforge[com]` adds `pywin32` for the experimental COM backend, `dwgforge[dxf]`
adds `ezdxf`, `dwgforge[dev]` adds the test and lint toolchain.

## Install

```powershell
pip install dwgforge
dwgforge doctor
```

`doctor` is the first thing to run and the first thing to report in a bug: it prints the resolved
`accoreconsole.exe`, the environment override, whether the optional packages are present and — via a
live probe — AutoCAD's version, product string, `SECURELOAD` value and `DWGCODEPAGE`.

From a checkout instead, for hacking on the library itself:

```powershell
git clone https://github.com/NOVA-XO/dwgforge.git
cd dwgforge
python -m venv .venv
.\.venv\Scripts\python.exe -m pip install -e ".[dev]"
.\.venv\Scripts\python.exe -m pytest -q
```

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

## Quickstart

```powershell
dwgforge doctor                    # is AutoCAD there, and what does it say about itself?
dwgforge demo out\plan.dwg         # draw the reference drawing
dwgforge demo out\plan.dwg --dry-run   # print the AutoLISP instead, run nothing
```

`demo` is the project's acceptance test in executable form: two layers, five entities, Mongolian
Cyrillic text. If it lands in `out\plan.dwg`, the whole pipeline works on your machine.

### 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. A small template is worth finding —
starting from a Civil 3D metric template makes every output file ~900 KB, where `acadiso` makes the
same drawing ~35 KB.

## The API in one screen

```python
from dwgforge import Drawing, DrawingOptions, Layer, write_dwg, emit_script, emit_lsp, doctor

dwg = Drawing(options=DrawingOptions(dwg_format="2018", ascii_mode="off"))

dwg.add_layer(Layer("ROAD", color=3))  # explicit; raises on a duplicate name
dwg.ensure_layer("BORDER")  # idempotent

dwg.line((0, 0), (100, 50), layer="ROAD")  # every point takes Pt | tuple | any 2/3-sequence
dwg.circle((50, 25), 12.5)
dwg.arc((0, 0), 40, 0.0, 1.57)  # angles in RADIANS, counter-clockwise from +X
dwg.point((10, 10))
dwg.polyline([(0, 0), (100, 0), (100, 60)], closed=True, bulges=[0.0, 0.41, 0.0])
dwg.text((0, 65), "Улаанбаатар 2026", height=2.5, rotation=0.0)
dwg.mtext((0, 75), "Мөр 1\\PМөр 2", height=2.5, width=60.0)

result = write_dwg(dwg, "out/plan.dwg")  # -> RunResult; raises RunFailedError unless check=False
result.ok, result.entities_ok, result.failures, result.artifacts, result.duration_s
print(result.summary())

emit_script(dwg)  # the .scr text that WOULD run. Runs nothing.
emit_lsp(dwg)  # the same program as an APPLOAD-able .lsp
doctor()  # the dict behind the `dwgforge doctor` command
```

Seven entity types today — `Line`, `Circle`, `Arc`, `PointEntity`, `Polyline`, `Text`, `MText` —
each a frozen dataclass that validates in `__post_init__` and raises `GeometryError` on bad input,
before AutoCAD is ever started.

---

## 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`, `solidlisp.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. `solidlisp.py` holds the verbatim helper defuns the solid layer calls. |
| **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`, `solids.py` | `Drawing` accumulates layers and entities; `SolidModel` accumulates 3D bodies and boolean operations. Both tag every item and render a complete validated program. Neither executes anything or imports a 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}
```

**Entities are 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. In the drawing pipeline (`Drawing` → `write_dwg`)
`(command ...)` appears at exactly one site — the save — and `tests/test_protocol.py` scans the
envelope to keep it that way.

**3D solids are the one documented exception.** There is no `entmake` for an ACIS body: it returns a
degenerate object with no extents, so `BOX`, `CYLINDER`, `CONE`, `SPHERE`, `TORUS`, `EXTRUDE` and the
booleans are the only route from AutoLISP to the modeller. `dwgforge.solids` therefore does issue
commands, and pays for it deliberately: every name is `_`-prefixed (defeats a localized AutoCAD) and
`.`-prefixed (defeats a redefined command); the snap and echo variables are already 0 from the
prelude; every command is followed by a bounded drain, because a command left at a prompt eats the
next line of the script; every creation is followed by a post-condition, because a failed command
returns `nil` rather than raising; and the set of commands the layer can emit is closed and pinned by
tests. Nothing else in the system issues a command.

### 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 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/`](https://github.com/NOVA-XO/dwgforge/tree/main/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.

### Cyrillic transport modes

`DrawingOptions(ascii_mode=...)` takes three values:

| Mode | When to use it |
| --- | --- |
| **`off`** (default) | Almost always. The `.scr` carries a UTF-8 BOM, so Cyrillic survives unchanged and reads back identical. |
| **`chr`** | Every character becomes `(chr 1052)`, making the file pure ASCII. On a toolchain that mangles Unicode this is the **only** mode that works for **layer names** and **style names**. |
| **`uplus`** | Only TEXT/MTEXT **content** becomes `\U+XXXX`. It does not work for layer names — `\U+` is a text-renderer code, not a symbol-table code — so dwgforge falls back to `chr` for those automatically. Text grows about 7×. |

---

## 3D solids

`SolidModel` is the same shape as `Drawing` — accumulate, then hand it to `write_dwg` — but it
produces **ACIS bodies** rather than drawn entities: things with a volume, a centre of mass and a
bore you can actually measure with `MASSPROP`.

```python
from dwgforge import SolidModel, write_dwg, solid_count

m = SolidModel()
outer = m.mark()
m.cylinder((0, 0, 0), 57.15, (500, 0, 0))  # DN100 pipe, 500 long
bore = m.mark()
m.cylinder((-0.5, 0, 0), 51.13, (500.5, 0, 0))
m.subtract_since(outer, bore)  # hollow it out

result = write_dwg(m, "out/pipe.dwg")
solid_count(result)  # -> 1
```

An AutoCAD solid command returns `nil`, not the body it made, so there is no handle to keep. Instead
of tracking entity names you place a **mark** and then say "union everything created since that
mark". A part built from eleven cylinders reads as three lines.

| | |
| --- | --- |
| Primitives | `box`, `cylinder`, `cone` (frustum with `top_radius=`), `sphere`, `torus`, `sector_prism` |
| History | `mark()`, `union_since`, `subtract_since(keep, cut)`, `intersect_since`, `move_since` |
| Reporting | `note()` → `RunResult.transcript`; `solid_count(result)` → the body count AutoCAD counted |

Angles are radians, matching `Drawing.arc`. A mark is a typed handle rather than a string, so a mark
from a different model is a Python error before any file is written. Every primitive validates in
`__post_init__`: a zero radius, a zero-length axis or a flat box raises `GeometryError` rather than
leaving a command sitting at a prompt.

---

## 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.**

| 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.

---

## 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.

Released versions are listed in
[CHANGELOG.md](https://github.com/NOVA-XO/dwgforge/blob/main/CHANGELOG.md).

---

# Монгол

## Товчхондоо

**dwgforge** бол Python дээр бичээд **жинхэнэ `.dwg` файл** гаргаж авдаг сан юм.

Ажиллах зарчим нь энгийн: таны Python код AutoLISP эх кодыг үүсгэнэ → түүнийг AutoCAD / Civil 3D
өөрөө дэлгэц харагдахгүйгээр (headless) ажиллуулна → геометр нь DWG файл дотор бууна. Өөрөөр
хэлбэл **DWG-г Autodesk-ийн өөрийнх нь бичигч бичиж байгаа** учир:

- DXF рүү хөрвүүлээд буцаах шат байхгүй,
- файлын форматыг задлан судлах шаардлагагүй,
- ажиллуулахад ямар ч гуравдагч сан суулгах хэрэггүй (`pip install` нь зөвхөн dwgforge өөрөө).

Хэрэглэх ердийн тохиолдол: **олон зургийг нэг загвараар үүсгэх** (жишээ нь эд ангийн каталог,
давтагдсан план, тайлангийн хуудас). Интерактив, хэрэглэгчийн сонголтод шууд хариулах ажилд
тохирохгүй — яагаад гэдгийг доор ["Python мөн үү?"](#python-мөн-үү-өөр-хэл-үү) хэсэгт тайлбарлав.

## Юу шаардлагатай вэ

| | |
| --- | --- |
| **Үйлдлийн систем** | Windows. `accoreconsole.exe` нь зөвхөн Windows дээр байдаг. |
| **AutoCAD** | AutoCAD 2026 эсвэл Civil 3D 2026 (хуучин хувилбар ч болно — зөвхөн зааж өгнө). `accoreconsole.exe` нь суулгацтай хамт ирдэг, нэмж худалдаж авах юмгүй. |
| **Python** | 3.12 буюу 3.13. |
| **Багц** | Байхгүй. Энэ нь санамсаргүй зүйл биш, зориудын хязгаарлалт: үндсэн backend нь юу ч суулгаагүй Python дээр ажиллах ёстой. |

Нэмэлт: `dwgforge[com]` нь COM backend-д зориулж `pywin32`, `dwgforge[dxf]` нь `ezdxf`,
`dwgforge[dev]` нь тест, шалгуурын хэрэгслүүдийг нэмнэ.

## Суулгах

```powershell
pip install dwgforge
dwgforge doctor
```

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

`doctor` бол хамгийн түрүүнд ажиллуулах команд. Тэр нь:

- `accoreconsole.exe`-г олсон эсэх, ямар замаас олсон,
- `DWGFORGE_ACCORECONSOLE` хувьсагч тохируулагдсан эсэх,
- `pywin32`, `ezdxf` байгаа эсэх,
- мөн AutoCAD-ыг үнэхээр асааж үзээд хувилбар, бүтээгдэхүүний нэр, `SECURELOAD`, `DWGCODEPAGE`-ийг
  нь хэлж өгнө.

Ямар нэг асуудал гарвал эхлээд үүнийг ажиллуулаад, гаралтыг нь хараарай.

Санг өөрийг нь засах бол эх кодоос:

```powershell
git clone https://github.com/NOVA-XO/dwgforge.git
cd dwgforge
python -m venv .venv
.\.venv\Scripts\python.exe -m pip install -e ".[dev]"
.\.venv\Scripts\python.exe -m pytest -q
```

## Эхний зураг

```powershell
dwgforge demo out\plan.dwg
```

Энэ нь хоёр давхарга, таван обьект, кирилл бичээстэй жишээ зургийг үүсгэнэ. Хэрэв
`out\plan.dwg` файл гарч ирвэл таны компьютер дээр бүх шат ажиллаж байна гэсэн үг.

`--dry-run` нэмбэл AutoCAD-ыг **огт ажиллуулахгүйгээр** үүсгэсэн AutoLISP кодыг дэлгэц рүү хэвлэнэ —
юу явж байгааг харах хамгийн хурдан арга.

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

Backend нь `accoreconsole.exe`-г дараах дарааллаар хайж, эхний олдсон дээрээ зогсоно:

1. `$env:DWGFORGE_ACCORECONSOLE` хувьсагч,
2. `PATH` дээрх `accoreconsole`,
3. `C:\Program Files\Autodesk\AutoCAD 20*` — шинэ хувилбараас нь эхэлж.

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

Хэрэв энэ хувьсагч **буруу зам заасан бол** dwgforge чимээгүйгээр өөр хувилбар руу шилжихгүй, шууд
алдаа өгнө. Ингэснээр үсэг андуурсны улмаас таны ажил санамсаргүйгээр өөр 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=...)` нь шинэ зураг эхлүүлэхийн оронд **байгаа `.dwg`-г нээж засна**.
Заавал `.dwg` байх ёстой: `.dwt` өгвөл accoreconsole ямар ч мессежгүйгээр 255 кодоор гардаг тул
dwgforge үүнийг Python талдаа шууд татгалзана. Хөнгөн загвар сонгох нь ач холбогдолтой — Civil 3D-ийн
метрийн загвараас эхэлбэл гаралт бүр ~900 KB болдог бол `acadiso` дээр ижил зураг ~35 KB болдог.

## Жишээ код

```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
```

Анхаарах хэдэн зүйл:

- **Кирилл үсгийг шууд бичиж болно** — давхаргын нэр, бичээсийн агуулга хоёуланд нь.
- **Өнцгийг радианаар** өгнө (`arc`), градусаар биш. `dwgforge.geometry.rad(90)` гэж хөрвүүлж болно.
- MTEXT дотор мөр таслахад `\P` кодыг ашиглана — Python дотор `"\\P"` гэж бичихийг мартуузай.
- Цэгийг `Pt`, `tuple`, эсвэл 2/3 элементтэй дараалал байдлаар өгч болно. `(0, 0)` ба `(0, 0, 0)`
  хоёулаа зөв.

## API-ийн тойм

```python
from dwgforge import Drawing, DrawingOptions, Layer, write_dwg, emit_script, emit_lsp, doctor

dwg = Drawing(options=DrawingOptions(dwg_format="2018", ascii_mode="off"))

dwg.add_layer(Layer("ЗАМ", color=3))  # давхардсан нэр өгвөл алдаа өгнө
dwg.ensure_layer("BORDER")  # байхгүй бол үүсгэнэ, байвал дуугүй өнгөрнө

dwg.line(...)  # LINE          dwg.polyline(...)  # LWPOLYLINE (bulge-тэй байж болно)
dwg.circle(...)  # CIRCLE        dwg.text(...)      # TEXT
dwg.arc(...)  # ARC           dwg.mtext(...)     # MTEXT
dwg.point(...)  # POINT

result = write_dwg(dwg, "out/plan.dwg")  # RunResult буцаана
emit_script(dwg)  # ажиллуулахгүйгээр .scr текстийг гаргаж авна
emit_lsp(dwg)  # APPLOAD хийж болох .lsp хэлбэрээр
doctor()  # `dwgforge doctor` командын ард байгаа dict
```

Одоогоор **долоон төрлийн обьект** байна. Тус бүр нь `__post_init__` дотроо утгаа шалгадаг frozen
dataclass — буруу утга өгвөл **AutoCAD асахаас ӨМНӨ** `GeometryError` гарна.

`RunResult` дотор: `ok`, `entities_ok`, `entities_failed`, `failures`, `saved`, `duration_s`,
`transcript`, `artifacts`, `summary()`, `raise_for_status()`.

## 3D биет (солид)

`SolidModel` нь `Drawing`-тэй яг ижил хэлбэртэй — хуримтлуулаад `write_dwg` рүү өгнө — гэхдээ зурсан
обьект биш **ACIS бие** үүсгэнэ: эзэлхүүнтэй, жинтэй, `MASSPROP`-оор хэмжиж болдог зүйл.

```python
from dwgforge import SolidModel, write_dwg, solid_count

m = SolidModel()
outer = m.mark()
m.cylinder((0, 0, 0), 57.15, (500, 0, 0))  # DN100 хоолой, 500 урт
bore = m.mark()
m.cylinder((-0.5, 0, 0), 51.13, (500.5, 0, 0))
m.subtract_since(outer, bore)  # нүхийг нь гаргана

result = write_dwg(m, "out/pipe.dwg")
solid_count(result)  # -> 1
```

AutoCAD-ын солид команд нь үүсгэсэн биеэ буцаадаггүй, `nil` буцаадаг. Тиймээс биет тус бүрийг гараар
хөтлөхийн оронд **тэмдэг (mark)** тавиад "тэр цэгээс хойш үүссэн бүгдийг нэгтгэ" гэж хэлнэ. Арван нэг
цилиндрээс бүтсэн эд анги гурван мөр болж уншигдана.

| | |
| --- | --- |
| Примитив | `box`, `cylinder`, `cone` (`top_radius=` өгвөл таслагдсан конус), `sphere`, `torus`, `sector_prism` |
| Түүх | `mark()`, `union_since`, `subtract_since(keep, cut)`, `intersect_since`, `move_since` |
| Тайлан | `note()` → `RunResult.transcript`; `solid_count(result)` → AutoCAD-ын тоолсон биетийн тоо |

Өнцөг нь радианаар (`Drawing.arc`-тай адил). Тэмдэг нь мөр биш **типтэй бариул** тул өөр моделийн
тэмдэг орвол файл бичихээс өмнө Python алдаа өгнө. Примитив бүр `__post_init__` дотроо шалгагдана:
тэг радиус, тэг урттай тэнхлэг, хавтгай хайрцаг зэрэг нь командыг асуулт дээр гацаахын оронд
`GeometryError` өгнө.

### Яагаад энд команд ашиглаж байна вэ

Дээр "геометрийг `entmake`-ээр үүсгэдэг" гэж бичсэн. Солид бол **баримтжуулсан ганц үл хамаарах
зүйл**: ACIS бие үүсгэх `entmake` байхгүй — оролдвол хэмжээсгүй хоосон бие буцаадаг. Тиймээс
`dwgforge.solids` команд ашиглах бөгөөд түүнийхээ төлбөрийг зориудаар төлдөг:

- команд бүр `_`-тэй (өөр хэлний AutoCAD-ыг ялна), `.`-тэй (дахин тодорхойлсон командыг ялна);
- зүүлт, эхо зэрэг хувьсагч prelude-аас аль хэдийн 0;
- команд бүрийн дараа хязгаарлагдмал `DF:drain` — асуулт дээр орхигдсон команд скриптийн дараагийн
  мөрийг хариулт гэж залгидаг;
- үүсгэлт бүрийн дараа пост-нөхцөл — амжилтгүй команд алдаа өгдөггүй, зүгээр `nil` буцаадаг тул
  үүнгүйгээр биетгүй зураг "амжилттай" гэж тайлагнагдана;
- гаргаж болох командын багц хаалттай бөгөөд тестээр бэхлэгдсэн.

Системд өөр ямар ч газар команд ашигладаггүй.

## Хэрхэн ажилладаг вэ

Систем нь **таван давхаргад** хатуу хуваагдсан. Хамаарал зөвхөн **доошоо** урсана — дээд давхаргыг
доод давхарга дуудахыг `tests/test_layering.py` нь бүх import-ыг AST-ээр уншиж **машинаар** хориглодог.

| Давхарга | Модуль | Хариуцах зүйл |
| --- | --- | --- |
| **L1** | `errors.py`, `lisp.py`, `protocol.py`, `solidlisp.py` | AutoLISP-ийн синтаксын мод, мөн LISP текст үүсгэх эрхтэй **цорын ганц** код: бутархай тооны бичиглэл, мөрийн escape, 32-бит бүхэл тооны хязгаар, кирилл дамжуулалт, мөрийн уртын хязгаар, AutoCAD-тай хийх sentinel гэрээ. |
| **L2** | `geometry.py`, `entities.py` | `Pt` цэг ба обьектуудын frozen dataclass-ууд. Тэдгээр нь `.to_lisp()`-ээр L1 рүү бууна; өөрсдөө мөр ч барихгүй, файлд ч хүрэхгүй. |
| **L3** | `document.py`, `solids.py` | `Drawing` нь давхарга, обьектуудыг; `SolidModel` нь 3D биет, боолийн үйлдлүүдийг хуримтлуулж, тус бүрд нь шошго өгч, бүрэн шалгагдсан хөтөлбөр болгож буулгана. Юу ч гүйцэтгэхгүй, backend-ийг мэдэхгүй. |
| **L4** | `backends/` | `Backend` протокол ба түүний хоёр хэрэгжүүлэлт: `AccoreConsoleBackend` (үндсэн, хамааралгүй) ба туршилтын `ComBackend`. Backend нь бэлэн текст ба зорилтот замыг л авдаг, геометрийн тухай юу ч мэдэхгүй. |
| **L5** | `api.py`, `cli.py` | L3 ба L4-ийг **нийлүүлдэг цорын ганц газар**. `write_dwg()` энд байна. |

```text
  таны скрипт
        |   dwg.line(...) / .circle(...) / .text("Улаанбаатар 2026")
        v
  обьектын dataclass-ууд                                L2  entities.py
        |   .to_lisp()
        v
  AutoLISP-ийн мод --> текст --> .scr файл               L1  lisp.py + protocol.py
        |                        UTF-8 BOM, CRLF, мөр <= 1900 тэмдэгт
        v
  accoreconsole.exe /s job.scr /l en-US                  L4  backends/accore.py
        |   (DF:em "0:LINE" (list (cons 0 "LINE") ...))  x N, дараа нь SAVEAS
        v
     out\plan.dwg      +      out\.dwgforge\plan.{scr,lsp,log}
```

### Яагаад `entmake`, яагаад `(command ...)` биш вэ

Геометрийг **`entmake`**-ээр үүсгэдэг. Шалтгаан нь:

- `entmake` нь `OSMODE` (обьект барих), `ORTHOMODE`, `CLAYER`, `CECOLOR` зэрэг тохиргооноос
  **хамаардаггүй** — команд бол хамаардаг, тэгээд геометр чимээгүй гажина.
- Командын нэр хэл бүрт өөр байдаг; DXF-ийн group код 20 жил хөдлөөгүй.
- Командын асуултын дараалал хувилбар бүрт өөрчлөгддөг; group код өөрчлөгддөггүй.

Бүх системд `(command ...)` **ганц газар** байдаг: файл хадгалах үед.

### Гурван артефакт

Ажиллалт бүрийн дараа гаралтын DWG-ийн хажууд `.dwgforge/` хавтас дотор гурван файл үлдэнэ —
**амжилттай ч, амжилтгүй ч**:

- **`<нэр>.scr`** — үнэхээр ажилласан хөтөлбөр. UTF-8 **BOM-той** (BOM-гүй бол AutoCAD түүнийг
  CP1252 гэж уншаад кирилл үсэг гажина) ба CRLF-тэй, төгсгөлдөө заавал шинэ мөртэй (үгүй бол
  сүүлийн мөр чимээгүй ажиллахгүй атлаа процесс 0 кодоор гардаг).
- **`<нэр>.lsp`** — ижил кодыг хүн уншихад зориулсан хэлбэрээр. AutoCAD дотор `APPLOAD` хийж болно,
  Python байхгүй хамт ажиллагсдад дамжуулж болно. **dwgforge өөрөө үүнийг хэзээ ч `(load)` хийхгүй.**
- **`<нэр>.log`** — AutoCAD-ын бүх гаралт, обьект тус бүрийн үр дүнг оруулаад.

Ямар нэг зүйл буруу болвол эхлээд эдгээр файлыг үзээрэй — асуултын хариу бараг үргэлж тэнд байдаг.

### Амжилтыг exit кодоор ХЭМЖДЭГГҮЙ

Энэ бол хамгийн чухал зарчмуудын нэг. `accoreconsole` нь:

- `; error: divide by zero` гарсан ч **0 кодоор** гардаг,
- `LOAD` амжилтгүй болсон ч **0 кодоор** гардаг,
- үл мэдэгдэх команд өгсөн ч **0 кодоор** гардаг.

Тиймээс dwgforge нь амжилтыг өөрөө шалгана: ажиллах үед угсарсан sentinel байх ёстой, алдааны
тоолуур тэг байх ёстой, алдааны мөр байх ёсгүй, **мөн** диск дээр хоосон биш файл байх ёстой.
Exit кодыг зөвхөн `RunResult.returncode` дотор мэдээллийн зорилгоор хадгалдаг, шийдвэрт хэрэглэдэггүй.

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

`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 тэмдэгтэд хүрсэн, эсвэл хаалт тэнцэхгүй байна. Хоёул accoreconsole-ыг үүрд 100% CPU дээр эргэлдүүлнэ. dwgforge мөрийг 1900-аар таслаж, хаалтыг **бичихээсээ өмнө** шалгадаг тул ихэвчлэн Python алдаа болж гарна. Timeout-ыг хэзээ ч бүү ав — тэр бол сүүлчийн хамгаалалт. |
| **Sentinel алга, "script never started"** | `.scr`-ийн BOM алдагдсан, эсвэл энэ компьютер дээр AutoCAD лицензгүй байна. Лицензийн алдаа бараг чимээгүй өнгөрдөг: exit код 0, гаралт тасарсан, `BEGIN` токен алга. `doctor`-оор шалга. |
| **255 код, ямар ч гаралтгүй** | Загвар болгож `.dwt` өгсөн байна. `.dwg` өг, эсвэл загварыг огт бүү өг. |
| **`.dwl` түгжээ / хадгалж чадсангүй** | Тухайн зураг Civil 3D дээр нээлттэй байна. accoreconsole түүнийг зөвхөн уншихаар нээгээд, хэдэн секунд хэвийн ажилласны эцэст `SAVEAS` дээр унана. Зургийг хаа. |
| **"Do you want to replace it?"** | Гарахгүй. Тэр асуулт headless процессыг үүрд зогсоодог тул dwgforge хадгалахын өмнө зорилтот файлыг устгаад, үлдэгдэл асуултуудыг цэвэрлэдэг. |
| **Нэг обьект зурагдаагүй атлаа амжилттай гэв** | Тийм байх боломжгүй: `entmake` унавал алдааны тоолуур нэмэгдэж, бүхэлдээ `FAIL` болно. `RunResult.failures`-ийг үз — тэнд байгаа шошго нь `drawing.entities[i]` рүү шууд заана. |
| **Кирилл үсэг гажсан** | `DrawingOptions(ascii_mode="chr")`-ийг туршиж үз. |

## Аюулгүй байдал

- dwgforge нь `SECURELOAD`, `TRUSTEDPATHS`-ыг **хэзээ ч бичдэггүй**. Эдгээр нь таны профайлын
  бүртгэлд үлддэг тул "түр зуур" сулруулсан хэрэгсэл нь таны машиныг **бүрмөсөн** сулруулна.
  `doctor` нь зөвхөн уншиж, утгыг нь хэлж өгдөг.
- dwgforge нь `(load)`-г **хэзээ ч дууддаггүй**. Бүх код `.scr` дотор шууд бичигддэг.
- **Хэрэглэгчийн бичсэн мөр хэзээ ч код болж чадахгүй** — зөвхөн AutoLISP-ийн текст утга болно.
  Escape хийх ажил ганц газар (`lisp.py`) хийгддэг, удирдах тэмдэгтийг шууд татгалздаг.
- **Sentinel-ийг хуурамчлах боломжгүй.** Токен бүрийг ажиллах үед `(DF:tok k)` угсардаг тул файл
  дотор бэлнээр байдаггүй; Python тал нь тухайн токеныг агуулсан ямар ч файл бичихээс татгалздаг.
- Дэд процессыг **үргэлж жагсаалт хэлбэрийн `argv`-аар** дууддаг, `shell=True` хэрэглэдэггүй.

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

**Civil 3D-ийн обьектуудыг (alignment, surface, corridor, COGO point) headless горимд үүсгэх
боломжгүй.** Энэ хязгаарлалт нь хууртмаар: `accoreconsole` нь 23 AECC модуль болон Civil 3D-ийн
метрийн загварыг ачаалдаг тул Civil 3D бэлэн байгаа мэт **харагддаг**. Гэвч тэдгээр нь зөвхөн
object enabler юм — `Aecc` командууд тодорхойлогдоогүй, `(vlax-get-acad-object)` нь `nil` буцаана,
улмаас `vla-*` дуудалт бүр `bad argument type: VLA-OBJECT nil` гэж унана. Функцууд нь бүгд
тодорхойлогдсон байдаг тул код зөв мэт уншигдаж, `nil` нь шалтгаанаасаа хол газар дэлбэрдэг.

Эдгээр обьектыг үүсгэхийн тулд **C#/.NET ObjectARX** бүтэн session дотор ашиглах шаардлагатай. Энэ
бол dwgforge-ийн алдаа биш, AutoCAD-ын headless горимын хязгаар — энэ repo дотор хэчнээн ажилласан
ч засагдахгүй.

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

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

Товчхондоо: **энэ ажилд Python тохирсон.** Гэхдээ хариулт бүх тохиолдолд Python талд байхгүй.

| Арга | Жинхэнэ DWG гарах уу | Civil 3D обьект | Бэлтгэл | Дүгнэлт |
| --- | --- | --- | --- | --- |
| **Python → AutoLISP → accoreconsole** (энэ төсөл) | **Тийм** — Civil 3D өөрөө бичнэ | Үгүй | Багц суулгахгүй | **Энд хожиж байна.** Бичихэд хамгийн тухтай атлаа жинхэнэ DWG гаргадаг. |
| **ezdxf** (Python) | **Үгүй** — зөвхөн DXF | Үгүй | `pip install ezdxf` | **Илүү сайн зохиогдсон сан**, гэхдээ энд хожигдоно (доор). |
| **C# / .NET ObjectARX** | Тийм | **Тийм — цорын ганц зам** | Компайл + суулгах мөчлөг | **Илүү сайн API.** Civil 3D обьект хэрэгтэй болсон өдөр зөв хариулт нь энэ. |
| **Цэвэр AutoLISP** | Тийм | Үгүй | Байхгүй | Жижиг интерактив хэрэгсэлд тохиромжтой. Тип ч алга, тест ч алга. |
| **Hy / IronPython** | — | — | — | Мухар зам. Долоо хоног бүү үрээрэй. |

**ezdxf** нь энэ repo дотор байгаа юунаас ч илүү сайн зохиогдсон сан. Гэвч тэр зөвхөн **DXF**
бичдэг. DXF-г DWG болгохын тулд ODA File Converter хэрэгтэй бөгөөд тэр нь зөвхөн **арилжааны бус**
хэрэглээнд үнэгүй — энэ бол техникийн бус, лицензийн асуудал. Нээлттэй эхийн DWG бичигчид ч завсрыг
нөхөхгүй: LibreDWG 0.13.4 нь R2000 ба түүнээс хуучин, ezdwg 0.11.0 нь зөвхөн AC1015 бичдэг.
dwgforge энэ асуултыг бүхэлд нь тойрч гардаг — **гадны файл огт үүсгэдэггүй**, DWG-г Civil 3D бичнэ.

**Хэмжээгээр хэлбэл:** процесс асахад **~2–8 секунд** тогтмол зарцуулагдана, түүний дараа обьект
үүсгэх нь бараг үнэгүй — 5000 `entmake` дуудалт **3.1 секундэд** ажилласан. Тиймээс:

- **Багцаар үүсгэх — тийм.** Нэг зураг ч бай, арван мянган обьект ч бай, тогтмол зардал уусна.
- **Интерактив засвар — үгүй.** Хүний нэг үйлдлийн дотор (< 1 сек) хариулах ёстой, эсвэл
  хэрэглэгчийн сонголтод шууд хариу үзүүлэх ажил бол энэ загварт тохирохгүй. Түүнийг AutoLISP
  эсвэл .NET дээр, session дотор бичих нь зөв.

Дараах нөхцөл үүссэн үед энэ загвараас **шилжих** цаг болсон:

1. **Civil 3D обьект хэрэгтэй болсон** → C#/.NET ObjectARX.
2. **Амьд сонголт, ажиллаж буй командад хариу үзүүлэх хэрэгтэй** → AutoLISP эсвэл .NET, session дотор.
3. **DXF хүлээн зөвшөөрөгдөх гаралт болсон** → шууд ezdxf; тэр илүү тухтай сан.
4. **Зураг тус бүрийн хүлээлт багцын хугацааг иддэг болсон** → нэг COM session-ыг халуун байлгах,
   эсвэл .NET рүү шилжих.

## Цаашид

Өргөтгөх цэгүүд, хэрэгцээтэй болох дарааллаараа:

1. L2-т шинэ обьектын төрлүүд — ellipse, spline, hatch, dimension, leader.
2. Блокийн тодорхойлолт ба `INSERT`.
3. Багц дамжуулалт: олон `Drawing`-ийг нэг халуун `Backend` дээр.
4. `Backend` протоколын ард DXF гаралт (ezdxf, нэмэлт багц).
5. .NET backend — Civil 3D-ийн AECC обьектод хүрэх цорын ганц зам.

Гарсан хувилбаруудыг
[CHANGELOG.md](https://github.com/NOVA-XO/dwgforge/blob/main/CHANGELOG.md)-ээс үзнэ үү.

---

## License

MIT. See [LICENSE](https://github.com/NOVA-XO/dwgforge/blob/main/LICENSE).
Copyright (c) 2026 NOVA-XO.
