fix(dxf): emit AutoCAD-compatible DXF#157
Conversation
… sections (#156) OpenSCAD emits a minimal DXF: $ACADVER=AC1006 (R10), no extent variables, and a TABLES section containing only LAYER. Lenient importers (LibreCAD, QCAD, Onshape, Inkscape, Fusion) recover from this, but AutoCAD refuses to open the file. The previous normalizer patched $ACADVER and converted LWPOLYLINE -> LINE, leaving the structural gaps intact. normalizeOpenSCADDxf now discards OpenSCAD's HEADER/TABLES wrappers, keeps the entity geometry (with the existing LWPOLYLINE conversion), and rebuilds the file to the R12 (AC1009) spec: - HEADER: $ACADVER=AC1009, $INSBASE, $EXTMIN/$EXTMAX (computed from entity coords), $LIMMIN/$LIMMAX - TABLES: LTYPE (with CONTINUOUS), LAYER (0), STYLE (STANDARD) - ENTITIES: passed through with LWPOLYLINE -> LINE conversion - Proper ENDSEC per section, terminating EOF - Coordinates formatted as plain decimals (no scientific notation) Validated by round-trip parsing the output with ezdxf 1.4.4: audit reports 0 errors and 0 fixes; LINE geometry round-trips with correct layer assignment.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
This pull request has been ignored for the connected project Preview Branches by Supabase. |
Greptile SummaryThis PR rewrites
Confidence Score: 3/5The rewrite produces structurally cleaner DXF than before, but the missing BLOCKS section means AutoCAD — the explicit target — will likely still reject the output, making the core goal unmet. The BLOCKS section is a required structural element in every R12 DXF file and sits between TABLES and ENTITIES. Its absence is hidden by the ezdxf validation step because ezdxf reconstructs missing sections on read; AutoCAD does not. The rest of the implementation — extent computation, table definitions, LWPOLYLINE conversion, coordinate formatting — is correct and well-structured. src/utils/dxfUtils.ts — specifically buildR12Dxf, which needs an empty BLOCKS section inserted between tables and entities before the spread into fromDxfPairs. Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A["normalizeOpenSCADDxf(dxf)"] --> B["toDxfPairs"]
B --> C["findEntitiesSectionStart"]
C -->|not found| D["return []"]
C -->|found| E["extractEntityPairs\n(LWPOLYLINE → LINE)"]
E --> F["computeExtents\n(codes 10-19 = X, 20-29 = Y)"]
F --> G["buildR12Dxf"]
G --> H["HEADER section\n$ACADVER=AC1009\n$INSBASE, $EXTMIN, $EXTMAX\n$LIMMIN, $LIMMAX"]
G --> I["TABLES section\nLTYPE CONTINUOUS\nLAYER 0\nSTYLE STANDARD"]
G --> J["⚠️ BLOCKS section\nMISSING — required by R12"]
G --> K["ENTITIES section\n(converted entity pairs)"]
G --> L["EOF"]
H --> M["fromDxfPairs → DXF text"]
I --> M
J -.->|"should exist between TABLES and ENTITIES"| M
K --> M
L --> M
Reviews (1): Last reviewed commit: "fix(dxf): emit AutoCAD-compatible R12 DX..." | Re-trigger Greptile |
| function buildR12Dxf(entityPairs: DxfPair[], extents: Extents): string { | ||
| const header: DxfPair[] = [ | ||
| { code: '0', value: 'SECTION' }, | ||
| { code: '2', value: 'HEADER' }, | ||
| { code: '9', value: '$ACADVER' }, | ||
| { code: '1', value: 'AC1009' }, | ||
| { code: '9', value: '$INSBASE' }, | ||
| { code: '10', value: '0.0' }, | ||
| { code: '20', value: '0.0' }, | ||
| { code: '30', value: '0.0' }, | ||
| { code: '9', value: '$EXTMIN' }, | ||
| { code: '10', value: formatCoord(extents.minX) }, | ||
| { code: '20', value: formatCoord(extents.minY) }, | ||
| { code: '30', value: '0.0' }, | ||
| { code: '9', value: '$EXTMAX' }, | ||
| { code: '10', value: formatCoord(extents.maxX) }, | ||
| { code: '20', value: formatCoord(extents.maxY) }, | ||
| { code: '30', value: '0.0' }, | ||
| { code: '9', value: '$LIMMIN' }, | ||
| { code: '10', value: formatCoord(extents.minX) }, | ||
| { code: '20', value: formatCoord(extents.minY) }, | ||
| { code: '9', value: '$LIMMAX' }, | ||
| { code: '10', value: formatCoord(extents.maxX) }, | ||
| { code: '20', value: formatCoord(extents.maxY) }, | ||
| { code: '0', value: 'ENDSEC' }, | ||
| ]; | ||
|
|
||
| const tables: DxfPair[] = [ | ||
| { code: '0', value: 'SECTION' }, | ||
| { code: '2', value: 'TABLES' }, | ||
| { code: '0', value: 'TABLE' }, | ||
| { code: '2', value: 'LTYPE' }, | ||
| { code: '70', value: '1' }, | ||
| { code: '0', value: 'LTYPE' }, | ||
| { code: '2', value: 'CONTINUOUS' }, | ||
| { code: '70', value: '0' }, | ||
| { code: '3', value: 'Solid line' }, | ||
| { code: '72', value: '65' }, | ||
| { code: '73', value: '0' }, | ||
| { code: '40', value: '0.0' }, | ||
| { code: '0', value: 'ENDTAB' }, | ||
| { code: '0', value: 'TABLE' }, | ||
| { code: '2', value: 'LAYER' }, | ||
| { code: '70', value: '1' }, | ||
| { code: '0', value: 'LAYER' }, | ||
| { code: '2', value: '0' }, | ||
| { code: '70', value: '0' }, | ||
| { code: '62', value: '7' }, | ||
| { code: '6', value: 'CONTINUOUS' }, | ||
| { code: '0', value: 'ENDTAB' }, | ||
| { code: '0', value: 'TABLE' }, | ||
| { code: '2', value: 'STYLE' }, | ||
| { code: '70', value: '1' }, | ||
| { code: '0', value: 'STYLE' }, | ||
| { code: '2', value: 'STANDARD' }, | ||
| { code: '70', value: '0' }, | ||
| { code: '40', value: '0.0' }, | ||
| { code: '41', value: '1.0' }, | ||
| { code: '50', value: '0.0' }, | ||
| { code: '71', value: '0' }, | ||
| { code: '42', value: '2.5' }, | ||
| { code: '3', value: 'txt' }, | ||
| { code: '4', value: '' }, | ||
| { code: '0', value: 'ENDTAB' }, | ||
| { code: '0', value: 'ENDSEC' }, | ||
| ]; | ||
|
|
||
| const entities: DxfPair[] = [ | ||
| { code: '0', value: 'SECTION' }, | ||
| { code: '2', value: 'ENTITIES' }, | ||
| ...entityPairs, | ||
| { code: '0', value: 'ENDSEC' }, | ||
| ]; | ||
|
|
||
| const eof: DxfPair[] = [{ code: '0', value: 'EOF' }]; | ||
|
|
||
| return fromDxfPairs([...header, ...tables, ...entities, ...eof]); |
There was a problem hiding this comment.
Missing required BLOCKS section
buildR12Dxf emits HEADER → TABLES → ENTITIES → EOF, but the R12 spec mandates a BLOCKS section between TABLES and ENTITIES — even when it's empty. ezdxf silently reconstructs missing sections during readfile, so doc.audit() returns zero errors regardless. AutoCAD's parser is stricter: without a 0 SECTION / 2 BLOCKS / 0 ENDSEC block in the right position, AutoCAD 2013+ will reject or mangle the file, directly defeating the stated purpose of this fix.
A minimal empty BLOCKS section needs to appear between the tables and entities arrays in the spread to fromDxfPairs.
| { code: '9', value: '$LIMMIN' }, | ||
| { code: '10', value: formatCoord(extents.minX) }, | ||
| { code: '20', value: formatCoord(extents.minY) }, | ||
| { code: '9', value: '$LIMMAX' }, | ||
| { code: '10', value: formatCoord(extents.maxX) }, | ||
| { code: '20', value: formatCoord(extents.maxY) }, |
There was a problem hiding this comment.
$LIMMIN/$LIMMAX set to entity extents, not drawing limits
Drawing limits ($LIMMIN/$LIMMAX) define the virtual sheet extent — the paper — not where geometry lives. Setting them to extents.minX/extents.minY means that any model with geometry at negative coordinates (common in OpenSCAD) produces negative $LIMMIN values. AutoCAD's ZOOM LIMITS framing then shows a "paper" that coincidentally wraps the geometry with zero clearance, which is surprising behaviour. Standard practice is $LIMMIN = (0, 0) and $LIMMAX = a positive sheet size. This won't cause AutoCAD to reject the file, but it's unexpected.
Closes #156
Root cause
OpenSCAD's WASM DXF exporter (
--export-format=dxf) emits a minimal file that lenient CAD importers (LibreCAD, QCAD, Onshape, Inkscape, Fusion) recover from, but AutoCAD's parser refuses to open. The deviations from the AutoCAD DXF Reference:$ACADVERdeclared asAC1006(R10). AutoCAD treats anything pre-AC1009(R12) as suspect.$INSBASE,$EXTMIN,$EXTMAX,$LIMMIN,$LIMMAX.LAYER—LTYPEandSTYLEtables are missing. The LAYER entry references linetypeCONTINUOUS, which is then never defined.The previous
normalizeOpenSCADDxf(added in #127) patched$ACADVERand convertedLWPOLYLINE→LINEfor R12 compatibility, but left the HEADER and TABLES gaps intact — which is exactly what AutoCAD trips on.What changed
src/utils/dxfUtils.ts—normalizeOpenSCADDxfis rewritten to discard OpenSCAD's HEADER/TABLES wrappers, keep the entity geometry (with the existing LWPOLYLINE → LINE conversion), and rebuild the surrounding sections to the R12 spec:$ACADVER=AC1009,$INSBASE,$EXTMIN/$EXTMAX(computed from actual entity coordinates),$LIMMIN/$LIMMAX.LTYPE(withCONTINUOUS),LAYER(0),STYLE(STANDARD) — the three tables AutoCAD R12 requires.LWPOLYLINE→LINEconversion preserved from the prior implementation.ENDSECper section and a terminatingEOF.toFixed(6)so AutoCAD never sees scientific notation.createDXFProjectionCodeand the OpenSCAD projection flow are unchanged. No new dependencies.Validation
npm run typecheck— passes.npm run lint— passes; warning count unchanged vsmaster(11 pre-existing warnings, 0 from this change).npm run build— not run (the 4 GB sandbox OOMs on the Vite build per task notes; the change is contained to a leaf utility and exercises no new runtime paths).AC1009/ R12.CONTINUOUS, layer0, styleSTANDARD.Auditorreports 0 errors and 0 fixes — file is structurally clean.How to verify
pip install ezdxfandezdxf.readfile(...)on the exported file;doc.audit()should return zero errors.Authored by Eve (Zach's AI agent) on behalf of Adam.
Summary by cubic
Fixes #156 by emitting AutoCAD-compatible R12 (AC1009) DXF so exported files open in AutoCAD. Rebuilds the DXF structure around OpenSCAD’s entities and computes proper extents.
normalizeOpenSCADDxfto strip OpenSCAD wrappers and generate an R12-compliant file.$ACADVER=AC1009; adds$INSBASE,$EXTMIN/$EXTMAX,$LIMMIN/$LIMMAXfrom entity bounds.LTYPE(CONTINUOUS),LAYER(0),STYLE(STANDARD).LWPOLYLINE→LINE; formats coordinates as fixed decimals; properENDSEC/EOF.Written for commit b6a319e. Summary will update on new commits. Review in cubic