# netDxf in WebAssembly and WebGPU

RoslynWeb builds the **complete 272-file netDxf library** from the [`netstandard` branch](https://github.com/wieslawsoltes/netDxf/tree/5b562312f683fc635405c149537ca488e4ec4d39), pinned at commit `5b562312f683fc635405c149537ca488e4ec4d39`. The source snapshot is under `vendor/netDxf/`; its provenance manifest records every source file's SHA-256. The build invokes the same Roslyn compiler running inside .NET WebAssembly that powers the editor and CLI.

The document API executes the full reader/writer in the **managed .NET WebAssembly runtime**. The generated JavaScript and native Wasm backends also compile and execute reader/writer method bodies from this complete library. Separate strict-compilation and document-execution tests check those paths against managed results. Generated programs use the supported framework services described below; successful compilation does not establish every possible API, input value, or drawing as compatible.

## Run the sample

After extracting a prebuilt workflow artifact, run `npm run serve`, open `http://localhost:8080/demo/dxf.html`, or follow **netDxf · WebGPU** from the compiler lab. The sample creates a CAD drawing through netDxf, opens text and binary DXF files, exports either format, shows library/geometry metrics, and displays entity warnings. Its WebGPU viewport supports fit, pan, zoom, and layer visibility.

For a source checkout:

```sh
npm run build
npm run serve
```

`npm run build` first builds RoslynWeb, then compiles netDxf and the managed scene bridge. If the compiler runtime already exists, `npm run build:netdxf` rebuilds only the DXF assets. No NuGet download is needed for the pinned netDxf source. The sample normally loads these prebuilt DLLs for faster startup; its source-compilation option compiles all 272 sources in the browser instead.

The renderer requires a browser with WebGPU and an available adapter, served through HTTPS or localhost. Adapter availability depends on the browser, graphics driver, and machine. Parsing, scene extraction, and DXF export remain available when a GPU adapter cannot be created. There is no hidden Canvas or WebGL fallback. See the [WebGPU adapter documentation](https://developer.mozilla.org/en-US/docs/Web/API/GPU/requestAdapter).

## Reuse the library and renderer

Copy `src/` and `dist/` from the prebuilt project while preserving their relative paths, or use the package exports from a locally installed package:

```js
import { createRoslyn } from '@roslynweb/core/browser';
import { createNetDxf, createDxfRenderer } from '@roslynweb/core/dxf';

const compiler = await createRoslyn();
const dxf = await createNetDxf({ compiler });
const renderer = await createDxfRenderer(document.querySelector('canvas'));
try {
  const drawing = await dxf.createSample();
  renderer.setScene(drawing.scene);
  renderer.fit();
  console.log(drawing.stats, drawing.issues);

  const bytes = await drawing.export({ binary: true });
  const reopened = await dxf.load(bytes);
  console.log(reopened.stats);
} finally {
  renderer.dispose();
  await dxf.dispose();
  compiler.dispose();
}
```

For plain browser modules use `./src/browser.js` and `./src/dxf/index.js` instead of package names. The application owns the compiler's lifetime. A DXF session disposes its document handles without disposing the supplied compiler. Multiple documents can remain open in one session. Keep the original document handle when saving: export serializes the managed `DxfDocument`, not the viewer's reduced geometry.

Open a selected file with `await dxf.load(new Uint8Array(await file.arrayBuffer()))`. The API also accepts DXF text and `ArrayBuffer` values. Input defaults to a 32 MiB limit, configurable with `maxInputBytes`; this transfer bound is not a bound on every allocation made while parsing. Each document exposes `scene`, `stats`, `issues`, `inspect()`, `refresh()`, `export({binary})`, and `dispose()`. Session calls are serialized so document operations cannot overlap inside the runtime.

To compile the full library at runtime:

```js
const dxf = await createNetDxf({
  compiler,
  compile: true,
  baseUrl: new URL('./dist/netdxf/', location.href),
  onProgress: ({ stage, message }) => console.log(stage, message),
});
console.log(dxf.compilation.library.performance);
```

The prebuilt and source paths verify asset sizes and SHA-256 against `dist/netdxf/manifest.json` by default. `loadAsset(url)` can supply a custom byte loader; Node `file:` assets work without an HTTP server. Manifest verification detects stale or mismatched deployment files; the manifest is distributed with the application and is not an independent signature.

The session registers the **whole library** as a compiler reference and runtime assembly. Your own C# can use netDxf APIs beyond the JavaScript convenience wrapper. For example, after `createNetDxf({compiler})`:

```js
const custom = await compiler.compile(`
using System;
using System.IO;
using netDxf;
using netDxf.Entities;
public static class CustomDrawing {
    public static string Create() {
        var document = new DxfDocument();
        document.Entities.Add(new Circle(new Vector2(10, 20), 5));
        using var stream = new MemoryStream();
        if (!document.Save(stream, false)) throw new Exception("DXF save failed");
        return Convert.ToBase64String(stream.ToArray());
    }
}`, { outputKind: 'library', assemblyName: 'CustomDrawing' });
if (!custom.success) throw new Error(JSON.stringify(custom.diagnostics));
const result = await compiler.invoke(custom.assemblyId, 'CustomDrawing', 'Create');
if (!result.success) throw new Error(result.error?.message);
const drawing = await dxf.load(Uint8Array.from(atob(result.result), c => c.charCodeAt(0)));
renderer.setScene(drawing.scene);
```

## Use from the CLI or Node

The existing CLI script command exposes the same full Wasm library:

```sh
# Create and save the sample as text DXF.
node bin/roslynweb.mjs script examples/cli/netdxf.mjs -- - out/sample.dxf text

# Read any supported DXF and write binary DXF, then parse it again.
node bin/roslynweb.mjs script examples/cli/netdxf.mjs -- drawing.dxf out/drawing-binary.dxf binary
```

The command prints the original/reopened document statistics and viewer issues. Its source is a reusable Node example. A standalone Node application imports `createRoslyn` from `@roslynweb/core/node` and `createNetDxf` from `@roslynweb/core/dxf`, then follows the same document API. Call `await compiler.close()` when finished to await the worker's termination. WebGPU rendering belongs in the browser; the document API and scene extraction work in Node.

## Direct Wasm and JavaScript geometry

`createNetDxfKernel` compiles a C# adapter against the complete registered netDxf DLL, then strictly compiles its reachable method bodies to native Wasm or JavaScript. It uses netDxf's own vector, matrix, angle and Bézier implementations. The sample's optional geometry comparison panel exercises the same API:

```js
import { createNetDxfKernel } from '@roslynweb/core/dxf';
// First register the library with createNetDxf({compiler}).
for (const backend of ['wasm', 'native-wasm', 'javascript']) {
  const kernel = await createNetDxfKernel({ compiler, backend });
  try {
    console.log(backend, await kernel.invoke('Distance2', [0, 0, 3, 4])); // 5
    console.log(await kernel.invoke('LineLength', [0, 0, 0, 2, 3, 6])); // 7
    console.log(await kernel.invoke('CircleArea', [5])); // 78.53981633974483
    console.log(await kernel.invoke('TrueColorArgb', [12, 34, 56])); // -15982024
    console.log(kernel.info);
  } finally {
    kernel.dispose();
  }
}
```

The eleven exported operations are `Distance2`, `Distance3`, `RotateX`, `RotateY`, `CrossZ`, `NormalizeAngle`, `CubicBezierCoordinate`, `LineLength`, `CircleArea`, `ArcSweep`, and `TrueColorArgb`. The entity operations construct actual netDxf `Line`, `Circle`, and `Arc` instances; the color operation uses `AciColor.ToColor`. Their validation exceptions are preserved. Their signatures are in `src/dxf/NetDxfKernel.cs`. `wasm` supplies the managed reference result; the other two backends execute emitted code. Kernel compilation is opt-in so document startup does not inspect and translate a library unnecessarily. Repeated emissions reuse RoslynWeb's compiler caches.

The JavaScript compiler now accepts an explicit `exports` selection. It retains reachable calls, delegates, virtual implementations, required type initializers, and known framework callbacks before applying strict compatibility checks. Reflection/dynamic-dispatch surfaces conservatively retain the whole input. Selecting a geometry kernel does not certify unselected reader/writer methods. The native Wasm compiler uses its existing export selection.

## Compilation identity and reproducibility

The upstream project targets several frameworks and signs its NuGet assembly. This browser build preserves all upstream C# files unchanged, selects the `NETSTANDARD` code path, compiles against RoslynWeb's bundled .NET 10 reference assemblies, and emits an **unsigned** `netDxf.netstandard.dll`. This is a source-compatible browser build, not a byte-identical replacement for the upstream strong-named `netstandard2.0` package. No public method or source file is removed from the full managed library.

The reproducible outputs in `dist/netdxf/` include the library DLL, scene bridge DLL, source bundle, and manifest. `docs/netdxf-build-verification.json` records the compiler/runtime version, PE sizes and hashes, diagnostics, and compilation timings. The source bundle contains the complete library and bridge so the sample can reproduce both inside the browser. The project's existing portable-PDB scheduling adaptation remains documented in `dist/browser-adaptation.json`; this feature does not add a new Roslyn binary patch.

## Rendering and compatibility boundaries

The renderer is a top/XY CAD viewport with actual WebGPU one-pixel line, triangle, and textured-label pipelines. It retains Z but does not provide a 3D camera or full linetype/lineweight styling. It preserves world coordinates in the scene and rebases GPU coordinates near the drawing center before converting to Float32. Layer visibility changes reuse uploaded buffers; pan/zoom update camera uniforms. Curve tessellation has an explicit tolerance and geometry budget. These choices avoid reparsing DXF or repeatedly uploading geometry during navigation.

| Entity surface | Viewer behavior |
| --- | --- |
| Lines, circles, arcs, ellipses, points | Geometry with world coordinates and arbitrary-axis normals |
| 2D/3D polylines and splines | Bulges, closure and curve tessellation; variable widths are reported and shown as centerlines |
| Block inserts | Nested transforms, block origins, nonuniform scale, layer-zero inheritance, ByBlock colors, and ancestor layer visibility |
| Solid, trace and 3D face | Filled triangle geometry |
| Mesh, polyface and polygon mesh | Face geometry or wireframe/control cage; subdivision limitations are reported |
| Existing dimension blocks, leader, MLine | Available line/curve/block geometry and supported TEXT/MTEXT labels |
| Solid hatch | Coplanar perimeter triangulation with holes and nested islands; Normal, Outer, and Ignore styles; tilted planes and nested transforms |
| Pattern hatch | Real line families with origin/angle/scale, positive dashes, negative gaps, zero-length dots, holes/islands and affine block transforms |
| Gradient hatch | Boundary geometry and an explicit unsupported-gradient notice |
| Wipeout | Background-color mask at its extracted position in display order; unsupported DXF sort-table ordering is reported |
| TEXT, MTEXT and attributes | Unicode browser font shaping, paragraph wrapping, alignment, rotation, width factor, oblique angle, OCS and block transforms; final rendering uses WebGPU |
| Raster images and other unsupported visuals | Structured issues with entity type/handle; original managed entities remain available to the library's writer |

Patterned hatches use the actual DXF line definitions and clip each line/dash family to the same validated region used for solid fills. Pattern origin, scale, angle, OCS and nested nonuniform INSERT transforms are preserved. Zero-length dashes render as clipped square markers in drawing units; the default marker side is 10% of the smaller family spacing or dash cycle, and `hatchDotSize` can override it. They are not fixed screen-pixel dots. Geometry options `maxHatchLines` (100,000 by default), `maxHatchWork` (10,000,000) and `maxVertices` bound tessellation across the scene; exceeding a budget reports a structured issue. Gradient fills remain unsupported. See the [DXF hatch definition](https://help.autodesk.com/cloudhelp/2024/ENU/AutoCAD-DXF/files/GUID-7C05C0EC-B0FB-4A86-A164-B9E5C6C03990.htm) and `docs/netdxf-pattern-verification.json` for the actual managed round-trip checks.

Block-contained curves and splines are flattened by netDxf with the bridge's reported 192-point precision where needed; the renderer's tolerance settings apply to curve DTOs it receives. Display toggles do not change the saved drawing's original layer settings. Only the document's active/model layout is shown by this sample.

The viewer has a defined entity surface. Unsupported entities are reported in structured issues; they remain in the original managed document for netDxf export. The viewer does not implement an AutoCAD graphics engine, CAD SHX font programs, image loading, layout printing, every dimension presentation, or gradient hatch fills. Browser font substitution and unimplemented MTEXT rich formatting are reported; browser fonts do not guarantee CAD-exact text metrics. Browser execution also cannot access an arbitrary local filesystem or native CAD plug-in. The pinned library's own format and fidelity limits continue to apply. Invalid/non-finite scene coordinates that cannot be represented in JSON are rejected without retaining an inaccessible document handle.

The upstream library supports text and binary DXF versions AutoCAD 2000, 2004, 2007, 2010, 2013, and 2018. It does not read DWG; its upstream documentation excludes proprietary REGION, SURFACE and 3DSOLID data and dynamic blocks. A complete source build cannot restore information the upstream parser does not support. See the [pinned upstream README](https://github.com/wieslawsoltes/netDxf/blob/5b562312f683fc635405c149537ca488e4ec4d39/README.md).

Whole-library direct native-Wasm and JavaScript compilation are checked separately from managed execution. The executable backend report in `docs/netdxf-backends-verification.json` records the tested export selection, compatibility diagnostics and execution comparisons. The native whole-library probe selects closed public static exports and their reachable methods; it does not export every instance method independently. Unsupported calls and types still produce strict diagnostics. Values outside an admitted framework service's documented domain fail explicitly during execution.

The whole-library probes and explicit reader/writer wrapper now emit with **zero compatibility diagnostics** on both generated backends under their stated export selections. The default whole-library probes retain 4,476 JavaScript methods and 2,934 native Wasm methods; the document wrapper retains 4,477 and 2,881 respectively. The report marks whole-library emission separately from actual invocation, so an emitted method is not counted as an executed test. Actual generated execution creates a drawing containing a LINE and a CIRCLE, writes it as ASCII or binary DXF, and reloads those bytes through netDxf. Each backend returns the same semantic result as managed .NET Wasm. The framework additions that make this possible include floating parsing and binary conversion, exact temporal values, virtual streams/files and code pages, collection and string operations, regular-expression splitting, and metadata-driven attribute construction.


The separate [document execution report](netdxf-document-verification.json) records **72 successful writes and 216 successful reads** across managed .NET Wasm, generated JavaScript and native Wasm. Each backend writes 24 cases: AutoCAD 2000, 2004, 2007, 2010, 2013 and 2018; ASCII and binary formats; and legacy/Unicode layer-name and text profiles. Every writer's output is read by every backend, so all nine writer/reader combinations pass 24 cases each. The fresh adapter emits 4,482 JavaScript methods and 2,893 native Wasm methods with zero diagnostics; no failed execution is replaced by another backend.

Each document contains LINE, CIRCLE, ARC, LWPOLYLINE, TEXT and MTEXT entities. The 83-field signature checks version and entity counts, layer names and colors, coordinates, radii and angles, line thickness/scale, polyline closure/elevation/bulges/widths, and text content/position/size/rotation. Strings match exactly; numeric values use a tolerance of `1e-11 × max(1, |expected|)` to allow equivalent floating-point formatting. Handles, comments and timestamps are excluded. This verifies those document semantics and does not establish every entity or arbitrary input file as compatible.

Eleven selected geometry, entity and color methods also emit with zero compatibility diagnostics and match 279 managed-oracle input cases on both generated backends. The separate entity suite exercises constructor validation, property mutations, layer events, polyline operations, indexed colors, exceptions and circle cloning. `CircleClone` is a required execution check on both generated backends: real netDxf cloning runs compiled method bodies and matches managed Wasm. Its conservative closure retains related `ICloneable` implementations. These checks establish the executed cases, while arbitrary clone implementations and other document inputs remain subject to the framework boundaries. No source methods are removed from the complete managed netDxf DLL.

To compile your own cloning operation after registering netDxf:

```js
const assembly = await compiler.compile(`
using netDxf;
using netDxf.Entities;
public static class CloneApi {
  public static double Radius(double radius) {
    var original = new Circle(new Vector3(1, 2, 0), radius);
    var copy = (Circle)original.Clone();
    return copy.Radius;
  }
}`, { assemblyName: 'CloneApi', outputKind: 'library', optimization: 'release' });
if (!assembly.success) throw new Error(JSON.stringify(assembly.diagnostics));
const artifact = await compiler.emitWasm(assembly, {
  exports: ['CloneApi.Radius'], optimize: true,
});
// In plain modules import loadWasm from './src/wasm/index.js'.
const { loadWasm } = await import('@roslynweb/core/wasm');
const program = await loadWasm(artifact.bytes);
try { console.log(program.invoke('CloneApi::Radius', [5])); } // 5
finally { program.dispose(); }
```

Use `emitJavaScript` with the same export selection and `strict: true` for the JavaScript backend. Selected cloning reaches considerably more methods than the small geometry kernel; keep the compiler alive to reuse warm emission caches.


## Integration issues found and handled

- The pinned netDxf reader skips HATCH polyline boundary group 73, and its boundary clone omits `IsClosed`. After export/reload, this loses the closing edge or closing bulge. The scene bridge closes only the temporary converted perimeter before tessellation. All 272 vendor sources remain byte-for-byte pinned. The managed curved-boundary tests cover ASCII, binary, opposite-format reload, clockwise and counterclockwise arcs/ellipses, both bulge signs, and tilted block transforms.
- The bundled .NET 10.0.0 WebAssembly interpreter differs from native .NET for two repeated multicast delegate sequence-removal cases. `Delegate.RemoveAll(sequence + sequence, sequence)` throws `System.IndexOutOfRangeException`; nested `Delegate.Remove` returns an incorrect non-null result. The generated JavaScript/native-Wasm delegate implementation passes the native CLR oracle for these cases. `tests/events-verify-native.mjs` reproduces the native baseline, and `tests/events-integration.mjs` records the managed-Wasm discrepancy without treating it as correct expected behavior.
- JSON string and char values now preserve every CLR UTF-16 code unit, including isolated surrogates, in source input, invocation arguments/results, IL string literals and string constants. The bridge writes isolated units as JSON escapes and decodes them losslessly. `tests/utf16-integration.mjs` verifies real Roslyn compilation and all five generated compiler modes. The managed JSON bridge explicitly rejects dictionary keys containing isolated surrogates; ordinary Unicode keys work. Generated-runtime ordinal comparison itself preserves isolated surrogates.

See the [compiled CAD service guide](CAD-COMPILER-SERVICES.md), [enum and core CAD framework services](cad-bcl.md), and [numeric formatting](CAD-NUMERIC-FORMATTING.md) for exact admitted signatures, providers, and runtime value restrictions. A supported signature can still contain an unsupported culture or format value; such execution fails explicitly and is never rerun automatically on another backend.

## Observed performance

These are local verification observations, not a cross-device benchmark. Reports include timings so results can be reproduced on the deployment machine.

| Operation | Observed result |
| --- | --- |
| Full 272-file source build in Node-hosted .NET Wasm, including startup and bridge | About 24.2 seconds |
| Full source compilation in Chromium sample | About 25.7 seconds for the tested source-mode restart |
| Prebuilt library/sample startup over localhost | About 1.5 seconds |
| Compiled library | 767,488 bytes; all source files retained |
| Managed scene bridge | 43,520 bytes |
| Generated sample display | 989 line segments, 637 triangles, and four text labels |
| Complete three-backend document execution matrix | About 11 minutes 34 seconds for fresh compilation, 72 writes and 216 semantic reads |
| Native analysis of the full reader/writer wrapper | 209.3 seconds before per-analysis caches; 18.4 seconds after, with identical ordered methods, diagnostics, dependencies and instruction count |

The native-analysis comparison used the same inspected reader/writer wrapper and pinned library: 2,881 retained methods, 95,886 instructions and zero diagnostics before and after. The caches reuse unchanged framework-callback and closed-generic analysis within one emission. The roughly 11.4-fold improvement describes this local analysis workload, not end-to-end compilation or execution on other machines.

Use the prebuilt path for normal startup and keep one compiler/session alive for repeated operations. The sample creates GPU buffers and label textures once per drawing; view/layer changes submit new frames without reloading the library, reshaping text, or recompiling C#. Text texture and buffer budgets bound GPU allocations. Unsupported crossing, touching, self-intersecting, or nonplanar hatch boundaries produce issues instead of an invented fill.

## Verification

```sh
npm run build:netdxf
npm run test:netdxf
npm run test:netdxf-compilers
npm run test:netdxf-document
npm run test:netdxf-entities
npm run test:compiler-services
npm test
npm run pages:build
npm run test:netdxf-browser
```

The integration suite loads the complete library in actual .NET WebAssembly, exercises text/binary save and reload, source compilation, document lifetime and error behavior, and parses the pinned upstream DXF fixtures. The compiler suite compares generated backends with managed execution and requires generated reader/writer execution. The document suite compiles a separate six-entity adapter and checks save/load semantics across managed Wasm, generated JavaScript and native Wasm, six DXF versions, both file formats and legacy/Unicode text profiles. The browser suite exercises the actual sample at a repository subpath and tests WebGPU rendering and unavailable-adapter behavior. Browser testing uses Playwright installed separately; CI installs its pinned Chromium runtime.

The source is MIT licensed; embedded Geometric Tools translations retain the Boost Software License and their original notices. See `THIRD-PARTY-NOTICES.md` and the license files in the vendored snapshot.
