Browser-native CAD kernel for Web Apps and AI CAD built with Rust, WebAssembly, and Three.js.
Website · Documentation · Live Demos · Blog · npm
Actively maintained and growing. We're building OpenGeometry in the open. APIs, examples, and package structure are evolving, and we are actively improving and expanding the project. Star the repo to follow along. If you have questions or want to get involved, join the Discord or check out the issues.
Version note. This README describes OpenGeometry 3.0.0. Check which line you have installed with
npm ls opengeometry: the 2.0 line has a different API. The hosted documentation still describes 2.0.
OpenGeometry is an open-source CAD kernel for the browser. It models B-rep solids, runs boolean operations (union, subtract, intersect), builds solids by extrude, sweep and revolve, and writes STEP files. The kernel is written in Rust and compiled to WebAssembly, and a TypeScript SDK for Three.js puts every body straight into your scene.
OpenGeometry is best suited for browser-based CAD, configurators, and geometry-heavy web tools. Whether you're building a parametric modeler, a solid modeling workflow, a geometry viewer, or a custom Three.js modeling tool, OpenGeometry gives you deterministic, kernel-backed primitives and operations without leaving JavaScript.
It is the geometry engine layer, not a full CAD application. Floorkit is a downstream application/toolkit built on top of OpenGeometry for AEC workflows. In this repository, OpenGeometry is the primary SDK and engine.
| Area | What 3.0 has |
|---|---|
| Primitives | Wires: rectangle, circle, polyline (open or closed), line-and-arc profile, arc. Solids: cuboid, cylinder, cylinder sector, sphere, cone or frustum, ring torus, planar polyhedron. Each can be placed on any plane. |
| Operations | Extrude a closed wire into a solid, with holes. Sweep a profile along an open or closed path of lines and arcs, each arc an exact bend, capped into a solid, open-ended into a sheet, or closed into a ring. Revolve a closed line-and-arc profile a full turn about an axis in its plane. Shell a solid into a hollow one with walls of one thickness. Union, subtract and intersect, with one to a hundred tool solids in one call. |
| Parametric edits | rebuild gives an existing body new parameters in place. mirror rebuilds a body as its reflection across a plane. |
| Assemblies | Assemblies with parent and child placement; translate, rotate, scale and place; independent copies and shared instances. |
| History | Marks with rollback and release; transactions, with a dry-run option. |
| Display for Three.js | Every body is a THREE.Group. Level of detail that follows the camera, tessellation in a web worker or inline, picking down to face and edge, outlines, colour and opacity. |
| Queries | Bounds, placements, the B-rep of a body, that B-rep placed in the world, the normal of a face at a point, whether a point is inside, volume and area within a stated error bound, whether a body is valid and closed, and the report of its last boolean. |
| Export | STEP, in millimetres or metres, with Y or Z up, together with an export report. The IFC exchange body of a solid, in world metres with Z up, for an IFC writer. |
Not in 3.0, though 2.0 had them: IFC file text and STL export; projection; loft; triangulation as a public call; curves other than lines, circles and arcs; wedges; and the 2.0 classes such as Cuboid, Polygon, Opening and Vector3.
npm install opengeometry threethreeis a peer dependency: versions>=0.168.0 <0.185.0are supported.- The package is ES modules only, and its TypeScript types are included.
- The package ships
opengeometry_bg.wasm(the kernel) andtessellation-worker.js(the display worker), and your page must be able to fetch both. With no arguments,OpenGeometry.create()looks for them next to the package's ownindex.js. If your bundler moves or rewritesindex.jswithout the two files, that lookup fails, so the dependable way is the one the quick start uses: copy both files fromnode_modules/opengeometry/into the folder your site serves at its root (in Vite, thepublic/folder by default) and pass their URLs tocreate.
import * as THREE from 'three';
import {
OpenGeometry, Solid, Wire,
OG_OPERATION_EXTRUDE, OG_OPERATION_SUBTRACT, OG_PRIMITIVE_CUBOID, OG_PRIMITIVE_RECTANGLE, OG_TRANSFORM_TRANSLATE,
} from 'opengeometry';
await OpenGeometry.create({ wasmURL: '/opengeometry_bg.wasm' }, { workerURL: '/tessellation-worker.js' });
const profile = new Wire(OG_PRIMITIVE_RECTANGLE, { width: 6, breadth: 0.2 });
const wall = new Solid(OG_OPERATION_EXTRUDE, { profile, distance: 3 });
const door = new Solid(OG_PRIMITIVE_CUBOID, { width: 0.9, height: 2.1, depth: 0.4 });
door.transform(OG_TRANSFORM_TRANSLATE, { offset: [2, 0, 0] });
wall.operate(OG_OPERATION_SUBTRACT, { tools: [door] });
const scene = new THREE.Scene();
scene.add(new THREE.HemisphereLight(0xffffff, 0x444444, 2), wall);
const camera = new THREE.PerspectiveCamera(50, innerWidth / innerHeight, 0.1, 100);
camera.position.set(6, 5, 8);
camera.lookAt(0, 1.5, 0);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(innerWidth, innerHeight);
document.body.append(renderer.domElement);
renderer.setAnimationLoop(() => renderer.render(scene, camera));
const { text, report } = await OpenGeometry.exportStep({ nodes: [wall], unit: 'millimetre', upAxis: 'Z' });This loads the kernel, makes a 6 m by 0.2 m rectangle wire on the ground plane, extrudes it 3 m up into a wall, and cuts a door opening through the wall with a cuboid placed at x = 2. The wall is a THREE.Group, so it goes into the scene like any other object and is drawn on every frame. The last line writes the wall as a STEP file: text is the file and report counts what was written.
The two URLs assume both files were copied to the site root, as described under Installation. The block uses top-level await, so it must run as a module. If your build rejects top-level await, raise its target (the examples set Vite's build.target to 'esnext') or move the code after the imports into an async function and call it.
Runnable examples live in main/opengeometry-three/examples-vite/: a showcase, a sweep, booleans, an opening, a cuboid, a cylinder and STEP export. From a clone, run npm --prefix main ci, npm run build and then npm run dev-example.
See OpenGeometry in action — interactive, browser-based demos showcasing the kernel's capabilities:
Demos include primitives rendering, shape generation, sweep operations, boolean operations, file exports, and more. All running client-side via WebAssembly.
Use OpenGeometry when you need:
- browser-based parametric modeling with Rust + WebAssembly performance
- cutout subtraction and other solid boolean workflows
- profile extrusion into solids for CAD or AEC modeling
- STEP export in web apps
- a Three.js-friendly CAD kernel instead of ad hoc mesh math
- a deterministic geometry engine behind AI-assisted CAD or design workflows
OpenGeometry is a good fit for AI-assisted CAD apps because the kernel layer stays explicit and deterministic. An AI assistant or agent can suggest modeling steps, generate profiles, or orchestrate edits, while OpenGeometry executes the actual geometry operations, booleans, and exports in a predictable browser runtime.
Good examples include:
- AI assistants that translate user intent into concrete modeling operations
- prompt-to-geometry or agent-driven editing flows inside browser CAD tools
- AI-first design interfaces that still need reliable extrusion, boolean, and export workflows
Good fit
- browser CAD, mechanical design, and geometry-heavy web applications
- Three.js-based modeling tools that need a real kernel behind them
- AI-first CAD frontends that need deterministic geometry execution in the browser
Not the default fit
- desktop-native CAD products instead of an embeddable web SDK
- non-browser runtimes with no WebAssembly or browser delivery story
- pure visualization-only apps where raw Three.js is enough and kernel-backed modeling is unnecessary
- Teams building browser-based CAD and geometry tools
- Developers evaluating WebAssembly-powered 3D for the web
- Contributors interested in the Rust → WASM geometry pipeline
- Anyone exploring open-source CAD kernel internals
If you just want a quick look, start with the hosted demos or the examples.
This section is a compact reference for writing OpenGeometry 3.0 code, by hand or with a coding assistant.
Package. opengeometry on npm. ES modules only, types included, peer dependency three >=0.168.0 <0.185.0.
Which line is installed. The 3.0 entry exports OpenGeometry.create, Solid and Wire. Code that uses Cuboid, Vector3 or setPlacement is written for 2.0 and does not work with 3.0. The version in node_modules/opengeometry/package.json settles it.
The whole public surface. The entry exports OpenGeometry, Solid, Sheet, Wire, SystemAssembly, OGMark, OGError and the OG_* constants. Every point and vector is [x, y, z]. Model units are metres, and Y is up.
| Export | Call and parameters |
|---|---|
OpenGeometry.create |
await OpenGeometry.create(input?, options?). input: { wasmURL?, wasmModule? }. options: { workerURL?, tessellation?: 'worker' | 'inline' }. |
Wire |
new Wire(kind, params, options?). OG_PRIMITIVE_RECTANGLE { width, breadth }, OG_PRIMITIVE_CIRCLE { radius }, OG_PRIMITIVE_POLYLINE { points, closed? }. |
| Profile wire | new Wire(OG_PRIMITIVE_PROFILE, { start, segments, closed }, options?). Points are [u, v] in plane; a segment is { to } for a line or { to, arc: { center, counterClockwise } } for an arc. A closed profile is an extrude profile or hole. |
| Arc wire | new Wire(OG_PRIMITIVE_ARC, { radius, startAngle, sweepAngle }, options?). Angles in radians from the plane x direction, centred on the plane origin; the sweep is nonzero and less than one turn. |
Solid |
new Solid(kind, params, options?). OG_PRIMITIVE_CUBOID { width, height, depth }, OG_PRIMITIVE_CYLINDER { radius, height }, OG_PRIMITIVE_CYLINDER_SECTOR { radius, height, startAngle, sweepAngle } (radians from the plane x direction, counter-clockwise seen from the side the normal points to; sweep nonzero and less than one turn), OG_PRIMITIVE_POLYHEDRON { points, faces } (planar faces as rings of point indices, counter-clockwise seen from outside), OG_PRIMITIVE_SPHERE { radius } (centred on the plane origin), OG_PRIMITIVE_CONE { radiusBottom, radiusTop, height } (standing on the plane; one radius may be zero, equal radii make a cylinder), OG_PRIMITIVE_TORUS { majorRadius, minorRadius } (lying on the plane; minor below major), OG_OPERATION_EXTRUDE { profile, holes?, distance }, OG_OPERATION_SWEEP { profile, path, caps? }, OG_OPERATION_REVOLVE { profile, axis? } (a full turn about axis { point: [u, v], direction: [du, dv] } in the profile's [u, v], by default its v axis); profile, holes and path are wires. |
Sheet |
new Sheet(OG_OPERATION_SWEEP, { profile, path, caps: false }): an open-ended sweep, faces that enclose no volume. It shows its surface, from the front only, and its outline; OpenGeometry.exportStep refuses it. |
| Body options | { ogId?, parent?, appearance?, plane? }. plane is { origin?, normal?, xDirection? } and applies to primitives only; by default it is the ground plane through the origin, normal [0, 1, 0], x direction [1, 0, 0]. |
| Booleans | solid.operate(kind, { tools, instances? }) with OG_OPERATION_SUBTRACT, OG_OPERATION_UNION or OG_OPERATION_INTERSECT; tools is one to a hundred solids. |
| Shell | solid.operate(OG_OPERATION_SHELL, { thickness, instances? }) hollows the solid in place and leaves walls thickness thick inside every face. |
| Transforms | node.transform(kind, params) on a body or an assembly: OG_TRANSFORM_TRANSLATE { offset }, OG_TRANSFORM_ROTATE { axis, degrees, pivot? }, OG_TRANSFORM_SCALE { factor, pivot? }, OG_TRANSFORM_PLACE { origin?, xDirection?, normal?, scale? }. getPlacement() and getWorldPlacement() return { origin, xDirection, normal, scale }. |
| Edits | body.rebuild(kind, params, { instances? }) with the kinds and parameters above; a wire stays a wire and a solid a solid. |
| Mirror | body.mirror({ origin, normal }, { instances? }) rebuilds the shape as its reflection across the plane through origin with normal normal, both in the body's own coordinates (type MirrorPlane). The placement does not change. A placement cannot hold a reflection: a negative scale throws InvalidTransform. |
| Copies | body.duplicate({ ogId?, parent? }) makes an independent copy; body.instance({ ogId?, parent? }) shares the shape. getInstanceCount(), makeUnique(). |
SystemAssembly |
new SystemAssembly({ ogId?, parent? }). addChild(children, { keepWorld? }), removeChild(child, { keepWorld? }), getChildren(), getParent(), getBounds(), transform, dispose(). Bodies have the same tree methods. |
| Queries | getBounds() ([minX, minY, minZ, maxX, maxY, maxZ], or null), getBrep(), getWorldBrep() (the BRep placed in the world), faceNormalAt(faceId, [x, y, z]) (the face's unit normal in the body's own coordinates), classifyPoint([x, y, z]) ('Inside', 'Outside', 'Boundary' or 'Unknown', in the body's own coordinates), measure({ deflection? }) ({ volume, area, errorBound, deflection }; the volume is within errorBound of the exact one), validity() ({ valid, closed, bodyType, error? }), solid.getReport() (the last boolean's report, or null). |
| Appearance | appearance in the options, or body.setAppearance({ color?, opacity?, outline?, pickOutline?, deflection? }). |
| Display | OpenGeometry.settled({ deflection? }) resolves to { failed }; OpenGeometry.flush({ geometry?: 'sync', bodies? }) (with bodies, meshes and places only those bodies now; hidden bodies, bodies with no shape and bodies in limbo (disposed while a mark is live) are skipped; a fully disposed body or one from a reset runtime throws Disposed before anything is meshed; bodies without geometry: 'sync', a non-array bodies or an item that is not a Wire, Sheet or Solid throws InvalidParameter); OpenGeometry.setDisplayDeflection(world?); OpenGeometry.setCameraMotion(moving). |
| Picking | OpenGeometry.resolveHit(intersection) takes a Three.js raycast hit and returns { ogId, shapeRevision, faceId?, edgeId? } or undefined. |
OGMark |
const mark = OpenGeometry.mark(), then mark.rollback() and mark.release(). OpenGeometry.markStats() returns { liveMarks, retainedRevisions }. OpenGeometry.transaction(fn, { dryRun? }) runs a synchronous fn and rolls back if it throws. |
| Events | OpenGeometry.on(event, handler) with 'geometry', 'warning', 'error' or 'fatal'; it returns a function that unsubscribes. |
| STEP | await OpenGeometry.exportStep({ nodes, unit?, upAxis?, name?, timestamp? }) returns { text, report }. nodes: bodies, assemblies or ogId strings. unit: 'millimetre' (default) or 'metre'; upAxis: 'Z' (default) or 'Y'. |
| IFC body | OpenGeometry.exportIfcBody(solid) returns the IFC exchange body (type AnalyticExchangeBodyV2): schema_version 2, world metres, Z up, representation_type 'AdvancedBrep' or 'CSG' for a solid with a cavity, entities, solids and faces. It refuses a Sheet, a Wire or an assembly. |
| 2D regions | OpenGeometry.booleanRegions2D(a, b, operation) and OpenGeometry.booleanCurvedRegions2D(a, b, operation) with 'union', 'intersection' or 'subtraction'. A region is { outer, holes }: an outer ring and a list of hole rings, each ring of { x, y, z } points at one height (type OffsetRegion) or of line and arc edges (type CurvedRegion2D). Outer rings run clockwise in XZ and holes counter-clockwise. A plain ring is not a region and throws InvalidParameter; the tolerance is OG_REGION_TOLERANCE. |
| 2D offsets | OpenGeometry.offsetRingVariable(ring, distances, holes?) insets a closed ring by one distance per edge, each hole { ring, distances } grown by its own; OpenGeometry.offsetPolylineRegions(path, width, closed, miterLimit?) strokes a path. Both return CurvedRegion2D[] with exact arcs. OpenGeometry.offsetPolyline(path, distance, { acuteThresholdDegrees?, bevel?, closed? }) offsets a path to one side and returns { edges, bevelled, closed } (type OffsetPath2D). Rings and paths are flat [x, y, z, ...] lists; y is ignored. |
OGError |
Failures throw an OGError with code (for example InvalidParameter, SharedShape, Disposed), call, message and details. |
| Reset | OpenGeometry.reset() frees every body and leaves an empty document. |
| Typed parameters | OG_PRIMITIVE_PARAMS_<KIND>(params), OG_OPERATION_PARAMS_EXTRUDE(params), OG_OPERATION_PARAMS_SWEEP(params) and OG_OPERATION_PARAMS_REVOLVE(params) return their argument unchanged; they only give the object a type. |
Rules that are easy to get wrong.
-
Await
OpenGeometry.createonce before creating any body. -
workerURLbelongs to the second argument ofcreate, not the first. -
Node has no
Workerand cannot fetch afile:URL. Pass the compiled module instead; tessellation then runs inline:import { readFileSync } from 'node:fs'; import { OpenGeometry } from 'opengeometry'; const wasm = readFileSync(new URL(import.meta.resolve('opengeometry/opengeometry_bg.wasm'))); await OpenGeometry.create({ wasmModule: new WebAssembly.Module(wasm) });
-
operatechanges the target's shape once. The tools stay separate, live solids: moving or disposing a tool later does not change the target, and a tool is drawn only if you add it to the scene. -
rebuildreplaces the shape, so an earlieroperateresult is gone; calloperateagain after it. -
A shape shared by instances cannot be edited one node at a time: pass
{ instances: 'all' }or callmakeUnique()first, or the call throwsSharedShape. -
Move bodies with
transform. Settingbody.position,rotationorscaleis reverted with aPlacementOverwrittenwarning. -
Add bodies to the Three.js scene yourself; a
SystemAssemblyis not a Three.js object. -
Surfaces use
MeshStandardMaterial, so the scene needs a light. -
Rendering is what asks for geometry. Render in a loop, or render once,
await OpenGeometry.settled(), and render again.settledreturns at once if nothing has been rendered. -
In Node nothing renders, so nothing asks for geometry: add the body to a
THREE.Scene, callscene.updateMatrixWorld(), then callOpenGeometry.flush({ geometry: 'sync' })orawait OpenGeometry.settled(); the mesh is then inbody.surface.geometry. After an edit,OpenGeometry.flush({ geometry: 'sync', bodies: [body] })meshes only that body and places its mesh, so the next frame shows it with no extra scene walk. -
STEP export defaults to millimetres and Z up. A wire cannot be exported on its own: the selection must contain a solid.
-
To check that a body is a valid solid, call
body.validity(): a valid closed solid gives{ valid: true, closed: true, bodyType: 'Solid' }. -
Call
dispose()on bodies and assemblies you no longer need. -
Catch
OGErrorand branch oncode. After a'fatal'event the runtime refuses every call untilOpenGeometry.reset().
What does not exist. IFC file text, STL and PDF export, projection, loft, public triangulation, curves other than lines, circles and arcs, wedges, and the 2.0 classes. Do not call them.
Where the truth is. index.d.ts in the installed package lists every export and its types. The examples in main/opengeometry-three/examples-vite/ are working 3.0 code, except that they import the built package by path (../../../dist/index.js) where an app imports from 'opengeometry'.
The hosted documentation at docs.opengeometry.io still describes the 2.0 API. For 3.0, use this README, the types in the package and the examples. To move code written for 2.0 to 3.0, follow MIGRATION.md.
| Path | What it holds |
|---|---|
main/opengeometry/ |
The Rust kernel, built to WebAssembly |
main/opengeometry-three/ |
The Three.js SDK, published as opengeometry |
main/scripts/ |
Build, check, benchmark and snapshot scripts |
docs/ |
The 2.0 documentation source, not yet rewritten for 3.0 |
Prerequisites: Node.js 26 and npm, Rust 1.88.0 with the wasm32-unknown-unknown target (pinned in main/rust-toolchain.toml), and wasm-pack.
npm --prefix main ci
npm run build
npm run dev-example
npm test
npm run check
npm run check:fullnpm --prefix main ciinstalls the dependencies.npm run buildbuilds the kernel to WebAssembly and the SDK, and writes the package tomain/dist/.npm run dev-exampleserves the example pages on a local Vite server; build first.npm testruns the kernel's Rust tests.npm run checkruns the gate: formatting, lints, type checks, source rules, the build, the Rust, WebAssembly and Node tests, and the performance budgets.npm run check:fullruns the same gate plus the browser tests on three 0.168 and 0.184. The browser tests need Playwright's Chromium, installed withnpx playwright install chromiuminmain/(on Linux, add--with-depsto install the system libraries, as CI does).
More in developer.md.
If your project uses OpenGeometry, please add the badge to your README and link back to us. It helps other people find the project.
[](https://opengeometry.io?utm_source=built-with-badge)Then tell us about it on Discord: we like to see what you build.
We'd love to have you involved — whether you're using OpenGeometry, building on it, or just curious.
- Discord — Chat with the team and community
- Twitter / X — Updates and announcements
- LinkedIn — Company updates
- GitHub Issues — Bug reports and feature requests
- Blog — Deep dives and release notes
Contributions are welcome — check the issues for good starting points or open a discussion on Discord. developer.md covers building, checks and releases.
OpenGeometry is released under the Mozilla Public License 2.0 (MPL-2.0). See LICENSE.md.
"It is my land. Who would I be if I did not try to make it better?" — A Knight.