Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 82 additions & 0 deletions AI-DOCs/2026-03-04-boolean-operations-implementation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# 2026-03-04 Boolean Operations Implementation

## What changed

- Updated the kernel boolean module to support **BRep-like outlines** for boolean outputs rather than raw triangulation edges.
- `OGBoolean` remains stateful (`last_result`) and now computes outlines by edge-adjacency analysis:
- build undirected edge buckets from result polygons
- suppress edges shared by coplanar faces (triangle-split seams)
- apply a feature-angle filter so smooth tessellation edges are hidden
- keep boundary edges and sharp-feature edges
- `OGBoolean::get_outline_geometry_serialized` now returns this BRep-like feature outline buffer.
- Added kernel test `coplanar_shared_triangle_edge_is_removed_from_outline` to verify internal triangulation diagonals are not emitted.
- Updated Three.js boolean binding API:
- Introduced `BooleanOperationKind` and `parseBooleanOperation` for deterministic operation selection.
- `BooleanShape` uses kernel outline output as before, but operation parsing now avoids ambiguous string casting in examples.
- Updated boolean example (`operations-boolean.ts`):
- added **Show Outline** toggle
- kept left/right shape selectors (Cuboid, Sphere, Cylinder, Wedge)
- operation dropdown now resolves via `parseBooleanOperation` for consistent behavior

## Why it changed

Follow-up requirements requested:

1. BRep-like outlines instead of triangulated outlines
2. consistency in operation selection behavior
3. an explicit outline toggle in the example

These are now addressed directly in kernel + wrapper + example layers.

## Robustness strategy

Boolean CSG core still uses robust controls:

- epsilon-based plane classification (`front`/`back`/`coplanar`/`spanning`)
- snap-grid normalization for deterministic splits
- post-op weld by epsilon snapping

Outline extraction now adds topology-aware edge filtering to remove coplanar split seams and smooth-surface tessellation artifacts.

## How to test locally

1. Kernel checks:

```bash
cargo fmt --manifest-path main/opengeometry/Cargo.toml
cargo test --manifest-path main/opengeometry/Cargo.toml
```

2. TS lint:

```bash
npm run lint:check
```

3. Build wasm + three package:

```bash
npm run build-core
npm run build-three
```

4. Example run:

```bash
npm --prefix main/opengeometry-three run dev-example-three
```

Then open:

- `main/opengeometry-three/examples-vite/operations/boolean.html`

## Backward compatibility

- Existing non-boolean wrappers remain unchanged.
- Boolean API stays additive; operation constants are still `BooleanOperation.Union|Intersection|Difference`.
- New parser helper improves call-site safety without breaking existing valid operation strings.

## Known caveats and follow-ups

- In this environment, wasm package artifacts (`main/opengeometry/pkg/opengeometry`) are unavailable, so full TS build/example rendering remains blocked.
- Existing repo-level lint errors outside this change still fail `npm run lint:check`.
10 changes: 9 additions & 1 deletion main/opengeometry-three/examples-vite/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ <h2 class="og-specs-section-title">Shapes</h2>
<section class="og-specs-section">
<div class="og-specs-section-head">
<h2 class="og-specs-section-title">Operations</h2>
<p class="og-specs-count">3 items</p>
<p class="og-specs-count">4 items</p>
</div>
<div class="og-specs-grid">
<a class="og-spec-card" href="./operations/offset.html">
Expand All @@ -153,6 +153,14 @@ <h2 class="og-specs-section-title">Operations</h2>
<div class="og-spec-meta-row"><dt>Control</dt><dd>Thickness</dd></div>
</dl>
</a>
<a class="og-spec-card" href="./operations/boolean.html">
<div class="og-spec-card-head"><h3 class="og-spec-card-title">Boolean</h3><p class="og-spec-badge">new</p></div>
<p class="og-spec-desc">Union / intersection / difference with numeric robustness controls.</p>
<dl class="og-spec-meta">
<div class="og-spec-meta-row"><dt>Domain</dt><dd>Kernel Ops</dd></div>
<div class="og-spec-meta-row"><dt>Control</dt><dd>Op + Epsilon</dd></div>
</dl>
</a>
<a class="og-spec-card" href="./operations/sweep-path-profile.html">
<div class="og-spec-card-head"><h3 class="og-spec-card-title">Sweep Path + Profile</h3><p class="og-spec-badge">ready</p></div>
<p class="og-spec-desc">Operation-level sweep from path primitive + profile primitive.</p>
Expand Down
12 changes: 12 additions & 0 deletions main/opengeometry-three/examples-vite/operations/boolean.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>OpenGeometry • Boolean Operation</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="../src/pages/operations-boolean.ts"></script>
</body>
</html>
175 changes: 175 additions & 0 deletions main/opengeometry-three/examples-vite/src/pages/operations-boolean.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
import * as THREE from "three";
import { Vector3 } from "../../../../opengeometry/pkg/opengeometry";
import {
BooleanOperation,
BooleanShape,
Cuboid,
Cylinder,
parseBooleanOperation,
Sphere,
Wedge,
} from "@og-three";
import {
bootstrapExample,
mountControls,
replaceSceneObject,
} from "../shared/runtime";

function buildOperand(kind: string, center: Vector3, color: number): THREE.Mesh {
switch (kind) {
case "sphere": {
const sphere = new Sphere({
center,
radius: 0.9,
widthSegments: 30,
heightSegments: 20,
color,
});
sphere.outline = true;
return sphere;
}
case "cylinder": {
const cylinder = new Cylinder({
center,
radius: 0.7,
height: 1.9,
segments: 32,
angle: Math.PI * 2,
color,
});
cylinder.outline = true;
return cylinder;
}
case "wedge": {
const wedge = new Wedge({
center,
width: 1.6,
height: 1.8,
depth: 1.4,
color,
});
wedge.outline = true;
return wedge;
}
default: {
const cuboid = new Cuboid({
center,
width: 1.6,
height: 1.8,
depth: 1.5,
color,
});
cuboid.outline = true;
return cuboid;
}
}
}

void bootstrapExample({
title: "Boolean (Union / Intersection / Difference)",
description:
"Boolean operations over Cuboid, Sphere, Cylinder, and Wedge operands with kernel-generated outlines.",
build: ({ scene }) => {
let result: THREE.Mesh | null = null;

const update = (state: Record<string, number | boolean | string>) => {
const left = buildOperand(
String(state.leftShape),
new Vector3(-0.55, 0.9, 0),
0x10b981
);
const right = buildOperand(
String(state.rightShape),
new Vector3(0.55, 0.9, 0),
0xf97316
);

const operation = parseBooleanOperation(String(state.operation));

const boolean = new BooleanShape(left, right, operation, {
epsilon: state.epsilon as number,
snap: state.snap as number,
});
boolean.outline = Boolean(state.showOutline);

boolean.material = new THREE.MeshStandardMaterial({
color: 0x2563eb,
transparent: true,
opacity: 0.72,
});

scene.add(left);
scene.add(right);
result = replaceSceneObject(scene, result, boolean);

left.removeFromParent();
right.removeFromParent();
};

mountControls(
"Boolean Controls",
[
{
type: "select",
key: "operation",
label: "Operation",
value: BooleanOperation.Union,
options: [
{ label: "Union", value: BooleanOperation.Union },
{ label: "Intersection", value: BooleanOperation.Intersection },
{ label: "Difference", value: BooleanOperation.Difference },
],
},
{
type: "select",
key: "leftShape",
label: "Left Shape",
value: "cuboid",
options: [
{ label: "Cuboid", value: "cuboid" },
{ label: "Sphere", value: "sphere" },
{ label: "Cylinder", value: "cylinder" },
{ label: "Wedge", value: "wedge" },
],
},
{
type: "select",
key: "rightShape",
label: "Right Shape",
value: "sphere",
options: [
{ label: "Cuboid", value: "cuboid" },
{ label: "Sphere", value: "sphere" },
{ label: "Cylinder", value: "cylinder" },
{ label: "Wedge", value: "wedge" },
],
},
{
type: "boolean",
key: "showOutline",
label: "Show Outline",
value: true,
},
{
type: "number",
key: "epsilon",
label: "Plane Epsilon",
min: 0.000001,
max: 0.005,
step: 0.000001,
value: 0.00001,
},
{
type: "number",
key: "snap",
label: "Snap Grid",
min: 0.000001,
max: 0.005,
step: 0.000001,
value: 0.00001,
},
],
update
);
},
});
30 changes: 28 additions & 2 deletions main/opengeometry-three/examples-vite/src/shared/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,16 @@ export type ExampleControlDefinition =
key: string;
label: string;
value: boolean;
}
| {
type: "select";
key: string;
label: string;
value: string;
options: Array<{ label: string; value: string }>;
};

export type ExampleControlState = Record<string, number | boolean>;
export type ExampleControlState = Record<string, number | boolean | string>;

interface BootstrapConfig {
title: string;
Expand Down Expand Up @@ -242,7 +249,7 @@ export function mountControls(
inputs.appendChild(rangeInput);
inputs.appendChild(numberInput);
row.appendChild(inputs);
} else {
} else if (definition.type === "boolean") {
const boolWrap = document.createElement("div");
boolWrap.className = "og-control-bool";

Expand All @@ -267,6 +274,25 @@ export function mountControls(
boolWrap.appendChild(boolLabel);
row.appendChild(header);
row.appendChild(boolWrap);
} else {
row.appendChild(header);
const select = document.createElement("select");
select.className = "og-control-select";

for (const option of definition.options) {
const optionElement = document.createElement("option");
optionElement.value = option.value;
optionElement.textContent = option.label;
optionElement.selected = option.value === definition.value;
select.appendChild(optionElement);
}

select.addEventListener("change", () => {
state[definition.key] = select.value;
emitChange();
});

row.appendChild(select);
}

panel.appendChild(row);
Expand Down
5 changes: 5 additions & 0 deletions main/opengeometry-three/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -666,3 +666,8 @@ export * from './src/shapes/';
* Reusable example builders for quickly wiring demo scenes.
*/
export * from './src/examples/';

/**
* Boolean/constructive solid geometry operations.
*/
export * from './src/operations/';
Loading
Loading