A client sends you an AutoCAD drawing. "Just display it in the browser," they say. How hard can it be?
I found out the hard way. After trying every open-source DXF library for JavaScript, I ended up building my own. Here's the story of dxf-render and what I learned about the surprisingly weird world of the DXF format.
Live Demo — drag and drop any DXF file to see it rendered.
"Where are the dimensions?"
My first attempt was simple: grab dxf-parser + three-dxf, wire them together, ship it. The basic shapes looked fine — lines, circles, arcs. Then I opened a real engineering drawing.
Half the dimensions were gone. The dashed center lines showed as solid. Hatched areas were empty outlines. And a floor plan drawn in a rotated coordinate system looked like abstract art.
I switched to dxf-viewer — the most capable option out there at 37K monthly downloads. Better, but still: only linear dimensions (no radial, angular, ordinate), no linetype patterns, no LEADER arrows. Every drawing had something missing.
The core issue? DXF is a 40-year-old format with a lot of features, and building a renderer that handles real-world files — not just textbook examples — takes more than a few entity handlers.
The DXF rabbit hole
If you've never worked with DXF, here's the fun part: the file format is a flat stream of numbered code/value pairs. No nesting, no XML, no JSON. Just thousands of lines like:
0
LINE
8
Layer1
10
0.0
20
0.0
11
100.0
21
50.0
Code 0 = entity type. Code 8 = layer name. Codes 10/20 = start point X/Y. Codes 11/21 = end point. Simple enough for LINE. But then you hit DIMENSION entities with 30+ codes, HATCH with recursive boundary paths, MTEXT with its own inline formatting language (\P for newline, \S for stacking fractions, {\fArial;styled text})...
And then there's OCS — the Object Coordinate System. Some CAD tools save entities in local coordinate systems defined by an extrusion direction vector. You need to implement the "Arbitrary Axis Algorithm" from the DXF spec to transform them back to world coordinates. Skip this and your 3D-originated drawings will be rotated, flipped, or shifted.
I wrote 25 entity handlers. Each one wrapped in try-catch, because real-world DXF files regularly break the spec. An entity parser that crashes on malformed data is useless — you need to skip the broken entity, log a warning, and keep going.
What I ended up with
After months of work, dxf-render handles 21 entity types — including the ones that other libraries skip:
- All 7 dimension types (linear, rotated, aligned, ordinate, radial, diametric, angular) — not just linear
- Linetype patterns resolved from the LTYPE table and applied as geometric dash patterns
- 25 built-in hatch patterns (ANSI31, HONEY, BRICK...) with proper clipping to boundary paths
- LEADER and MULTILEADER — those annotation arrows that show up in every mechanical drawing
- Full OCS via the Arbitrary Axis Algorithm
- Vector text rendered with opentype.js — no bitmap textures, sharp at any zoom
The most satisfying moment was opening a complex architectural plan and seeing it come out right.
Five lines to render a DXF
import { parseDxf, createThreeObjectsFromDXF, loadDefaultFont } from "dxf-render";
const dxf = parseDxf(dxfText);
await loadDefaultFont();
const { group } = await createThreeObjectsFromDXF(dxf);
scene.add(group); // add to any Three.js scene
That's the core API. Parse, create Three.js objects, add to scene. It works with React, Vue, Svelte, Angular, vanilla JS — anything that can host a Three.js canvas.
Full working examples: Vanilla TS | React | Vue — all on StackBlitz, runnable in the browser.
Parser without Three.js
There's a separate entry point for when you just need the data:
import { parseDxf } from "dxf-render/parser";
const dxf = parseDxf(dxfText);
// → layers, entities, blocks, styles, header variables — all typed
Zero dependencies. Works in Node.js, Deno, Bun. Useful for data extraction, server-side processing, or building your own renderer.
Performance tricks I learned the hard way
The draw call problem
My first naive renderer created one THREE.Line per DXF entity. A modest floor plan with 5,000 lines = 5,000 draw calls = slideshow.
The fix: a GeometryCollector that accumulates all line segments, points, and mesh triangles by layer::color key, then flushes them into merged BufferGeometry objects. 5,000 draw calls became ~50. The GPU doesn't care about individual lines — it cares about batches.
Keeping the UI alive
Large DXF files can have 50,000+ entities. Processing them synchronously freezes the browser. createThreeObjectsFromDXF() yields back to the event loop every ~16ms and supports AbortSignal for cancellation:
const { group } = await createThreeObjectsFromDXF(dxf, {
signal: abortController.signal,
onProgress: (p) => updateProgressBar(p),
});
Instant dark mode without re-rendering
AutoCAD's color index 7 means "black on light background, white on dark." Instead of re-building the entire scene when the user toggles dark mode, I use sentinel color values tracked in a MaterialCacheStore. Calling materials.switchTheme(true) updates the affected materials in-place — the theme switch is instant.
How it compares
I'll let the table speak:
| Feature | dxf-render | dxf-viewer | dxf-parser | three-dxf |
|---|---|---|---|---|
| Rendering | ✅ Three.js | ✅ Three.js | ❌ parse only | ✅ Three.js |
| Entity types | 21 | ~15 | ~15 parsed | ~8 |
| Linetype patterns | ✅ | ❌ all solid | — | ❌ |
| Dimension types | all 7 | linear only | — | ❌ |
| LEADER / MULTILEADER | ✅ | ❌ | — | ❌ |
| Hatch patterns | 25 built-in | ✅ | — | ❌ |
| OCS support | full | Z-flip only | — | ❌ |
| TypeScript | native | .d.ts | native | ❌ |
| Tests | 853 | 0 | ✅ | 0 |
| Parser-only mode | ✅ zero deps | ❌ | ✅ | ❌ |
| Last updated | 2026 | 2024 | 2023 | 2019 |
No shade to these projects — they solved real problems for a lot of people. dxf-render just goes deeper on entity coverage and rendering accuracy.
What I'd do differently
Start with tests earlier. I wrote most of the 853 tests after the fact. Having them from the start would have caught color resolution edge cases (ByLayer inheriting from ByBlock inside nested INSERTs) much sooner.
Don't underestimate text. MTEXT parsing alone is ~400 lines. The inline formatting language is underdocumented and inconsistently implemented across CAD tools. I ended up building a custom glyph cache with hand-crafted vector paths for special characters that opentype.js doesn't handle (diameter symbols, plus-minus signs, degree marks).
DXF files in the wild are broken. Entities with missing required codes, color indices out of range, splines with zero control points. The parser needs to be paranoid and forgiving at the same time.
What's next
- Polyline width — LWPOLYLINE with variable start/end width (common in P&ID diagrams)
- Entity interaction — click and hover events via raycaster
- More hatch patterns — importing from the QCAD pattern library
Try it
npm install dxf-render three
- GitHub — star if it's useful ⭐
- Live Demo — upload any DXF
- StackBlitz — try it without installing
For Vue 3 users: dxf-vuer wraps dxf-render into a drop-in viewer component.
Working with CAD files in the browser? I'd love to hear about your use case — drop a comment or open an issue.
Top comments (0)