Skip to content

fix(dxf): emit AutoCAD-compatible DXF#157

Open
eve-assistant-bot[bot] wants to merge 1 commit into
masterfrom
eve/issue-156-dxf-autocad-compat
Open

fix(dxf): emit AutoCAD-compatible DXF#157
eve-assistant-bot[bot] wants to merge 1 commit into
masterfrom
eve/issue-156-dxf-autocad-compat

Conversation

@eve-assistant-bot
Copy link
Copy Markdown
Contributor

@eve-assistant-bot eve-assistant-bot Bot commented May 18, 2026

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:

  1. $ACADVER declared as AC1006 (R10). AutoCAD treats anything pre-AC1009 (R12) as suspect.
  2. HEADER omits extent variables — no $INSBASE, $EXTMIN, $EXTMAX, $LIMMIN, $LIMMAX.
  3. TABLES section contains only LAYERLTYPE and STYLE tables are missing. The LAYER entry references linetype CONTINUOUS, which is then never defined.

The previous normalizeOpenSCADDxf (added in #127) patched $ACADVER and converted LWPOLYLINELINE for R12 compatibility, but left the HEADER and TABLES gaps intact — which is exactly what AutoCAD trips on.

What changed

src/utils/dxfUtils.tsnormalizeOpenSCADDxf is 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:

  • HEADER: $ACADVER=AC1009, $INSBASE, $EXTMIN/$EXTMAX (computed from actual entity coordinates), $LIMMIN/$LIMMAX.
  • TABLES: LTYPE (with CONTINUOUS), LAYER (0), STYLE (STANDARD) — the three tables AutoCAD R12 requires.
  • ENTITIES: passed through unchanged, with LWPOLYLINELINE conversion preserved from the prior implementation.
  • Proper ENDSEC per section and a terminating EOF.
  • Coordinates formatted via toFixed(6) so AutoCAD never sees scientific notation.

createDXFProjectionCode and the OpenSCAD projection flow are unchanged. No new dependencies.

Validation

  • npm run typecheck — passes.
  • npm run lint — passes; warning count unchanged vs master (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).
  • Round-trip parse with ezdxf 1.4.4 on a synthetic OpenSCAD-style DXF input (1 LINE + 1 closed LWPOLYLINE):
    • Detected as AC1009 / R12.
    • Tables present: CONTINUOUS, layer 0, style STANDARD.
    • 5 LINE entities in modelspace (1 original + 4 from the polyline), coords round-trip exactly.
    • Auditor reports 0 errors and 0 fixes — file is structurally clean.

How to verify

  1. Build the app, paste any OpenSCAD model, choose .DXF from the parametric download menu, and open the file in AutoCAD (any version 2013+).
  2. Or run the round-trip parser locally: pip install ezdxf and ezdxf.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.

  • Bug Fixes
    • Rewrote normalizeOpenSCADDxf to strip OpenSCAD wrappers and generate an R12-compliant file.
    • HEADER: sets $ACADVER=AC1009; adds $INSBASE, $EXTMIN/$EXTMAX, $LIMMIN/$LIMMAX from entity bounds.
    • TABLES: adds required LTYPE (CONTINUOUS), LAYER (0), STYLE (STANDARD).
    • ENTITIES: preserves geometry; converts LWPOLYLINELINE; formats coordinates as fixed decimals; proper ENDSEC/EOF.

Written for commit b6a319e. Summary will update on new commits. Review in cubic

… 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.
@vercel
Copy link
Copy Markdown

vercel Bot commented May 18, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
cadam Ready Ready Preview, Comment May 18, 2026 2:12pm

Request Review

@supabase
Copy link
Copy Markdown

supabase Bot commented May 18, 2026

This pull request has been ignored for the connected project sgprnbvihmydyrzvkcir because there are no changes detected in supabase directory. You can change this behaviour in Project Integrations Settings ↗︎.


Preview Branches by Supabase.
Learn more about Supabase Branching ↗︎.

@zachdive zachdive marked this pull request as ready for review May 18, 2026 20:37
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 1 file

Re-trigger cubic

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 18, 2026

Greptile Summary

This PR rewrites normalizeOpenSCADDxf to produce a structurally complete AutoCAD R12 (AC1009) DXF by discarding OpenSCAD's minimal HEADER/TABLES, extracting and converting the entity geometry, computing bounding extents, and rebuilding all required sections from scratch.

  • Replaces the old patch-in-place approach with a full DXF reconstruction: new HEADER (with $ACADVER, $INSBASE, $EXTMIN/$EXTMAX, $LIMMIN/$LIMMAX), new TABLES (LTYPE, LAYER, STYLE), and a regenerated ENTITIES section with LWPOLYLINE→LINE conversion preserved.
  • The required BLOCKS section (empty, between TABLES and ENTITIES per the R12 spec) is absent from buildR12Dxf; ezdxf reconstructs it silently during audit, masking the gap, but AutoCAD's strict parser requires it.
  • $LIMMIN/$LIMMAX are set to entity extents rather than conventional drawing-limit values (e.g., 0,0 to a sheet size), which can produce negative limits for models with negative coordinates.

Confidence Score: 3/5

The 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

Filename Overview
src/utils/dxfUtils.ts Rewrites normalizeOpenSCADDxf to rebuild a full R12 DXF around extracted entities; approach is sound but the required BLOCKS section (between TABLES and ENTITIES per the R12 spec) is absent, which will likely cause AutoCAD to reject the output despite the ezdxf audit passing.

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
Loading

Reviews (1): Last reviewed commit: "fix(dxf): emit AutoCAD-compatible R12 DX..." | Re-trigger Greptile

Comment thread src/utils/dxfUtils.ts
Comment on lines +238 to +314
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]);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread src/utils/dxfUtils.ts
Comment on lines +256 to +261
{ 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) },
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

0 participants