From 1a33cd1f36a7514d05115566d02c50c8b32d29be Mon Sep 17 00:00:00 2001 From: blackboots <47943405+aka-blackboots@users.noreply.github.com> Date: Wed, 4 Mar 2026 09:46:00 +0100 Subject: [PATCH] Add voxel-constrained boolean operations across kernel and three bindings --- ...03-04-boolean-operations-implementation.md | 86 ++++ .../examples-vite/operations/boolean.html | 1 + .../src/pages/operations-boolean.ts | 64 +++ main/opengeometry-three/index.ts | 5 + .../src/operations/boolean.ts | 72 +++ .../src/operations/index.ts | 1 + main/opengeometry/src/lib.rs | 1 + main/opengeometry/src/operations/boolean.rs | 453 ++++++++++++++++++ 8 files changed, 683 insertions(+) create mode 100644 AI-DOCs/opengeometry/2026-03-04-boolean-operations-implementation.md create mode 100644 main/opengeometry-three/examples-vite/operations/boolean.html create mode 100644 main/opengeometry-three/examples-vite/src/pages/operations-boolean.ts create mode 100644 main/opengeometry-three/src/operations/boolean.ts create mode 100644 main/opengeometry-three/src/operations/index.ts create mode 100644 main/opengeometry/src/operations/boolean.rs diff --git a/AI-DOCs/opengeometry/2026-03-04-boolean-operations-implementation.md b/AI-DOCs/opengeometry/2026-03-04-boolean-operations-implementation.md new file mode 100644 index 0000000..d947b15 --- /dev/null +++ b/AI-DOCs/opengeometry/2026-03-04-boolean-operations-implementation.md @@ -0,0 +1,86 @@ +# Boolean Operations System (Kernel + JS Bindings + Three Adapter) + +## What changed + +This change adds a full boolean operation flow that can be executed from Rust, consumed from wasm/js bindings, and rendered through `@opengeometry/kernel-three`. + +### 1) Kernel boolean operation module +- Added `main/opengeometry/src/operations/boolean.rs`. +- Introduced wasm-exported `BooleanOperation` enum: + - `Union` + - `Intersection` + - `Difference` +- Introduced wasm-exported `OGBooleanResult` that: + - accepts two serialized BReps, + - computes the result, + - returns both serialized BRep and triangulated render geometry. + +### 2) Boolean algorithm and constraint model +To provide a boolean operation that works across all existing solids with the current project architecture, the implementation uses a **voxel-constrained CSG** pipeline: + +1. Convert each input BRep face-loop set to triangles. +2. Build a combined bounding box. +3. Sample voxel centers on a regular grid (`voxel_size` is the constraint parameter). +4. Classify sample points as inside/outside each shape using parity ray-casting against triangle soups. +5. Evaluate boolean logic (`union`, `intersection`, `difference`). +6. Reconstruct a watertight surface by emitting only boundary voxel faces. + +This is robust for heterogeneous shape inputs because it does not depend on exact face-face intersection topology repair in floating point. Instead, the user controls precision/performance through `voxel_size`. + +### 3) Public API wiring +- Registered the new module in `main/opengeometry/src/lib.rs` via `pub mod boolean;` in operations. + +### 4) Three.js adapter update +- Added `main/opengeometry-three/src/operations/boolean.ts` with: + - `BooleanMesh` class (extends `THREE.Mesh`), + - `compute(first, second, operation, options)` method, + - optional constraints (`voxelSize`, `color`, `opacity`), + - `getBrepData()` pass-through for chaining operations. +- Added operation exports in: + - `main/opengeometry-three/src/operations/index.ts` + - `main/opengeometry-three/index.ts` + +### 5) Working example +- Added new interactive example: + - Page: `main/opengeometry-three/examples-vite/src/pages/operations-boolean.ts` + - HTML entry: `main/opengeometry-three/examples-vite/operations/boolean.html` +- Example lets the user switch operation and voxel constraint interactively. + +### 6) Tests +Added kernel tests in `main/opengeometry/src/operations/boolean.rs`: +- union of overlapping cuboids returns non-empty geometry, +- difference of overlapping cuboids returns non-empty geometry. + +## Why this changed + +The project already has many shape generators that emit BReps but does not yet have a production path for generic shape boolean operations. This patch introduces a single, unified operation path that can accept any shape capable of producing BRep data and gives users a practical constraint knob (`voxel_size`) for balancing robustness and fidelity. + +## How to test locally + +### Kernel tests +```bash +cargo test --manifest-path main/opengeometry/Cargo.toml +``` + +### Three adapter lint smoke for the new file +```bash +npx eslint main/opengeometry-three/src/operations/boolean.ts +``` + +### Example build (optional) +```bash +npm run build-example-three +``` +Then open `main/opengeometry-three/examples-dist/operations/boolean.html`. + +## Backward compatibility + +- Existing shape APIs are unchanged. +- Existing render wrappers are unchanged. +- New functionality is additive. + +## Known caveats and follow-ups + +1. The current implementation is voxel-based (approximate), not exact analytic BRep-BRep splitting. +2. Output triangle density grows quickly as `voxel_size` decreases. +3. Future follow-up can add adaptive grids, cached spatial acceleration, and exact predicate/splitting stages for high-fidelity CAD workflows. diff --git a/main/opengeometry-three/examples-vite/operations/boolean.html b/main/opengeometry-three/examples-vite/operations/boolean.html new file mode 100644 index 0000000..869299b --- /dev/null +++ b/main/opengeometry-three/examples-vite/operations/boolean.html @@ -0,0 +1 @@ +
diff --git a/main/opengeometry-three/examples-vite/src/pages/operations-boolean.ts b/main/opengeometry-three/examples-vite/src/pages/operations-boolean.ts new file mode 100644 index 0000000..89cdab1 --- /dev/null +++ b/main/opengeometry-three/examples-vite/src/pages/operations-boolean.ts @@ -0,0 +1,64 @@ +import { BooleanMesh, Cuboid, Vector3 } from "@og-three"; +import * as THREE from "three"; +import { + bootstrapExample, + mountControls, + replaceSceneObject, +} from "../shared/runtime"; + +bootstrapExample({ + title: "Operation: Boolean", + description: + "Voxel-constraint boolean operation between two cuboids (union / intersection / difference).", + build: ({ scene }) => { + let current: THREE.Group | null = null; + + mountControls( + "Boolean Parameters", + [ + { + type: "select", + key: "operation", + label: "Operation", + value: "union", + options: [ + { label: "Union", value: "union" }, + { label: "Intersection", value: "intersection" }, + { label: "Difference (A - B)", value: "difference" }, + ], + }, + { type: "number", key: "offsetX", label: "B Offset X", min: -1.25, max: 1.25, step: 0.05, value: 0.45 }, + { type: "number", key: "voxelSize", label: "Voxel Size", min: 0.08, max: 0.4, step: 0.02, value: 0.15 }, + ], + (state) => { + const a = new Cuboid({ + center: new Vector3(-0.2, 0, 0), + width: 1.4, + height: 1.2, + depth: 1.1, + color: 0x1d4ed8, + }); + + const b = new Cuboid({ + center: new Vector3(state.offsetX as number, 0, 0), + width: 1.2, + height: 1.2, + depth: 1.3, + color: 0xdc2626, + }); + + const result = new BooleanMesh(); + result.compute(a, b, state.operation as "union" | "intersection" | "difference", { + voxelSize: state.voxelSize as number, + color: 0x16a34a, + opacity: 0.8, + }); + + const group = new THREE.Group(); + group.add(result); + + current = replaceSceneObject(scene, current, group); + } + ); + }, +}); diff --git a/main/opengeometry-three/index.ts b/main/opengeometry-three/index.ts index 01a6e71..cac489f 100644 --- a/main/opengeometry-three/index.ts +++ b/main/opengeometry-three/index.ts @@ -666,3 +666,8 @@ export * from './src/shapes/'; * Reusable example builders for quickly wiring demo scenes. */ export * from './src/examples/'; + +/** + * Boolean/constructive solid operations wrappers. + */ +export * from './src/operations/'; diff --git a/main/opengeometry-three/src/operations/boolean.ts b/main/opengeometry-three/src/operations/boolean.ts new file mode 100644 index 0000000..7e8ea0c --- /dev/null +++ b/main/opengeometry-three/src/operations/boolean.ts @@ -0,0 +1,72 @@ +import { BooleanOperation, OGBooleanResult } from "../../../opengeometry/pkg/opengeometry"; +import * as THREE from "three"; + +export type BooleanInputShape = { + getBrepData: () => unknown; +}; + +export type BooleanKind = "union" | "intersection" | "difference"; + +export interface BooleanOptions { + voxelSize?: number; + color?: number; + opacity?: number; +} + +function toKernelOperation(operation: BooleanKind): BooleanOperation { + switch (operation) { + case "union": + return BooleanOperation.Union; + case "intersection": + return BooleanOperation.Intersection; + case "difference": + return BooleanOperation.Difference; + } +} + +export class BooleanMesh extends THREE.Mesh { + private readonly solver = new OGBooleanResult(); + + constructor() { + super(new THREE.BufferGeometry(), new THREE.MeshStandardMaterial({ color: 0x33aa33, transparent: true, opacity: 0.8 })); + } + + compute( + first: BooleanInputShape, + second: BooleanInputShape, + operation: BooleanKind, + options?: BooleanOptions, + ) { + const voxelSize = options?.voxelSize ?? 0.15; + + this.solver.compute_from_brep_serialized( + JSON.stringify(first.getBrepData()), + JSON.stringify(second.getBrepData()), + toKernelOperation(operation), + voxelSize, + ); + + const geometry = new THREE.BufferGeometry(); + const geometryData = JSON.parse(this.solver.get_geometry_serialized()); + geometry.setAttribute("position", new THREE.Float32BufferAttribute(geometryData, 3)); + geometry.computeVertexNormals(); + + if (this.geometry) { + this.geometry.dispose(); + } + + this.geometry = geometry; + + const color = options?.color ?? 0x33aa33; + const opacity = options?.opacity ?? 0.8; + this.material = new THREE.MeshStandardMaterial({ + color, + transparent: opacity < 1, + opacity, + }); + } + + getBrepData() { + return JSON.parse(this.solver.get_brep_serialized()); + } +} diff --git a/main/opengeometry-three/src/operations/index.ts b/main/opengeometry-three/src/operations/index.ts new file mode 100644 index 0000000..01af61b --- /dev/null +++ b/main/opengeometry-three/src/operations/index.ts @@ -0,0 +1 @@ +export * from './boolean'; diff --git a/main/opengeometry/src/lib.rs b/main/opengeometry/src/lib.rs index d33ebb4..e08d2cf 100644 --- a/main/opengeometry/src/lib.rs +++ b/main/opengeometry/src/lib.rs @@ -4,6 +4,7 @@ pub mod geometry { } pub mod operations { + pub mod boolean; pub mod extrude; pub mod offset; pub mod sweep; diff --git a/main/opengeometry/src/operations/boolean.rs b/main/opengeometry/src/operations/boolean.rs new file mode 100644 index 0000000..9a4f0c3 --- /dev/null +++ b/main/opengeometry/src/operations/boolean.rs @@ -0,0 +1,453 @@ +use std::collections::HashSet; + +use openmaths::Vector3; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; +use wasm_bindgen::prelude::*; + +use crate::brep::{Brep, Edge, Face, Vertex}; +use crate::operations::triangulate::triangulate_polygon_with_holes; + +const EPSILON: f64 = 1.0e-9; + +#[wasm_bindgen] +#[derive(Clone, Copy, Serialize, Deserialize)] +pub enum BooleanOperation { + Union, + Intersection, + Difference, +} + +#[wasm_bindgen] +#[derive(Clone, Serialize, Deserialize)] +pub struct OGBooleanResult { + brep: Brep, +} + +#[wasm_bindgen] +impl OGBooleanResult { + #[wasm_bindgen(constructor)] + pub fn new() -> Self { + Self { + brep: Brep::new(Uuid::new_v4()), + } + } + + /// Runs a voxelized boolean operation from two serialized BReps. + /// + /// `voxel_size` acts as the robustness/performance constraint: + /// - larger values are faster but coarser + /// - smaller values are slower but more accurate + #[wasm_bindgen] + pub fn compute_from_brep_serialized( + &mut self, + a_brep_serialized: String, + b_brep_serialized: String, + operation: BooleanOperation, + voxel_size: f64, + ) -> Result<(), JsValue> { + let brep_a: Brep = serde_json::from_str(&a_brep_serialized) + .map_err(|err| JsValue::from_str(&format!("Invalid first BRep payload: {err}")))?; + let brep_b: Brep = serde_json::from_str(&b_brep_serialized) + .map_err(|err| JsValue::from_str(&format!("Invalid second BRep payload: {err}")))?; + + if voxel_size <= EPSILON || !voxel_size.is_finite() { + return Err(JsValue::from_str( + "voxel_size must be a positive finite number", + )); + } + + self.brep = voxel_boolean(&brep_a, &brep_b, operation, voxel_size); + Ok(()) + } + + #[wasm_bindgen] + pub fn get_brep_serialized(&self) -> String { + serde_json::to_string(&self.brep).unwrap() + } + + #[wasm_bindgen] + pub fn get_geometry_serialized(&self) -> String { + let mut vertex_buffer: Vec