{"id":26873,"plugin_id":"plugins_6ac09476ef008191a35887b22b0d048a","kind":"skill","collection_source":"plugin_package","comparison_source":null,"observed_at":"2026-10-06T00:02:41.551Z","digest":"7fdb7ac6f919b02faf9b91b89f75bb990ef4df303d4ef71129427b75aa9f6d07","against":null,"payload":{"description":"Generate, regenerate, and validate 2D DXF drawings from Python build123d sources. Use for DXF files, `.py` drawing scripts, @dxf models, 2D profiles, outlines, templates, gaskets, panels, flat patterns, laser/plasma/waterjet cut layouts, and 2D drawing exports of CAD geometry. Open and visually review existing DXF files in CAD Viewer.","included_files":[{"relative_path":"LICENSE","size_in_bytes":1074},{"relative_path":"agents/openai.yaml","size_in_bytes":300},{"relative_path":"references/generator-templates.md","size_in_bytes":6210},{"relative_path":"requirements.txt","size_in_bytes":25}],"name":"dxf","skill_md_contents":"---\nname: dxf\ndescription: Generate, regenerate, and validate 2D DXF drawings from Python build123d sources. Use for DXF files, `.py` drawing scripts, @dxf models, 2D profiles, outlines, templates, gaskets, panels, flat patterns, laser/plasma/waterjet cut layouts, and 2D drawing exports of CAD geometry. Open and visually review existing DXF files in CAD Viewer.\nlicense: MIT\n---\n\n# DXF generation and validation\n\nProvenance: maintained in [earthtojake/text-to-cad](https://github.com/earthtojake/text-to-cad).\nUse the installed local skill files as the runtime source of truth; the\nrepository link is only for provenance and release review.\n\n## Setup\n\nThis skill's commands are thin entrypoints over the `cadgen` distribution, which\ncarries the Python build runtime and the JavaScript it executes. Install it once:\n\n```bash\npython -m pip install -r requirements.txt\n```\n\nDrawings are build123d geometry, so a drawing build loads the CAD kernel like a\nSTEP build does (~2.5s cold; the warm daemon absorbs it on re-runs).\n`cadgen dxf snapshot` needs no Node at all: it flattens the drawing with `ezdxf`\n(which arrives with cadgen) and paints it in the bundled headless browser.\n\n## Purpose\n\nCreate or modify 2D DXF drawings from natural-language requirements or from CAD\ngeometry, generate validated drawing artifacts, and return checked outputs. A\nDXF drawing's source of truth is a Python file named `<name>.py` defining one\nparameterless `@dxf` model function.\n\n**A drawing is a model.** It has the same wrapper, record, freshness gate and\nbuild job a `@step` part has; its one output is the `.dxf` file; it has no\ngeometry tree (nothing links to a drawing). Every run writes the sibling\n`<name>.dxf` (or the `out=` the decorator names); an unchanged source is a\nno-op; a drawing that calls a part model — `bracket()` inside its body — is\nstale whenever that part's GEOMETRY changes and current when it does not;\n`cadgen store why <drawing>.py` explains the verdict; `--force` rebuilds it\nanyway. The CAD Viewer and `dxf snapshot` read the `.dxf` file itself, so the\nfile you hand a cutting service, the file the viewer draws and the file a\nsnapshot renders are one and the same.\n\n## The contract\n\n**A `@dxf` function takes no parameters and returns build123d 2D geometry. The\nengine writes the DXF.** You never construct a document, name a file, or place\nan entity — the same division of labor `@step` has.\n\n```python\nfrom cadgen import build123d as bd\nfrom cadgen import dxf\n\n\nHOLE_D = 4.5\n\n\n@dxf\ndef gasket():\n    with bd.BuildSketch() as cut:\n        bd.Rectangle(60, 40)\n        bd.Circle(HOLE_D / 2, mode=bd.Mode.SUBTRACT)\n    return cut.sketch          # bare shape -> the CUT layer\n\n\nif __name__ == \"__main__\":\n    gasket()\n```\n\n- **Bare shape** → one `CUT` layer. That is the whole contract for most drawings.\n- **`{layer: shape}`** → named layers, when the drawing genuinely has more than\n  one CAM operation (`CUT` / `ENGRAVE` / `SCORE`). A `Compound` whose children\n  are all labelled means the same thing.\n- **No parameters.** Dimensions are module constants (`HOLE_D = 4.5`) or\n  constants imported from the part the drawing derives from; a different\n  drawing is a different file.\n- **Text** is `bd.Text(...)` engraved OUTLINES on a marking layer, never a DXF\n  `TEXT` entity: cut and marking toolchains consume geometry, and font rendering\n  inside CAM is unreliable.\n- **Geometry must lie in the XY plane.** A face taken from a solid sits at that\n  solid's height; relocate it (`flatten.flatten_face(face)`, or\n  `bd.Location((0, 0, -z)) * face`). The engine REFUSES off-plane geometry rather\n  than silently writing its XY shadow.\n- **Output bytes are a function of the geometry.** Layers are sorted by name and\n  entities by geometric content, so an unchanged drawing rebuilds to an identical\n  file, cold or warm, on any machine.\n\n## The three DXF workflows\n\nCopy the full template for the applicable workflow from\n`references/generator-templates.md` when creating a new drawing.\n\n1. **Drafted from scratch** (gaskets, panels, templates, cut layouts with no 3D\n   model behind them): a `<name>.py` that builds sketches and returns them.\n\n2. **Flat pattern of a generated STEP part**: a drawing script beside the model\n   it derives from, with its OWN stem (one model per file — `bracket_drawing.py`\n   beside `bracket.py`). Import the model and call it, exactly as an assembly\n   composes a child: importing never builds, and inside the drawing's build the\n   call returns the part's geometry (building the part first if it is stale).\n\n   ```python\n   from cadgen import dxf, flatten\n   from bracket import bracket        # a child: tracked by its RESULT\n\n   KERF = 0.15\n\n\n   @dxf\n   def bracket_drawing():\n       return flatten.flat_pattern(bracket(), coordinate=3.0, kerf=KERF)\n\n\n   if __name__ == \"__main__\":\n       bracket_drawing()\n   ```\n\n   The drawing's record pins the part's tree, so a part edit that changes its\n   geometry makes the drawing stale, and one that does not (a comment, a\n   refactor, a colour) leaves it current. Constants imported from the part\n   (`from bracket import THICKNESS`) are tracked by value the same way.\n\n3. **Flat pattern of an imported STEP** (a `.step`/`.stp` with no Python source):\n   read it with `cadgen.read_step` (warm from the store, the same geometry as\n   `build123d.import_step`). Like every file a build reads, it is an input:\n   replacing the vendor STEP makes the drawing stale on its own, with no `--force`.\n\n   ```python\n   from pathlib import Path\n\n   from cadgen import dxf, flatten, read_step\n\n   _HERE = Path(__file__).resolve().parent\n\n   KERF = 0.15\n\n\n   @dxf\n   def panel_flat():\n       panel = read_step(_HERE / \"imported\" / \"vendor_panel.step\")   # recorded input\n       return flatten.flat_pattern(panel, coordinate=3.0, kerf=KERF)\n\n\n   if __name__ == \"__main__\":\n       panel_flat()\n   ```\n\n   **Never read a STEP this project generates.** Reading the `.step` a `@step`\n   model writes is not a loop, it is a drawing whose input changes on every run of\n   the model: the freshness gate can never say \"current\", every build is a full\n   rebuild, and the flat pattern depends on what the last run left on disk. Keep\n   source STEPs in an `imported/` directory beside the drawing, committed like any\n   other input — input path and output path being different files is the whole\n   rule. For a STEP this project DOES generate, use workflow 2 instead: import the\n   model script and call it, which is tracked by result and never touches an\n   artifact.\n\nOne model per file is the recommendation, and a drawing gets its own script:\na file MAY declare several models — two `@dxf` drawings, or a `@dxf` beside a\n`@step` — and each is its own record, output and job (a sole model writes\n`<file>.dxf`; models sharing a file write `<function>.dxf`), but they share the\nfile's closure, so editing one rebuilds them all. A drawing composes models,\nnever the reverse: calling a `@dxf` function from a `@step` body is just its 2D\ngeometry and links nothing. The viewer catalog is artifacts-only: scripts never\nlist; the `.dxf` the run writes is the entry the viewer renders.\n\n## Use this skill when\n\nUse this skill when the user asks for DXF files, 2D drawings, profiles, outlines,\ntemplates, gaskets, panels, flat patterns, or cut layouts for laser, plasma,\nwaterjet, or CNC routing.\n\nUse `$cad` for the 3D part or assembly a DXF derives from. Use `$sendcutsend` for\nSendCutSend-specific upload preflight.\n\n## Defaults\n\nUse these defaults unless the user specifies otherwise:\n\n- Units: millimeters. The engine sets them; a drawing never declares units.\n- Geometry lives at 1:1 scale in the XY plane.\n- Cut profiles close. Open contours belong on bend/engrave/reference layers —\n  generation validation enforces this (see Validation).\n- For CAD-backed parts, derive contours from the real topology with\n  `cadgen.flatten` rather than redrawing them: `planar_faces` selects,\n  `flatten_face` lays a face into XY exactly, `union_faces` fuses, and\n  `flat_pattern` does all of it in one call. Hand-drawn parametric outlines only\n  when there is no reliable 3D topology.\n- Kerf / tool-radius compensation is `flatten.offset_profile(shape, amount)` or\n  `flat_pattern(..., kerf=...)`; never hand-offset coordinates.\n- **Curves stay curves.** The union and the offset are exact OCC operations, so a\n  filleted corner exports as an `ARC` and a hole as a `CIRCLE`, kerf included. A\n  profile that comes out as hundreds of short `LINE`s means something fell back\n  to the sampled path — investigate rather than accept it.\n- Layers carry intent: keep cut geometry and bend/fold lines on separate layers,\n  and include \"bend\" in bend-layer names so downstream tools classify them as\n  bends rather than cuts.\n- DXF layers are drawing structure, not STEP part/assembly structure.\n\n## Tool\n\n```bash\npython <drawing>.py [flags]                    # its __main__ calls the @dxf model, which writes the .dxf\ncadgen dxf snapshot <drawing.dxf> <file.png>   # render it\ncadgen store why <drawing>.py                  # why the drawing is stale or current\n```\n\n**Running the script (its `__main__` call) is the only door.** There is no\n`cadgen dxf build`: a `.dxf` has no derived state a command must materialize —\nthe file IS the product, and both the CAD Viewer and `dxf snapshot` draw it\nstraight from its own bytes. The drawing's gate makes a rebuild cheap: an unchanged\nsource whose `.dxf` still verifies and whose part children are unchanged is a\nno-op, and `--force` rebuilds anyway. The bytes are a function of the\ndrawing's GEOMETRY, so a cold run and a warm daemon worker write the same\nfile. Builds never wait on or cancel one another; a drawing that calls parts\nbuilds them in parallel like any parent.\n\nAn imported `.dxf` needs nothing at all — hand it straight to snapshot or the\nViewer.\n\nUse the active project Python interpreter; treat `python` as an interpreter\nplaceholder, and use `--help` for the full interface. Target paths resolve from\nthe command's current working directory; run from the workspace that owns the\nartifacts with cwd-relative target paths. Keep a drawing script in the same\ndirectory as the geometry it derives from, named `<name>.py`.\n\nFlags (a model script runs itself; there is no generation CLI):\n\n- `--force` — regenerate even when the recorded output is current.\n- `--verbose`, `--json`.\n\nA run answers on stdout exactly as a STEP model's does — `built DXF/plate_drawing.dxf`\nor `current DXF/plate_drawing.dxf` — with progress on stderr; `--json` makes the\nresult one JSON line (`outcome`, `document`, and `tree`, which is null for a\ndrawing) and the progress one JSON line per transition.\n\nOne script, one drawing: run each script you want built. Do not put output paths\nin the `@dxf` function's return value; `out=` on the decorator is the only\nplace a drawing names its destination (relative to the script).\n\n`cadgen dxf snapshot` draws a drawing flat, to a PNG still — the same picture\nthe CAD Viewer shows, from the same flattening, through the same drawing code:\n\n```bash\ncadgen dxf snapshot path/to/imported.dxf review.png\ncadgen dxf snapshot path/to/drawing.dxf review.png --appearance dark\n```\n\nIt takes the `.dxf` document only — a model script is refused by name (run\n`python <drawing>.py`, then snapshot the drawing it wrote). The whole drawing is\nfitted to the image and painted head on, in the pens the file declares; an\nentity with no pen of its own (ACI 7) takes the appearance's foreground on its\nbackground. The command flattens the drawing with `ezdxf` and renders it through\nthe shared snapshot CLI (`cadgen.snapshot_cli`) and the same headless browser\nruntime every rendering skill uses.\n\nOUT — the second positional — is written exactly as given, with a relative path resolved against the\ncurrent working directory. The target is deleted before the render starts and the\nfinished image is written atomically, so: reuse one name while iterating (every read\nis provably the render you just ran), name the iterations when you genuinely need to\ncompare two. Invalid request combinations fail before touching OUT; after a request is\naccepted, OUT is cleared first so a later failure leaves a missing file instead of\na stale image. A directory (`tmp/` as OUT) is the\ndon't-care case and gets a generated timestamped name inside it, printed on the\n`saved snapshot:` line.\n\nGrammar: `cadgen dxf snapshot TARGET [OUT] [flags]`. Flags: `--appearance\nlight|dark`, `--size-profile`, `--width`/`--height`, `--job`, `--debug`,\n`--json`. That is the whole surface: a drawing is not a scene, so there is no\ncamera to pose, no display settings to configure, no render mode, no parts to\nlist, no section to cut and no view to label — `--camera`, `--display`,\n`--mode` and `--view-labels` are not flags this command has. A `--job` file that\ncarries any of them (or `scale`, an output `label`/`viewLabel`, or\n`output.padding`/`viewLabels`/`tightFrame`) is refused by name before anything\nis rendered; a job's `output.renderScale` and `output.transparent` still apply.\n\nNo CLI inspects an existing `.dxf`. For entity/layer checks read it with `ezdxf`\ndirectly (it arrives with build123d), and `validate_dxf_file` for the drawing checks;\nreview geometry visually (see [Show the model](#show-the-model)).\n\n## Workflow\n\n1. Convert the request into a short brief: outline dimensions, holes and slots, layers, units, output path, and validation targets.\n2. Pick the workflow: drafted from scratch, flat pattern of a generated model (create and validate the 3D geometry with `$cad` first), or flat pattern of an imported STEP.\n3. Write or edit the `<name>.py` source with meaningful dimensions as named constants, reusing the model's geometry helpers instead of duplicating formulas.\n4. Run each drawing script directly (`python <drawing>.py`); do not sweep directories.\n\n```bash\npython path/to/source.py\npython path/to/source.py --force\n```\n\n5. Validate the generated DXF deterministically, then hand off and report.\n\n## Show the model\n\nShow the user each file you create or change, and any they ask to see. Snapshots and\nvalidation don't replace this.\n\n- If your tools include `cad_show` (your host may prefix it), use it, and follow its\n  description for when to call it again. `cad_view` reads what the user selected;\n  `cad_screenshot` shows you what they see. Neither is a review of your own work.\n- Otherwise run the CAD Viewer from the models directory (usually `models/`, not an\n  artifact's output folder):\n\n  ```bash\n  cd /absolute/path/to/model-workspace && cadgen viewer --host 127.0.0.1 --json --detach\n  ```\n\n  `--detach` returns once the viewer answers requests and leaves it running in the\n  background: always pass it, since a foreground viewer never exits (and piping its\n  output through `tail` can hide the URL for good). It starts or reuses the viewer.\n  Read `url` from its one JSON line (never guess the port); for each file under that\n  directory return `url?file=<URL-encoded relative path>`, or `url` alone to review the\n  directory. If it fails to launch, say so.\n\nThe viewer renders saved DXF files as read-only 2D drawings; it never runs\ngeneration scripts. Drag to pan, wheel/pinch to zoom, double-click to fit.\n\n## Validation\n\nValidation happens IN generation, not after: every `@dxf` build runs the drawing\nchecks on the document the engine just serialized, before anything is written, and\na build with error findings fails. The checks: cut-layer profiles must close\n(polylines, circles, or chained line/arc loops), zero-length/degenerate entities are\nrejected, exact duplicate geometry (double-cut risk) is rejected, explicitly unitless\ndocuments are rejected, and an empty modelspace is rejected. Open geometry is allowed\nonly on bend/engrave/reference-intent layers (matched by name).\n\nThe same checks run post-hoc on any existing `.dxf` file — including one that\nnever came from a generator — through `cadgen.drawing_checks`:\n\n```python\nfrom cadgen.drawing_checks import validate_dxf_file\n\nfor finding in validate_dxf_file(\"path/to/file.dxf\"):\n    print(finding.render())\n```\n\nBeyond the built-in checks, verify requested dimensions with targeted `ezdxf` reads\n(entity counts by layer, drawing extents, every dimension the user specified) against\nthe generated sibling `.dxf` (or the `out=` path when one is declared), and\nreview geometry visually in the CAD Viewer:\n\n```python\nimport ezdxf\n\ndoc = ezdxf.readfile(\"path/to/source.dxf\")\nmsp = doc.modelspace()\ncut = msp.query('*[layer==\"CUT\"]')\nholes = msp.query('CIRCLE[layer==\"CUT\"]')\n```\n\nReport only checks that actually ran.\n\n## Handoff\n\nShow every drawing you created or changed ([Show the model](#show-the-model)).\nReport any failure explicitly.\n\nFinal responses should include generated files, returned viewer links, validation\nactually run, and assumptions.\n"},"changes":[],"summary":"First saved snapshot. No earlier version is available for comparison.","summary_kind":"deterministic","summary_metadata":{}}