A vanilla TypeScript demo that shows how to embed @mlightcad/cad-simple-viewer in a web page: open DXF/DWG files, create a sample drawing, register custom commands, load @mlightcad/cad-simple-ui-plugin for toolbar chrome (including export), and dynamically load HTML/PDF/SVG export plugins on first use.
- Local files — Open
.dxf/.dwgvia the file picker - New drawing — Create a sample drawing with predefined entities (
DocCreator) - Simple UI toolbar — View/review tools, theme/locale toggles, and export submenu via
cad-simple-ui-plugin - Layer Manager dock — Toolbar Layer Manager opens a Chrome DevTools-style dock panel (layers tab) beside the canvas; the host page must provide a dedicated canvas parent (see Dock panel host layout)
- Custom command — Demo ellipse command (
ellipsedemo) - Dynamic export plugins — HTML (
chtml), PDF (cpdf), SVG (csvg) load in separate chunks when triggered from the toolbar export menu - Split viewer bundle — Default Vite config puts
cad-simple-viewerin its own chunk so the main entry stays small and the page loads quickly (see Vite configuration) - Browser-only — Parsing and rendering run in the browser (Web Workers + WebAssembly for DWG)
- Node.js ≥ 20 and pnpm ≥ 10
pnpm install
pnpm dev # Vite dev server (default http://localhost:5173)
pnpm build # Typecheck + production build
pnpm preview # Serve dist/The build copies DWG/MTEXT workers and viewer-runtime.iife.js into dist/assets/ (see Vite configuration).
DWG parsing and MTEXT rendering run in separate worker scripts. DXF is parsed by the built-in converter in @mlightcad/data-model (no separate worker). Host apps must deploy the DWG/MTEXT worker files and set webworkerFileUrls in AcApDocManager.createInstance(). Before opening a drawing, verify the workers are reachable — do not probe them with a plain GET (the LibreDWG worker alone is ~12 MB).
This example centralizes URLs in src/workerConfig.ts and demonstrates the readiness APIs from @mlightcad/cad-simple-viewer in src/main.ts:
import { AcApDocManager } from '@mlightcad/cad-simple-viewer'
import { WEBWORKER_FILE_URLS } from './workerConfig'
// Option 1: check before createInstance (HEAD + ranged GET fallback; caches successes)
const ready = await AcApDocManager.checkWebworkerReadiness(WEBWORKER_FILE_URLS)
if (!ready) {
throw new Error('CAD worker scripts are missing or blocked')
}
AcApDocManager.createInstance({
webworkerFileUrls: WEBWORKER_FILE_URLS,
checkWorkersOnInit: true // Option 3: async probe during init
})
const manager = AcApDocManager.instance
manager.events.workersReady.addEventListener(({ ready }) => {
if (!ready) console.error('CAD workers are not reachable')
})
// Option 2: re-check on an existing manager before openDocument()
if (!(await manager.areWorkersReady())) {
throw new Error('CAD worker scripts are missing or blocked')
}| API | When to use |
|---|---|
AcApDocManager.checkWebworkerReadiness(urls) |
Before createInstance() — no manager needed |
manager.areWorkersReady() |
Before openDocument() / openUrl() on an existing instance |
checkWorkersOnInit: true + events.workersReady |
Fire-and-forget probe at init; react in UI when ready is false |
manager.workersReady |
Last probe result: true, false, or null (not checked yet) |
Successful URL probes are cached for the page lifecycle; failures are not cached at the probe layer, so transient network errors can succeed on a later call. After each check, workersReady is true or false (null only before the first check).
To test a failure locally, temporarily rename or omit a worker file under dist/assets/ after pnpm build, then try Open.
- Start the dev server and open the URL shown in the terminal.
- Open — Choose a
.dxfor.dwgfile, or click New to create the sample drawing. The viewer and plugins initialize on first use (not at page load). - After initialization, a collapsible toolbar appears on the right with view tools, Layer Manager, theme/locale toggles, and an Export submenu (HTML, PDF, SVG). Click Layer Manager to open the dock panel on the left (layers list). Each export dynamically imports its plugin chunk on first use, so the first run may take a moment.
- Run the custom ellipse command from the viewer command line:
ellipsedemo.
Toast messages at the top report success or errors. The window title updates when a document is activated.
| Format | Notes |
|---|---|
| DXF | Built-in converter in @mlightcad/data-model (no separate worker) |
| DWG | LibreDWG WebAssembly via libredwg-parser-worker.js |
Toolbar chrome comes from @mlightcad/cad-simple-ui-plugin, not custom HTML buttons. Registration lives in src/register.ts:
import { registerSimpleUiPlugin } from '@mlightcad/cad-simple-ui-plugin/register'
import { AcApDocManager, applyUiTheme } from '@mlightcad/cad-simple-viewer'
const host = document.getElementById('viewerPane')!
applyUiTheme('dark', host)
AcApDocManager.createInstance({
container: document.getElementById('cad-container')!,
busyIndicatorHost: host,
// ... webworkerFileUrls, htmlViewerRuntimeUrl, etc.
})
await registerSimpleUiPlugin(AcApDocManager.instance.pluginManager, {
host,
dockPanel: {
defaultOpen: false,
defaultSide: 'left',
defaultHeight: 240,
defaultWidth: 280
},
toolbar: {
placement: 'right',
items: 'default',
collapsible: true
}
})This example calls registerPlugins(host) from src/register.ts, which registers lazy export plugins and the simple UI plugin together. See the cad-simple-ui-plugin README for toolbar customization (items, appendItems, placement, etc.).
Starting with cad-simple-ui-plugin 1.5.8, the toolbar Layer Manager button runs the layer command and opens a dock panel tab (not a floating popover as in 1.5.7). The dock mounts on the viewer canvas parent inside host, then uses flex layout so the canvas shrinks when the panel is open.
Give the page a dedicated canvas area under viewerPane. Do not put full-screen overlays (upload UI, FABs) as direct siblings of #cad-container under host without a wrapper — otherwise the dock wraps those nodes into its main flex slot and the panel may fail to appear correctly.
<div id="viewerPane" class="viewer-container">
<!-- Optional overlays stay on the host; they are outside the dock mount target -->
<div class="upload-screen">...</div>
<button type="button" class="reopen-fab">Open</button>
<!-- Dock mount target = parent of #cad-container -->
<section class="viewer-canvas-area">
<div id="cad-container"></div>
</section>
</div>Suggested CSS (matches index.html):
.viewer-container {
position: relative;
display: flex;
flex-direction: column;
width: 100vw;
height: 100vh;
overflow: hidden;
}
.viewer-canvas-area {
position: relative;
flex: 1;
min-height: 0;
min-width: 0;
overflow: hidden;
}
#cad-container {
position: absolute;
inset: 0;
overflow: hidden;
}Behavior after a drawing is open:
| Action | Result |
|---|---|
Toolbar Layer Manager / sendStringToExecute('layer') |
Opens the dock (or focuses the Layers tab) |
Dock close button / layerclose |
Closes the dock panel |
| Collapse the toolbar | Closes the dock panel |
Override the mount element with dockPanel.mountTarget when you need a custom layout.
Export commands live in separate npm packages, not in cad-simple-viewer. This example does not statically import those packages into the main bundle. Instead, each plugin is registered with a lazy loader that uses dynamic import(), so Vite/Rollup emits a separate chunk per plugin and the browser fetches it only when the user runs a trigger command (from the toolbar Export menu or via sendStringToExecute).
| Package | Plugin name | Factory | Trigger commands | Purpose |
|---|---|---|---|---|
@mlightcad/cad-html-plugin |
HtmlPlugin |
createHtmlPlugin() |
chtml |
Export drawing to offline HTML |
@mlightcad/cad-pdf-plugin |
PdfPlugin |
createPdfPlugin() |
cpdf, ipdf |
Export to PDF / import vector PDF |
@mlightcad/cad-svg-plugin |
SvgPlugin |
createSvgPlugin() |
csvg |
Export drawing to SVG |
See the Plugin System wiki for how to build and register your own plugins.
Do not add top-level imports such as import { createHtmlPlugin } from '@mlightcad/cad-html-plugin' — that would pull the plugin into the main bundle. Keep plugin packages in dependencies (the bundler needs them at build time to emit lazy chunks), but load them inside lazy loaders.
This repo uses each package's /register subpath in src/register.ts (registerLazyHtmlPlugin, registerLazyPdfPlugin, registerLazySvgPlugin). The stubs stay in the main bundle; plugin code loads on first trigger.
Alternatively, register manually on pluginManager after AcApDocManager.createInstance():
import { AcApDocManager } from '@mlightcad/cad-simple-viewer'
AcApDocManager.createInstance({
container: document.getElementById('cad-container')!,
autoResize: true,
webworkerFileUrls: {
mtextRender: './assets/mtext-renderer-worker.js',
dwgParser: './assets/libredwg-parser-worker.js'
},
// Required for HTML export — must match where viewer-runtime.iife.js is served
htmlViewerRuntimeUrl: './assets/viewer-runtime.iife.js'
})
const pluginManager = AcApDocManager.instance.pluginManager
pluginManager.registerLazyPlugin({
name: 'HtmlPlugin',
triggers: ['chtml'],
loader: async () => {
const { createHtmlPlugin } = await import('@mlightcad/cad-html-plugin')
return createHtmlPlugin()
}
})
pluginManager.registerLazyPlugin({
name: 'PdfPlugin',
triggers: ['cpdf', 'ipdf'],
loader: async () => {
const { createPdfPlugin } = await import('@mlightcad/cad-pdf-plugin')
return createPdfPlugin()
}
})
pluginManager.registerLazyPlugin({
name: 'SvgPlugin',
triggers: ['csvg'],
loader: async () => {
const { createSvgPlugin } = await import('@mlightcad/cad-svg-plugin')
return createSvgPlugin()
}
})How it works:
- Registration —
registerLazyPluginrecords the plugin name, trigger command(s), and an asyncloaderonly. No plugin code runs yet. - First trigger — When the user runs
chtml,cpdf,ipdf, orcsvg(via UI orsendStringToExecute), the plugin manager calls the matchingloader. - Dynamic import — The loader’s
import('@mlightcad/cad-*-plugin')fetches the plugin chunk, invokes the factory (createHtmlPlugin, etc.), and registers the returned plugin instance. - Subsequent use — The plugin stays loaded; later exports do not re-download the chunk.
To verify code-splitting, run pnpm analyze and open stats.html — the viewer stack and each plugin should appear as separate chunks (or the viewer inside main, depending on your Vite setup; see below).
Vite controls how @mlightcad/cad-simple-viewer, its DWG/MTEXT workers, and export plugins land in dist/. This repo ships two supported setups. Both keep export plugins out of the main bundle via dynamic import() in registerLazyPlugin loaders; they differ in whether the viewer itself is split into its own chunk.
Shared settings (both approaches):
base: './'— relative asset URLs for static hosting (e.g. GitHub Pages).build.modulePreload: false— do not inject<link rel="modulepreload">for lazy chunks; plugin bundles load only when a trigger command runs.vite-plugin-static-copy— copy DWG/MTEXT workers andviewer-runtime.iife.jsintodist/assets/(required at runtime; not part of the JS bundle graph).pnpm analyze—vite build --mode analyzewritesstats.htmlfor bundle inspection.
Use rollupOptions.output.manualChunks to emit @mlightcad/cad-simple-viewer and its core dependencies (data-model, three-renderer, geometry-engine) as a single output file (e.g. cad-simple-viewer-[hash].js). Your app shell stays in main-[hash].js, which stays small so the initial HTML/JS parse is fast. Export plugins remain separate async chunks.
When to use: production demos or apps where you want the smallest main entry and a dedicated, cacheable viewer chunk.
import { resolve } from 'path'
import { defineConfig } from 'vite'
import { visualizer } from 'rollup-plugin-visualizer'
import { viteStaticCopy } from 'vite-plugin-static-copy'
const VIEWER_STACK =
/\/(cad-simple-viewer|three-renderer|data-model|geometry-engine)\//
export default defineConfig(({ mode }) => ({
base: './',
build: {
modulePreload: false,
rollupOptions: {
input: { main: resolve(__dirname, 'index.html') },
output: {
manualChunks(id) {
const path = id.replace(/\\/g, '/')
if (VIEWER_STACK.test(path)) return 'cad-simple-viewer'
}
}
}
},
plugins: [
viteStaticCopy({
targets: [
{
src: './node_modules/@mlightcad/cad-simple-viewer/dist/*-worker.js',
dest: 'assets'
},
{
src: './node_modules/@mlightcad/cad-html-plugin/dist/viewer-runtime.iife.js',
dest: 'assets'
}
]
}),
mode === 'analyze' &&
visualizer({ filename: 'stats.html', gzipSize: true, brotliSize: true })
].filter(Boolean)
}))This matches the current vite.config.ts.
Omit manualChunks. Rollup bundles cad-simple-viewer into the main entry. Export plugins are still lazy-loaded with dynamic import() and never ship in that initial payload.
When to use: smaller projects or prototypes where a simpler config matters more than minimizing main-bundle size.
import { resolve } from 'path'
import { defineConfig } from 'vite'
import { visualizer } from 'rollup-plugin-visualizer'
import { viteStaticCopy } from 'vite-plugin-static-copy'
export default defineConfig(({ mode }) => ({
base: './',
build: {
modulePreload: false,
rollupOptions: {
input: { main: resolve(__dirname, 'index.html') }
}
},
plugins: [
viteStaticCopy({
targets: [
{
src: './node_modules/@mlightcad/cad-simple-viewer/dist/*-worker.js',
dest: 'assets'
},
{
src: './node_modules/@mlightcad/cad-html-plugin/dist/viewer-runtime.iife.js',
dest: 'assets'
}
]
}),
mode === 'analyze' &&
visualizer({ filename: 'stats.html', gzipSize: true, brotliSize: true })
].filter(Boolean)
}))| Approach A (separate viewer chunk) | Approach B (viewer in main) | |
|---|---|---|
| Main bundle size | Smallest | Includes full viewer stack |
| Vite config | manualChunks for viewer deps |
No extra Rollup output options |
| Export plugins | Lazy chunks via import() |
Lazy chunks via import() |
| Workers / HTML runtime | Static copy to assets/ |
Static copy to assets/ |
Plugin packages stay normal dependencies in package.json in both cases — the bundler needs them at build time to emit lazy chunks; they are not inlined into the main entry unless you add top-level static imports.
After a document is loaded, use the toolbar Export submenu, or trigger export via API:
AcApDocManager.instance.sendStringToExecute('chtml')
AcApDocManager.instance.sendStringToExecute('cpdf')
AcApDocManager.instance.sendStringToExecute('csvg')sendStringToExecute runs the matching lazy loader on first use. You do not need to call loadByTrigger unless you want to preload a plugin before the user clicks export.
HTML export embeds viewer-runtime.iife.js from @mlightcad/cad-html-plugin (not from cad-simple-viewer). Copy it next to your workers and set htmlViewerRuntimeUrl to that path.
vite-plugin-static-copy (see Vite configuration) copies:
./node_modules/@mlightcad/cad-simple-viewer/dist/*-worker.js→assets/(DWG parser, mtext renderer)./node_modules/@mlightcad/cad-html-plugin/dist/viewer-runtime.iife.js→assets/
If viewer-runtime.iife.js is missing or the URL is wrong, the dev server may return index.html instead and the exported HTML will fail with Unexpected token '<'.
{
"dependencies": {
"@mlightcad/cad-simple-viewer": "^1.5.9",
"@mlightcad/cad-simple-ui-plugin": "^1.5.9",
"@mlightcad/data-model": "^1.12.2",
"@mlightcad/cad-html-plugin": "^1.5.9",
"@mlightcad/cad-pdf-plugin": "^1.5.9",
"@mlightcad/cad-svg-plugin": "^1.5.9"
}
}Add cad-simple-ui-plugin for toolbar chrome. Add export plugin packages only for the formats you need, and register each one with lazy loaders as shown above.
<body>
<div id="viewerPane" class="viewer-container">
<section class="viewer-canvas-area">
<div id="cad-container"></div>
</section>
</div>
</body>viewerPane— host forapplyUiTheme,busyIndicatorHost, and the simple UI plugin (hostoption).viewer-canvas-area— parent of the canvas; preferred dock mount target (see Dock panel host layout)cad-container— WebGL / view canvas parent passed toAcApDocManager.createInstance({ container })
import { AcApDocManager } from '@mlightcad/cad-simple-viewer'
import { AcDbOpenDatabaseOptions } from '@mlightcad/data-model'
// ... createInstance + registerLazyPlugin loaders as above ...
const fileContent = await readFile(file)
const options: AcDbOpenDatabaseOptions = {
minimumChunkSize: 1000,
readOnly: true
}
await AcApDocManager.instance.openDocument(file.name, fileContent, options)| Topic | Implementation |
|---|---|
| Document manager | AcApDocManager.createInstance({ container, busyIndicatorHost, baseUrl, webworkerFileUrls, htmlViewerRuntimeUrl }) |
| UI theme | applyUiTheme('dark', host) before createInstance |
| Simple UI | registerSimpleUiPlugin via src/register.ts — toolbar, dock panel / Layer Manager, export submenu |
| Dock host layout | #viewerPane + .viewer-canvas-area + #cad-container in index.html |
| Local open | openDocument(name, ArrayBuffer, options) |
| Custom commands | commandManager.addCommand(...) — see src/ellipseCmd.ts |
| Lazy export plugins | /register stubs in src/register.ts — HTML, PDF, SVG chunks on first trigger |
| Export (API) | sendStringToExecute('chtml' | 'cpdf' | 'csvg') — same commands as toolbar export menu |
| Workers & assets | webworkerFileUrls, static copy in Vite (see Vite configuration) |
| Worker readiness | checkWebworkerReadiness, areWorkersReady, checkWorkersOnInit, workersReady event (see Web Worker readiness) |
Lazy initialization: AcApDocManager is created on first file open or New, not at page load.
| Path | Role |
|---|---|
index.html |
Upload UI, viewerPane host, .viewer-canvas-area dock mount, #cad-container |
src/main.ts |
CadViewerApp — lazy init, worker readiness checks, file open, new drawing |
src/workerConfig.ts |
Shared webworkerFileUrls paths for init and readiness probes |
src/register.ts |
Plugin registration — lazy export plugins + simple UI |
src/ellipseCmd.ts |
Custom ellipsedemo command |
src/docCreator.ts |
Sample drawing factory for New |
vite.config.ts |
Approach A: manualChunks for viewer stack, modulePreload: false, static copy of workers + viewer-runtime.iife.js |
cad-simple-viewer supports modifying drawings in real time (add/edit entities). The API patterns are similar to AutoCAD RealDWG; see realdwg-web and class DocCreator.
@mlightcad/cad-simple-ui-plugin— Framework-agnostic toolbar and Layer Manager dock panel used by this example@mlightcad/cad-viewer— Full Vue UI with built-in lazy plugin registrationcad-simple-viewer-example(monorepo) — Same simple UI plugin with a predefined-file sidebar layout