A web viewer in ten lines
A web page that builds a cuboid with the WebAssembly kernel and draws it, lit, with a grid, its edges and an orbit camera: two files, and about ten lines of JavaScript. The package's Viewer does the drawing, through WebGPU where the browser has it and WebGL2 where it does not. No framework, no bundler, no 3D library. Every file below is shown whole, and each one was run as it is.
What you need
Download the Web Pro zip, cadaclysm-<version>-web-pro.zip, and use its pkg/ folder. That folder is the one build with all three parts this page uses: the reader (it carries the license, and reads files), the kernel (cadaclysm_blacksmith_cuboid, cadaclysm_blacksmith_cut) and the viewer (Viewer and view.js). The latest release's is cadaclysm-0.9.0-web-pro.zip; in a checkout of the SDK, python fetch.py --web pro downloads it with a Web Pro license (see In a web page). You also need a static file server; Python's is enough.
The zips' other folders are smaller builds with a part left out, and this tutorial does not run on them:
pkg-reader-viewer/(and the Web Reader zip'spkg/): reader and viewer, no kernel, so noshowSolidand no cuboid. It draws files withshowScene.pkg-kernel/: reader and kernel, no viewer: it builds parts and draws nothing.pkg-reader/: the reader alone.pkg-viewer/: the viewer alone, for arrays of your own (showMesh).
Make a folder with the page's files beside pkg/:
viewer/
├── pkg/ the Web Pro zip's pkg/: reader + kernel + viewer
├── cadaclysm.lic your license, if you have one
├── index.html the page: one canvas
├── main.js the ten lines
└── worker/ step 6: the same page, drawn from a Web Worker
The page
index.html: a canvas that fills the window, and main.js loaded as an ES module.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>A cuboid</title>
<style>
html, body { margin: 0; height: 100%; background: #15171a; }
canvas { display: block; width: 100%; height: 100%; }
</style>
</head>
<body>
<canvas id="view"></canvas>
<script type="module" src="main.js"></script>
</body>
</html>
The canvas needs a size in CSS: the viewer draws at that size, in device pixels, and follows it when the window changes.
A cuboid on screen
main.js, the whole of it:
import { load, Viewer, cadaclysm_blacksmith_cuboid, cadaclysm_blacksmith_solid_free } from "./pkg/index.js";
import { attachControls } from "./pkg/view.js";
await load({ license: "./cadaclysm.lic" });
const canvas = document.getElementById("view");
const viewer = await Viewer.create(canvas, { surfaces: "exact" }); // WebGPU, else WebGL2
const box = cadaclysm_blacksmith_cuboid(60, 40, 20); // millimetres, centred on the origin
viewer.showSolid(box); // meshed inside the module, framed
cadaclysm_blacksmith_solid_free(box); // the viewer keeps only what it drew
attachControls(canvas, viewer); // drag, right-drag, wheel, pinch
viewer.start();
console.log("drawing through", viewer.backend + ",", viewer.surfaces);
loadstarts the module and hands it the license. Without one, or on a host the license does not name, everything still works, with a notice on the console.Viewer.createtakes the canvas and picks the API: WebGPU if the browser has it, WebGL2 if not.surfaces: "exact"asks for the cuboid's faces as the exact surfaces they are rather than a mesh of them (see Exact surfaces below).cadaclysm_blacksmith_cuboidis the kernel's call, named as in the C API. It returns a handle, a number.showSolidmeshes the solid inside the module and draws it, with its edges, and frames the camera on it. The geometry never passes through JavaScript.- Free the solid when you are done with it. Its memory is in the wasm module, where the garbage collector never looks. The viewer keeps its own copy of what it drew, and of the solid itself until it is removed, so the handle can go straight away.
attachControls, frompkg/view.js, turns the pointer into the camera: drag to orbit, right-drag or Shift-drag to pan, the wheel to zoom, two fingers to pinch.startdraws on the browser's animation frames, and only when something has changed.
Serve it and open it
A module and its .wasm are not loaded from a file: URL, so serve the folder:
cd viewer
python -m http.server 8000
Open http://localhost:8000/. A grey cuboid, 60 × 40 × 20 mm, sits lit on a grid, with its edges drawn and the camera framed on it. Drag it round. This is it, running: the files above, served beside this site's own module (drag to orbit, right-drag to pan, the wheel to zoom; open it on its own):
The console says drawing through webgpu, exact or drawing through webgl2, triangles: viewer.backend names the API the browser gave, and viewer.surfaces what it draws. If the page stays empty, the console says why. The usual cause is a server that sends .wasm with a type other than application/wasm. Without a license everything still works: the console warns once that none was loaded, and adds a notice on every file opened and every STEP file written. A license names the websites it covers, and localhost and file: pages always pass that check, so development never needs the license to know where the site will live.
Change it
A line each, in main.js after the viewer is made. A colour, for what one show drew (showSolid returns an id):
const id = viewer.showSolid(box);
viewer.setColour(id, [0.2, 0.6, 0.9]);
A layer off. The layers are surfaces, edges, curves, isocurves and grid:
viewer.setVisible("grid", false);
A standard view (top, front, right, iso), and a parallel projection:
viewer.view("top");
viewer.setOrthographic(true);
A file instead of a shape (add open to the import from ./pkg/index.js). open reads STEP, IGES, SAT, 3dm, IFC, BREP or SCAD, and the scene is drawn as read, with its colours and parts:
viewer.showScene(open(bytes, "step"));
Or triangles of your own, as typed arrays (normals, edges and a colour are optional):
viewer.showMesh({ positions, indices });
Each show adds to what is drawn; frame() fits the camera to all of it again, frame(id) to one item, or frame(id, node) to one of its local nodes. Besides orbit/pan/zoom, a fly camera moves on look(dx, dy) (a drag, the eye held still) and key(code, down) (WASD to move, Q/E up and down) — bind every keydown/keyup event's code and let draw() or start()'s loop step the camera each frame.
Heavy work in a worker
A kernel call runs on the thread that makes it. A cuboid takes milliseconds, but a boolean or a large STEP file does not, and while it runs a page on the main thread cannot scroll or answer a click. So move the module into a Web Worker: the page hands its canvas over with transferControlToOffscreen and forwards the pointer with forwardControls, and the worker builds, draws and takes the input with receiveControls. worker/index.html is the page of step 2 loading page.js instead of main.js.
worker/page.js:
import { forwardControls } from "../pkg/view.js";
const canvas = document.getElementById("view");
const worker = new Worker(new URL("./worker.js", import.meta.url), { type: "module" });
// Size the drawing buffer before handing it over: the worker hears forwardControls'
// size messages only once its viewer exists, so the first one may be missed.
canvas.width = Math.round(canvas.clientWidth * devicePixelRatio);
canvas.height = Math.round(canvas.clientHeight * devicePixelRatio);
const offscreen = canvas.transferControlToOffscreen();
worker.postMessage({ canvas: offscreen }, [offscreen]);
forwardControls(canvas, worker);
worker/worker.js, a cuboid with a bore cut through it:
import { load, Viewer, cadaclysm_blacksmith_cuboid, cadaclysm_blacksmith_cylinder,
cadaclysm_blacksmith_translate, cadaclysm_blacksmith_cut, cadaclysm_blacksmith_solid_free } from "../pkg/index.js";
import { receiveControls } from "../pkg/view.js";
// Not a top-level await: the page's canvas can arrive while the module loads, and a
// message that comes before self.onmessage is set is lost.
const ready = load({ license: "../cadaclysm.lic" });
self.onmessage = async ({ data }) => {
if (!data.canvas) return; // the rest are receiveControls' messages
await ready;
const viewer = await Viewer.create(data.canvas);
receiveControls(viewer);
viewer.start();
// Heavy work belongs here: a boolean blocks the thread it runs on, and this is not the page's.
const box = cadaclysm_blacksmith_cuboid(60, 40, 20);
const bar = cadaclysm_blacksmith_cylinder(8, 40);
const moved = cadaclysm_blacksmith_translate(bar, 0, 0, -20);
const part = cadaclysm_blacksmith_cut(box, moved, 0.05);
viewer.showSolid(part, { colour: [0.85, 0.55, 0.3] });
for (const h of [box, bar, moved, part]) cadaclysm_blacksmith_solid_free(h);
};
Open http://localhost:8000/worker/: an orange block with a hole through it, drawn and orbited exactly as before, while the page's own thread stays free.
Exact surfaces
By default the viewer draws triangles: a mesh of each face, made inside the module. With surfaces: "exact" it draws a kernel solid's faces and a read file's as the trimmed surfaces they are, evaluated on the GPU, and their B-rep edges as exact curves, so a bore stays round however close the camera goes. Ask for it when the viewer is made, as main.js does, or switch a viewer that is already drawing, and back:
const viewer = await Viewer.create(canvas, { surfaces: "exact" });
viewer.setSurfaces("triangles");
viewer.setSurfaces("exact");
Exact surfaces need WebGPU. On WebGL2, or where the exact shaders did not build, the viewer draws triangles as before, and viewer.surfaces says "triangles": it names what is drawn, not what was asked for, so it can read "triangles" right after create until something exact is shown, and may turn to "triangles" a frame later on a device whose exact shaders fail. A solid's face colours are kept in exact mode; meshes of your own and STL files draw triangles in either mode. It is worth it for a part, or a few hundred: a large assembly costs CPU every moving frame, and a 7,900-part assembly orbits more smoothly as triangles.
Where next
In a web page is the reference: the five builds in the two zips, every method of the Viewer, the kernel and the license. pkg/cadaclysm.d.ts types every export. A page that throws a viewer away calls stop() and then free(): the loop ends first, then the viewer lets go of its canvas, its GPU buffers and what it drew.