diff --git a/README.md b/README.md index 5a159d2..52e3407 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,16 @@
- Browser-native CAD kernel for Web Apps and AI CAD built with Rust, WebAssembly, and Three.js. + Open-source CAD kernel for the web: exact B-rep solids, booleans, extrude, sweep and revolve, and STEP export, in Rust and WebAssembly with a TypeScript SDK for Three.js. For product configurators, mechanical parts, buildings, robotics and AI agents that model.
@@ -19,7 +22,7 @@
- Website · Documentation · Live Demos · Blog · npm + Website · Documentation · Live Demos · Blog · npm
--- @@ -32,11 +35,24 @@ ## What is OpenGeometry? -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 an **open-source CAD kernel for the web**. It models **B-rep solids** with exact planes, cylinders, spheres, cones and tori, runs **boolean operations** (union, subtract, intersect), builds solids by **extrude, sweep and revolve**, hollows them with **shell**, and writes **STEP** files that other CAD and CAM tools open. The kernel is written in **Rust** and compiled to **WebAssembly**. A **TypeScript SDK for Three.js** puts every body straight into your scene, and the same calls run in **Node.js** with no browser. -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, not a CAD application: you build the product, OpenGeometry does the solid modelling. The same calls give the same shape every time, so an app, a script or an AI agent can drive it and check the result with exact queries: volume, area, bounds, validity, and whether a point is inside. -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. +## Use cases + +| Field | What you build | What OpenGeometry gives you | +| --- | --- | --- | +| **Product configurators and e-commerce** | A shopper picks sizes and options and sees the exact part, then orders it | Parametric primitives that `rebuild` in place, booleans for cut-outs, STEP for the factory | +| **Mechanical parts and enclosures** | Plates, brackets, housings, flanges, pipe parts | Rounded boxes, slots, cylinders, pipe segments, through-holes in one call, shell for walls, STEP in millimetres | +| **Manufacturing and fabrication** | Web tools that turn an order into a production file | Exact B-rep, valid and closed solids, STEP export with an export report | +| **Architecture, construction and interiors** | Walls, openings, slabs, furniture, MEP parts | Polysolid walls along a path, extrude with holes, sweeps along arcs, the IFC exchange body for an IFC writer | +| **Robotics and automation** | Fixtures, grippers, end-of-arm tools, work-cell layouts | Assemblies with placements, point-inside tests, measured volume, picking down to face and edge | +| **AI agents and prompt-to-CAD** | An agent that turns a request into a part, then checks it | Plain JSON parameters, deterministic results, `validity()` and `measure()` to verify, typed errors with a `code` | +| **Education and geometry tools** | Teaching solids, booleans and B-rep in a browser tab | Open source, runs client-side, the B-rep of every body is readable | +| **Server-side generation** | A Node.js service that makes STEP files from parameters | The same API in Node, with inline tessellation and no browser | + +Floorkit, a toolkit for building design, is one application built on OpenGeometry. ## Features @@ -68,92 +84,61 @@ npm install opengeometry three ```ts import * as THREE from 'three'; import { - OpenGeometry, Solid, Wire, - OG_OPERATION_EXTRUDE, OG_OPERATION_SUBTRACT, OG_PRIMITIVE_CUBOID, OG_PRIMITIVE_RECTANGLE, OG_TRANSFORM_TRANSLATE, + OpenGeometry, Solid, + OG_OPERATION_SUBTRACT, OG_PRIMITIVE_CYLINDER, OG_PRIMITIVE_ROUNDED_BOX, 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 plate = new Solid(OG_PRIMITIVE_ROUNDED_BOX, { width: 0.12, height: 0.01, depth: 0.08, radius: 0.01 }); +const holes = [[-0.045, -0.025], [0.045, -0.025], [-0.045, 0.025], [0.045, 0.025]].map(([x, z]) => { + const hole = new Solid(OG_PRIMITIVE_CYLINDER, { radius: 0.004, height: 0.02 }); + hole.transform(OG_TRANSFORM_TRANSLATE, { offset: [x, -0.005, z] }); + return hole; +}); +plate.operate(OG_OPERATION_SUBTRACT, { tools: holes }); 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); +scene.add(new THREE.HemisphereLight(0xffffff, 0x444444, 2), plate); +const camera = new THREE.PerspectiveCamera(50, innerWidth / innerHeight, 0.001, 10); +camera.position.set(0.12, 0.1, 0.14); +camera.lookAt(0, 0, 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' }); +const { text, report } = await OpenGeometry.exportStep({ nodes: [plate], 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. +This loads the kernel and makes a 120 mm by 80 mm mounting plate, 10 mm thick, with rounded corners (model units are metres). It drills four 8 mm bolt holes through the plate with one boolean call: the four cylinders are the tools. The plate 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 plate as a STEP file in millimetres, ready for a CAD or CAM tool: `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/`](./main/opengeometry-three/examples-vite/): a showcase, a sweep, booleans, an opening, a cuboid, a cylinder, a rounded section, a sweep along an arc path, a curved profile united with a straight one, and STEP export. From a clone, run `npm --prefix main ci`, `npm run build` and then `npm run dev-example`. +Runnable examples live in [`main/opengeometry-three/examples-vite/`](./main/opengeometry-three/examples-vite/). Shapes: cuboid, cylinder, slot prism, rounded box, pipe segment, polysolid wall, the regular polygon, slot and rounded rectangle wires, and a rounded section. Operations: booleans, an opening, a sweep, a sweep along an arc path, a curved profile united with a straight one, and STEP export. From a clone, run `npm --prefix main ci`, `npm run build` and then `npm run dev-example`. ## Demos -See OpenGeometry in action — interactive, browser-based demos showcasing the kernel's capabilities: - -**[demos.opengeometry.io](https://demos.opengeometry.io?utm_source=github)** - -Demos include primitives rendering, shape generation, sweep operations, boolean operations, file exports, and more. All running client-side via WebAssembly. - -## When to use OpenGeometry - -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 - -### Why it works well for AI-powered CAD apps - -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 / Not the right fit - -**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 +Interactive demos run the kernel in your browser, with nothing to install: **[demos.opengeometry.io](https://demos.opengeometry.io?utm_source=github)**. -**Not the default fit** +## When not to use it -- 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 +- You need a desktop CAD application. OpenGeometry is a library you embed. +- You only show meshes you already have. Plain Three.js is enough. +- You need splines, ellipses, lofts, fillets or STL export. 3.0 does not have them. -## Who is this for? +## For AI agents and coding assistants -- 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** +OpenGeometry is built to be driven by code, including code an agent writes. Every call takes plain parameters, gives the same shape for the same input, and can be checked with `validity()`, `measure()`, `getBounds()` and `classifyPoint()`. -If you just want a quick look, start with the [hosted demos](https://demos.opengeometry.io?utm_source=github) or the [examples](./main/opengeometry-three/examples-vite/). +- **[llms.txt](https://docs.opengeometry.io/llms.txt)**: an index of every documentation page, in Markdown, for a model's context. +- **[Use a coding agent](https://docs.opengeometry.io/opengeometry/agents)**: the 3.0 rules that agents get wrong most often, and the sources to trust. +- **`index.d.ts`** in the installed package: every export and its types. +- The reference below: the whole public surface on one page. -## Using OpenGeometry with AI coding assistants +### API reference on one page -This section is a compact reference for writing OpenGeometry 3.0 code, by hand or with a coding assistant. +This 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`. diff --git a/docs/docs.json b/docs/docs.json index 42caa33..74a04b6 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -19,7 +19,9 @@ "metatags": { "og:site_name": "OpenGeometry Docs", "og:locale": "en_US", - "twitter:card": "summary", + "og:image": "https://docs.opengeometry.io/logo/og.png", + "twitter:card": "summary_large_image", + "twitter:image": "https://docs.opengeometry.io/logo/og.png", "twitter:site": "@openGeometry" } }, @@ -245,10 +247,10 @@ } ], "logo": { - "light": "/logo/logoDark.png", - "dark": "/logo/logoLight.png" + "light": "/logo/wordmark-light.png", + "dark": "/logo/wordmark-dark.png" }, - "favicon": "/favicon.svg", + "favicon": "/favicon.png", "navbar": { "links": [ { diff --git a/docs/favicon.png b/docs/favicon.png new file mode 100644 index 0000000..df02444 Binary files /dev/null and b/docs/favicon.png differ diff --git a/docs/favicon.svg b/docs/favicon.svg deleted file mode 100644 index b3c55bb..0000000 --- a/docs/favicon.svg +++ /dev/null @@ -1,5 +0,0 @@ - diff --git a/docs/logo/logo-dark.png b/docs/logo/logo-dark.png new file mode 100644 index 0000000..c06e4b0 Binary files /dev/null and b/docs/logo/logo-dark.png differ diff --git a/docs/logo/logo-light.png b/docs/logo/logo-light.png new file mode 100644 index 0000000..3514965 Binary files /dev/null and b/docs/logo/logo-light.png differ diff --git a/docs/logo/logoDark.png b/docs/logo/logoDark.png deleted file mode 100644 index 80407b6..0000000 Binary files a/docs/logo/logoDark.png and /dev/null differ diff --git a/docs/logo/logoLight.png b/docs/logo/logoLight.png deleted file mode 100644 index cf79d3e..0000000 Binary files a/docs/logo/logoLight.png and /dev/null differ diff --git a/docs/logo/og.png b/docs/logo/og.png new file mode 100644 index 0000000..f4db409 Binary files /dev/null and b/docs/logo/og.png differ diff --git a/docs/logo/wordmark-dark.png b/docs/logo/wordmark-dark.png new file mode 100644 index 0000000..b49bf87 Binary files /dev/null and b/docs/logo/wordmark-dark.png differ diff --git a/docs/logo/wordmark-light.png b/docs/logo/wordmark-light.png new file mode 100644 index 0000000..3dfa135 Binary files /dev/null and b/docs/logo/wordmark-light.png differ diff --git a/main/opengeometry-three/tests/browser/pages/quick-start.ts b/main/opengeometry-three/tests/browser/pages/quick-start.ts index ca660ba..55f8718 100644 --- a/main/opengeometry-three/tests/browser/pages/quick-start.ts +++ b/main/opengeometry-three/tests/browser/pages/quick-start.ts @@ -1,7 +1,7 @@ import * as THREE from 'three'; import { - OpenGeometry, Solid, Wire, - OG_OPERATION_EXTRUDE, OG_OPERATION_SUBTRACT, OG_PRIMITIVE_CUBOID, OG_PRIMITIVE_RECTANGLE, OG_TRANSFORM_TRANSLATE, + OpenGeometry, Solid, + OG_OPERATION_SUBTRACT, OG_PRIMITIVE_CYLINDER, OG_PRIMITIVE_ROUNDED_BOX, OG_TRANSFORM_TRANSLATE, } from '../../../../dist/index.js'; import { activeBackend } from '../../../../dist/testing.js'; import { createRenderer, publishFixture, releasePage } from '../support/test-page'; @@ -10,28 +10,30 @@ await OpenGeometry.create({ wasmURL: '/opengeometry_bg.wasm' }, { workerURL: '/t const ERRORS: unknown[] = []; OpenGeometry.on('error', (event) => { ERRORS.push(event); }); -const PROFILE = new Wire(OG_PRIMITIVE_RECTANGLE, { width: 6, breadth: 0.2 }); -const WALL = new Solid(OG_OPERATION_EXTRUDE, { profile: 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 PLATE = new Solid(OG_PRIMITIVE_ROUNDED_BOX, { width: 0.12, height: 0.01, depth: 0.08, radius: 0.01 }); +const HOLES = [[-0.045, -0.025], [0.045, -0.025], [-0.045, 0.025], [0.045, 0.025]].map(([x, z]) => { + const hole = new Solid(OG_PRIMITIVE_CYLINDER, { radius: 0.004, height: 0.02 }); + hole.transform(OG_TRANSFORM_TRANSLATE, { offset: [x, -0.005, z] }); + return hole; +}); +PLATE.operate(OG_OPERATION_SUBTRACT, { tools: HOLES }); 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); +SCENE.add(new THREE.HemisphereLight(0xffffff, 0x444444, 2), PLATE); +const CAMERA = new THREE.PerspectiveCamera(50, innerWidth / innerHeight, 0.001, 10); +CAMERA.position.set(0.12, 0.1, 0.14); +CAMERA.lookAt(0, 0, 0); const RENDERER = createRenderer(innerWidth, innerHeight); RENDERER.render(SCENE, CAMERA); const SETTLED = await OpenGeometry.settled(); RENDERER.render(SCENE, CAMERA); RENDERER.setAnimationLoop(() => { RENDERER.render(SCENE, CAMERA); }); -const STEP = await OpenGeometry.exportStep({ nodes: [WALL], unit: 'millimetre', upAxis: 'Z' }); +const STEP = await OpenGeometry.exportStep({ nodes: [PLATE], unit: 'millimetre', upAxis: 'Z' }); publishFixture('ogQuickStart', { - wall: WALL, - bounds: WALL.getBounds(), + plate: PLATE, + bounds: PLATE.getBounds(), renderer: RENDERER, settled: SETTLED, report: STEP.report, diff --git a/main/opengeometry-three/tests/browser/specs/quick-start.spec.ts b/main/opengeometry-three/tests/browser/specs/quick-start.spec.ts index 2b508a1..6efcdc4 100644 --- a/main/opengeometry-three/tests/browser/specs/quick-start.spec.ts +++ b/main/opengeometry-three/tests/browser/specs/quick-start.spec.ts @@ -5,16 +5,17 @@ function checkQuickStart(): void { if (!fixture) throw new Error('The quick start page published no fixture'); if (fixture.errors.length) throw new Error(`The SDK emitted errors: ${JSON.stringify(fixture.errors)}`); if (fixture.backend !== 'worker') throw new Error(`Expected worker, got ${fixture.backend}`); - if (fixture.settled.failed.length || !fixture.wall.record) throw new Error('The wall did not settle'); + if (fixture.settled.failed.length || !fixture.plate.record) throw new Error('The plate did not settle'); const { products, solids, faces } = fixture.report; - if (products !== 1 || solids !== 1 || faces !== 22) { + if (products !== 1 || solids !== 1 || faces !== 14) { throw new Error(`STEP counts differ: ${JSON.stringify({ products, solids, faces })}`); } if (!fixture.text.startsWith('ISO-10303-21')) throw new Error('The STEP text does not start with ISO-10303-21'); const bounds = fixture.bounds; const near = (actual: number, target: number): boolean => Math.abs(actual - target) <= 1e-6; - if (!bounds || !near(bounds[0], -3) || !near(bounds[3], 3) || !near(bounds[1], 0) || !near(bounds[4], 3)) { - throw new Error(`The wall bounds differ: ${JSON.stringify(bounds)}`); + const expected = [-0.06, 0, -0.04, 0.06, 0.01, 0.04]; + if (!bounds || expected.some((target, index) => !near(bounds[index] ?? Number.NaN, target))) { + throw new Error(`The plate bounds differ: ${JSON.stringify(bounds)}`); } } @@ -36,7 +37,7 @@ function countDrawnPixels(): number { return drawn; } -test('the README quick start boots from URLs, draws the wall and exports it', async ({ page }) => { +test('the README quick start boots from URLs, draws the plate and exports it', async ({ page }) => { const problems: string[] = []; page.on('pageerror', (error) => { problems.push(error.message); }); page.on('console', (message) => { if (message.type() === 'error') problems.push(message.text()); }); diff --git a/main/opengeometry-three/tests/browser/support/fixture-types.ts b/main/opengeometry-three/tests/browser/support/fixture-types.ts index c52ff6d..3febace 100644 --- a/main/opengeometry-three/tests/browser/support/fixture-types.ts +++ b/main/opengeometry-three/tests/browser/support/fixture-types.ts @@ -177,7 +177,7 @@ export type AcceptanceFixture = { }; export type QuickStartFixture = { - wall: Solid; + plate: Solid; bounds: Bounds | null; renderer: THREE.WebGLRenderer; settled: Awaited