A professional, lightweight, extensible frontend CAD viewer for modern browsers.
Live demo: cad-viewer-iys.pages.dev
Source: github.com/flyfish-dev/cad-viewer
The project provides a clean loader architecture for DWG, DXF and DWFx/XPS-compatible DWF preview, normalizes format-specific data into a common CadDocument, and renders it through a retained WebGL pipeline with a lightweight Canvas overlay for text and images. Files are read locally in the browser; the viewer does not upload drawings to a backend.
DWG support uses
@mlightcad/libredwg-web/ LibreDWG WebAssembly. DXF support uses JavaScript parsing plus a built-in fallback parser. DWFx support parses XPSFixedPagevector pages. Classic.dwfWHIP/W2D/W3D streams are detected and reported clearly, but full classic DWF decoding still requires a dedicated WHIP decoder or DWF Toolkit/WASM implementation.
- Fixed DWG color fidelity for layer-indexed drawings. Indexed LibreDWG layers now resolve from ACI instead of the converter placeholder
0xffffff, preventing monochrome-white output. - Preserved DWG true-color values even when the RGB integer is in the ACI range, such as
0x0000ff. - Added BYBLOCK color inheritance for expanded INSERT/block entities.
- Fixed the noisy Vite build output where
@mlightcad/libredwg-webprinted a multi-megabytedata:application/wasm;base64,...warning. - DWG worker builds now use the lean LibreDWG ESM wrapper and load
/wasm/libredwg-web.js+/wasm/libredwg-web.wasmat runtime, avoiding duplicated inline wasm assets. build:libnow createsdist/index.jsas a compatibility re-export for integrations that still request/dist/index.js.- Vite dev mode now serves a small
/dist/index.jscompatibility shim that forwards stale demo pages to/demo/main.ts. npm run previewnow builds the demo first, so clean checkouts do not fail becausedist-demois missing.
- The default rendering backend is now a retained WebGL renderer. CAD primitives are flattened once and uploaded to GPU buffers; pan/zoom updates only view uniforms.
- Added spatial indexing and viewport culling. Line, triangle and point batches are bucketed across the drawing bounds, so zoomed-in views submit only visible batches.
- Added large-drawing memory controls: coordinates are stored relative to the drawing center in
Float32Array, colors are stored inUint8Array, and temporary CPU arrays are released after upload. - Text and images render in a separate overlay with minimum screen-height and maximum visible-label limits.
CadViewernow supportsrenderer: 'auto' | 'webgl' | 'canvas2d';autoprefers WebGL and falls back to Canvas2D.- Demo now reports renderer backend, visible primitives and estimated GPU memory.
- DWG parsing now runs in a dedicated Web Worker by default, so LibreDWG WASM initialization and binary decoding no longer block pan/zoom/UI interactions.
- The worker keeps the LibreDWG WASM instance warm and reuses it across DWG loads.
- Added cancellable loading through
AbortSignal, worker timeout support, progress events, and explicit worker asset configuration. - Reduced DWG memory pressure by stripping raw parser objects from worker payloads unless
keepRaw: trueis explicitly enabled. - Demo now includes a loading overlay, progress bar, cancel action and loader mode indicator.
viewer.destroy()disposes canvas listeners and terminates owned DWG workers, which is safe for SPA route changes.- Library exports
supportsDwgWorkerandDwgWorkerClientfor advanced integrations.
- Pure frontend viewer component:
new CadViewer({ container })ornew CadViewer({ canvas }). - Loader registry: DWG, DXF and DWF loaders are independent and replaceable.
- DWG preview: browser-local parsing through LibreDWG WebAssembly, executed in a Web Worker by default.
- DXF preview: JavaScript parser path with fallback support for common ASCII DXF
ENTITIES. - DWF/DWFx preview: DWFx/XPS 2D
FixedPagerendering for paths, glyphs and images. - CAD color handling: ACI, BYLAYER, BYBLOCK inheritance, DWG layer colors, true color, fill color, opacity and adaptive contrast.
- High-performance WebGL viewport controls: retained GPU buffers, spatial culling, zoom, pan, fit-to-view, cursor world coordinates and zoom percentage.
- Professional demo UI: drag-and-drop, compact toolbar, status strip, parse/render timing, entity summary and warnings.
- Library + demo builds: publishable npm package and Cloudflare Pages demo.
npm install @flyfish-dev/cad-viewerFor local development from this repository:
npm install
npm run devThe DWG loader needs LibreDWG WASM assets in a public directory. This repository copies and validates them for the demo:
npm run copy:wasm
npm run check:wasmThe demo resolves wasmPath to an absolute URL before sending it to the worker. In your own app, prefer an absolute path or URL, for example /wasm or new URL('wasm/', document.baseURI).href. Avoid passing a worker-relative path such as ./wasm unless it is resolved on the UI thread first.
When publishing the npm package, build:lib also copies these files into dist/wasm and exposes them as package subpaths. Applications still need to serve the .wasm from a public URL and pass that directory as wasmPath.
Use Vite for the source demo:
npm install
npm run devFor a production preview, use:
npm run previewDo not serve the source directory with a plain static server and expect TypeScript entries to run. If a stale page requests /dist/index.js, run npm run build:lib to create the compatibility entry, or use the Vite dev server above.
import { CadViewer } from '@flyfish-dev/cad-viewer';
import '@flyfish-dev/cad-viewer/style.css';
const viewer = new CadViewer({
container: document.querySelector('#viewer')!,
renderer: 'auto', // WebGL first, Canvas2D fallback
wasmPath: new URL('wasm/', document.baseURI).href,
canvasOptions: {
background: '#05070d',
foreground: '#f8fafc',
contrastMode: 'adaptive',
minColorContrast: 2.45
}
});
const input = document.querySelector<HTMLInputElement>('input[type=file]')!;
input.addEventListener('change', async () => {
const file = input.files?.[0];
if (!file) return;
await viewer.loadFile(file);
});The default renderer: 'auto' path attempts to create CadWebGLRenderer first. Instead of traversing every entity and rebuilding Canvas2D paths on every zoom, the renderer builds a retained scene once in setDocument():
CadDocument
↓
flatten blocks / curves / fills
↓
Float32Array positions + Uint8Array colors
↓
spatial GPU batches
↓
WebGL drawArrays with viewport culling
Tunable options:
new CadViewer({
container,
renderer: 'auto', // force 'webgl' or 'canvas2d' when needed
canvasOptions: {
enableSpatialIndex: true,
spatialIndexCellCount: 96,
maxVerticesPerBatch: 32768,
maxCurveSegments: 72,
textMinPixelHeight: 4,
maxVisibleTextLabels: 2400,
powerPreference: 'high-performance',
preserveDrawingBuffer: false
},
onRenderStats(stats) {
console.log(stats.backend, stats.visiblePrimitiveCount, stats.gpuMemoryBytes);
}
});For very large drawings, lower maxCurveSegments, increase spatialIndexCellCount, and cap maxVisibleTextLabels.
DwgLoader uses a module Web Worker by default in browsers. The worker imports @mlightcad/libredwg-web, initializes LibreDWG WASM inside the worker thread, caches that WASM instance, decodes DWG bytes, normalizes the result into a structured-clone-safe CadDocument, and sends only the normalized scene back to the UI thread. Canvas rendering remains on the main thread.
const controller = new AbortController();
const viewer = new CadViewer({
container,
wasmPath: new URL('wasm/', document.baseURI).href,
useWorker: true,
workerTimeoutMs: 120_000,
onLoadProgress(progress) {
console.log(progress.phase, progress.message, progress.percent);
}
});
await viewer.preloadDwg(); // optional: warm the worker before the first file
await viewer.loadFile(file, { signal: controller.signal });
// cancel a large DWG load
controller.abort();Advanced deployments can override the worker constructor when the bundler or CDN has a custom asset layout:
new CadViewer({
container,
wasmPath: new URL('wasm/', document.baseURI).href,
workerUrl: new URL('/assets/dwg-worker.js', window.location.origin)
});The default package is worker-first. For non-browser runtimes, register a custom DWG loader instead of disabling workers.
const viewer = new CadViewer({
container, // HTMLElement; creates a canvas inside
canvas, // optional existing HTMLCanvasElement
renderer: 'auto', // 'auto' | 'webgl' | 'canvas2d'
wasmPath: '/wasm', // LibreDWG WebAssembly asset path. Absolute URL/path recommended for workers
autoFit: true,
canvasOptions: {
background: '#05070d',
foreground: '#ffffff',
contrastMode: 'adaptive', // 'adaptive' | 'preserve'
minColorContrast: 2.45,
showPageBounds: true,
showUnsupportedMarkers: false,
trueColorByteOrder: 'rgb',
enableSpatialIndex: true,
spatialIndexCellCount: 96,
maxVerticesPerBatch: 32768,
maxCurveSegments: 72,
textMinPixelHeight: 4,
maxVisibleTextLabels: 2400
},
useWorker: true, // default for DWG
workerTimeoutMs: 0, // 0 = disabled
onLoadProgress(progress) {},
onLoad(result) {},
onError(error) {},
onRenderStats(stats) {},
onViewChange(event) {}
});
await viewer.loadFile(file);
await viewer.loadBuffer(arrayBuffer, 'drawing.dxf');
viewer.fit();
viewer.zoomIn();
viewer.zoomOut();
await viewer.preloadDwg(); // optional DWG worker/WASM warmup
viewer.setCanvasOptions({ background: '#f7f8fb', foreground: '#111827' });
viewer.clear();
viewer.destroy();File / ArrayBuffer
↓
CadLoaderRegistry
↓
DwgLoader | DxfLoader | DwfLoader | custom loader
↓
CadDocument
↓
CadWebGLRenderer | CadCanvasRenderer fallback
↓
WebGL preview + Canvas overlay
Each loader returns a normalized CadDocument:
interface CadDocument {
format: 'dwg' | 'dxf' | 'dwf' | 'dwfx' | 'xps' | 'unknown';
layers: Record<string, CadLayer>;
blocks: Record<string, CadBlock>;
entities: CadEntity[];
pages?: CadPage[];
warnings: string[];
raw?: unknown;
}Register a custom loader:
viewer.registerLoader({
id: 'my-cad-format',
label: 'My CAD Format',
formats: ['unknown'],
accepts(input) {
return input.fileName?.endsWith('.cad') ?? false;
},
async load(input) {
return {
document: {
format: 'unknown',
layers: {},
blocks: {},
entities: [],
metadata: {},
warnings: []
},
bytes: input.buffer instanceof Uint8Array ? input.buffer.byteLength : 0,
elapsedMs: 0,
format: 'unknown',
warnings: []
};
}
});| Format | Loader | Coverage |
|---|---|---|
| DWG | DwgLoader |
Uses LibreDWG WebAssembly. Rendering coverage depends on the entities exposed by LibreDWG conversion. |
| DXF | DxfLoader |
Uses dxf-parser plus fallback parsing. Supports core entities, blocks/inserts, colors/layers, polylines, text, hatch boundaries and splines as preview polylines. |
| DWFx / XPS | DwfLoader |
Parses ZIP/OPC packages and renders 2D FixedPage path/glyph/image content. |
| Classic DWF | DwfLoader detection |
Detects WHIP/W2D/W3D package content and returns a clear unsupported error. Full classic DWF requires a dedicated WHIP decoder or DWF Toolkit/WASM. |
The color resolver follows CAD semantics instead of treating all numbers as RGB:
- explicit CSS color or true color object,
- explicit DWG true-color integer, including low RGB values such as
0x0000ff, - entity ACI (
colorIndex,colorNumber,colorin1..255), - BYBLOCK inheritance (
0) when expanding INSERT/block geometry, - BYLAYER lookup (
256/ unset), - viewer foreground fallback.
Layer colors prefer a valid ACI index when a converter also exposes a placeholder RGB value. ACI 7 is foreground-dependent: it renders light on dark canvas and dark on light canvas. With contrastMode: 'adaptive', colors too close to the current canvas background are adjusted just enough to stay readable. Use contrastMode: 'preserve' when exact plotted colors are more important than screen readability.
If a particular converter exposes true-color integers in BGR order:
new CadViewer({ canvasOptions: { trueColorByteOrder: 'bgr' } });npm install
npm run dev # run the demo
npm run typecheck # TypeScript validation
npm run build # library + demo
npm run preview # preview built demo- Build and inspect the package:
npm run build:lib
npm run pack:dry- Publish:
npm login
npm publish --access publicThe package also exposes:
npm run release:npmDirect upload with Wrangler. The repository includes public/_headers, so Cloudflare Pages serves .wasm as application/wasm and caches it long-term:
npm install
npm run build:demo
npx wrangler pages deploy dist-demo --project-name cad-viewerOr use the included script:
npm run deploy:pagesFor GitHub Actions, configure repository secrets:
CLOUDFLARE_API_TOKEN
CLOUDFLARE_ACCOUNT_ID
The workflow is included at .github/workflows/pages.yml.
src/
core/ shared format detection, colors, geometry, transforms, normalized types
loaders/ DwgLoader, DxfLoader, DwfLoader and CadLoaderRegistry
viewer/ CadViewer component and Canvas renderer
demo/ professional Vite demo UI
docs/ English and Chinese architecture / format notes
scripts/ clean and LibreDWG WASM copy helpers
public/wasm/ demo WASM asset output directory
AGPL-3.0-only. This is a strict copyleft license: if you distribute modified versions or offer modified versions over a network, review your source-code disclosure obligations carefully.
The default DWG loader depends on @mlightcad/libredwg-web, which is GPL-3.0-only. For closed-source commercial use, replace the DWG loader with a properly licensed parser/converter and review all dependency licenses.