From 4167c3282f7cdeeaa6583d5485adb8c1d4c14a59 Mon Sep 17 00:00:00 2001 From: blackboots <47943405+aka-blackboots@users.noreply.github.com> Date: Sun, 1 Mar 2026 12:26:58 +0100 Subject: [PATCH 1/2] Add wedge HTML example in opengeometry-three --- .../2026-03-01-wedge-shape-support.md | 35 ++++ main/opengeometry-three/examples/wedge.html | 101 ++++++++++ main/opengeometry-three/src/shapes/index.ts | 3 +- main/opengeometry-three/src/shapes/wedge.ts | 137 +++++++++++++ .../examples/pdf_primitives_all.rs | 9 + main/opengeometry/src/lib.rs | 1 + main/opengeometry/src/primitives/wedge.rs | 180 ++++++++++++++++++ main/opengeometry/src/scenegraph.rs | 64 +++++-- 8 files changed, 516 insertions(+), 14 deletions(-) create mode 100644 AI-DOCs/opengeometry/2026-03-01-wedge-shape-support.md create mode 100644 main/opengeometry-three/examples/wedge.html create mode 100644 main/opengeometry-three/src/shapes/wedge.ts create mode 100644 main/opengeometry/src/primitives/wedge.rs diff --git a/AI-DOCs/opengeometry/2026-03-01-wedge-shape-support.md b/AI-DOCs/opengeometry/2026-03-01-wedge-shape-support.md new file mode 100644 index 0000000..e341778 --- /dev/null +++ b/AI-DOCs/opengeometry/2026-03-01-wedge-shape-support.md @@ -0,0 +1,35 @@ +# Wedge shape support in kernel and three integration + +## What changed +- Added a new kernel primitive `OGWedge` with configurable `center`, `width`, `height`, and `depth`. +- Registered wedge in the kernel exports and scene manager APIs (`addWedgeToScene` and `addWedgeToCurrentScene`). +- Extended the PDF primitives example to generate a dedicated wedge projection PDF. +- Added a new `Wedge` shape wrapper in `opengeometry-three` and exported it in the shapes index. +- Added a browser example page at `main/opengeometry-three/examples/wedge.html` to visualize wedge geometry in Three.js. + +## Why it changed +The kernel already supports common primitive and solid shapes (e.g., cuboid and cylinder). Wedge support was added to align with this existing shape model and make wedge available both in kernel usage and the three.js integration package. + +## How to test locally +1. Kernel checks: + - `cd main/opengeometry && cargo fmt -- --check` + - `cd main/opengeometry && cargo check` + - `cd main/opengeometry && cargo test` +2. Build wasm bindings used by three package: + - `npm run build-core` +3. Three package type/build checks: + - `npm run build-three` +4. Kernel example output: + - `cd main/opengeometry && cargo run --example pdf_primitives_all -- wedge_demo` + - Confirm `wedge_demo_wedge.pdf` is generated. +5. Browser example: + - `python3 -m http.server 8080` + - Open `http://localhost:8080/main/opengeometry-three/examples/wedge.html` + +## Backward-compatibility notes +- No existing primitive behavior or API signatures were modified. +- Changes are additive only: a new primitive and new scene manager methods. + +## Known caveats and follow-ups +- `opengeometry-three` consumes wasm symbols from `main/opengeometry/pkg/opengeometry`; ensure `npm run build-core` is run before consuming wedge from JS/TS. +- If package-level docs enumerate available shapes, add wedge there in a follow-up. diff --git a/main/opengeometry-three/examples/wedge.html b/main/opengeometry-three/examples/wedge.html new file mode 100644 index 0000000..c2be377 --- /dev/null +++ b/main/opengeometry-three/examples/wedge.html @@ -0,0 +1,101 @@ + + + + + + OpenGeometry Three - Wedge Example + + + +
+
OpenGeometry Wedge in Three.js
+ + + + diff --git a/main/opengeometry-three/src/shapes/index.ts b/main/opengeometry-three/src/shapes/index.ts index 36df28f..4b5c6f9 100644 --- a/main/opengeometry-three/src/shapes/index.ts +++ b/main/opengeometry-three/src/shapes/index.ts @@ -1,4 +1,5 @@ export * from './polygon'; export * from './cylinder'; export * from './cuboid'; -export * from './opening'; \ No newline at end of file +export * from './opening'; +export * from './wedge'; diff --git a/main/opengeometry-three/src/shapes/wedge.ts b/main/opengeometry-three/src/shapes/wedge.ts new file mode 100644 index 0000000..d08395b --- /dev/null +++ b/main/opengeometry-three/src/shapes/wedge.ts @@ -0,0 +1,137 @@ +import { OGWedge, Vector3 } from "../../../opengeometry/pkg/opengeometry"; +import * as THREE from "three"; +import { getUUID } from "../utils/randomizer"; + +export interface IWedgeOptions { + ogid?: string; + center: Vector3; + width: number; + height: number; + depth: number; + color: number; +} + +export class Wedge extends THREE.Mesh { + ogid: string; + options: IWedgeOptions = { + center: new Vector3(0, 0, 0), + width: 1, + height: 1, + depth: 1, + color: 0x00ff00, + }; + + private wedge: OGWedge; + #outlineMesh: THREE.Line | null = null; + + set color(color: number) { + this.options.color = color; + if (this.material instanceof THREE.MeshStandardMaterial) { + this.material.color.set(color); + } + } + + constructor(options?: IWedgeOptions) { + super(); + this.ogid = options?.ogid ?? getUUID(); + this.wedge = new OGWedge(this.ogid); + + this.options = { ...this.options, ...options }; + this.options.ogid = this.ogid; + + this.setConfig(this.options); + } + + validateOptions() { + if (!this.options) { + throw new Error("Options are not defined for Wedge"); + } + } + + setConfig(options: IWedgeOptions) { + this.validateOptions(); + + const { width, height, depth, center, color } = options; + this.wedge.set_config(center.clone(), width, height, depth); + this.options.color = color; + + this.generateGeometry(); + } + + cleanGeometry() { + this.geometry.dispose(); + if (Array.isArray(this.material)) { + this.material.forEach((mat) => mat.dispose()); + } else { + this.material.dispose(); + } + } + + generateGeometry() { + this.cleanGeometry(); + + const geometryData = this.wedge.get_geometry_serialized(); + const bufferData = JSON.parse(geometryData); + + const geometry = new THREE.BufferGeometry(); + geometry.setAttribute( + "position", + new THREE.Float32BufferAttribute(bufferData, 3) + ); + + const material = new THREE.MeshStandardMaterial({ + color: this.options.color, + transparent: true, + opacity: 0.6, + }); + + geometry.computeVertexNormals(); + geometry.computeBoundingBox(); + + this.geometry = geometry; + this.material = material; + + if (this.#outlineMesh) { + this.outline = true; + } + } + + getBrepData() { + const brepData = this.wedge.get_brep_serialized(); + if (!brepData) { + throw new Error("Brep data is not available for this wedge."); + } + return JSON.parse(brepData); + } + + set outline(enable: boolean) { + if (this.#outlineMesh) { + this.remove(this.#outlineMesh); + this.#outlineMesh.geometry.dispose(); + this.#outlineMesh = null; + } + + if (enable) { + const outlineBuffer = this.wedge.get_outline_geometry_serialized(); + const outlineData = JSON.parse(outlineBuffer); + + const outlineGeometry = new THREE.BufferGeometry(); + outlineGeometry.setAttribute( + "position", + new THREE.Float32BufferAttribute(outlineData, 3) + ); + + const outlineMaterial = new THREE.LineBasicMaterial({ color: 0x000000 }); + this.#outlineMesh = new THREE.LineSegments(outlineGeometry, outlineMaterial); + this.add(this.#outlineMesh); + } + } + + get outlineMesh() { + return this.#outlineMesh; + } + + discardGeometry() { + this.geometry.dispose(); + } +} diff --git a/main/opengeometry/examples/pdf_primitives_all.rs b/main/opengeometry/examples/pdf_primitives_all.rs index 88b6b83..7fe44ef 100644 --- a/main/opengeometry/examples/pdf_primitives_all.rs +++ b/main/opengeometry/examples/pdf_primitives_all.rs @@ -9,6 +9,7 @@ use opengeometry::primitives::line::OGLine; use opengeometry::primitives::polygon::OGPolygon; use opengeometry::primitives::polyline::OGPolyline; use opengeometry::primitives::rectangle::OGRectangle; +use opengeometry::primitives::wedge::OGWedge; use openmaths::Vector3; fn export_named_scene( @@ -89,6 +90,9 @@ fn main() -> Result<(), Box> { 40, ); + let mut wedge = OGWedge::new("wedge".to_string()); + wedge.set_config(Vector3::new(0.0, 0.0, 0.0), 2.4, 1.6, 1.2); + export_named_scene( &format!("{}_line.pdf", output_prefix), "OGLine Projection", @@ -124,6 +128,11 @@ fn main() -> Result<(), Box> { "OGCylinder Projection", &cylinder.to_projected_scene2d(&camera, &hlr), )?; + export_named_scene( + &format!("{}_wedge.pdf", output_prefix), + "OGWedge Projection", + &wedge.to_projected_scene2d(&camera, &hlr), + )?; Ok(()) } diff --git a/main/opengeometry/src/lib.rs b/main/opengeometry/src/lib.rs index 8dc7196..a1c9ad9 100644 --- a/main/opengeometry/src/lib.rs +++ b/main/opengeometry/src/lib.rs @@ -22,6 +22,7 @@ pub mod primitives { pub mod polygon; pub mod polyline; pub mod rectangle; + pub mod wedge; } pub mod brep; diff --git a/main/opengeometry/src/primitives/wedge.rs b/main/opengeometry/src/primitives/wedge.rs new file mode 100644 index 0000000..9978aa8 --- /dev/null +++ b/main/opengeometry/src/primitives/wedge.rs @@ -0,0 +1,180 @@ +use serde::{Deserialize, Serialize}; +use wasm_bindgen::prelude::*; + +use crate::brep::{Brep, Edge, Face, Vertex}; +use crate::export::projection::{project_brep_to_scene, CameraParameters, HlrOptions, Scene2D}; +use crate::operations::triangulate::triangulate_polygon_with_holes; +use openmaths::Vector3; +use uuid::Uuid; + +#[wasm_bindgen] +#[derive(Clone, Serialize, Deserialize)] +pub struct OGWedge { + id: String, + center: Vector3, + width: f64, + height: f64, + depth: f64, + brep: Brep, +} + +#[wasm_bindgen] +impl OGWedge { + #[wasm_bindgen(setter)] + pub fn set_id(&mut self, id: String) { + self.id = id; + } + + #[wasm_bindgen(getter)] + pub fn id(&self) -> String { + self.id.clone() + } + + #[wasm_bindgen(constructor)] + pub fn new(id: String) -> OGWedge { + let internal_id = Uuid::new_v4(); + + OGWedge { + id, + center: Vector3::new(0.0, 0.0, 0.0), + width: 1.0, + height: 1.0, + depth: 1.0, + brep: Brep::new(internal_id), + } + } + + #[wasm_bindgen] + pub fn set_config(&mut self, center: Vector3, width: f64, height: f64, depth: f64) { + self.center = center; + self.width = width; + self.height = height; + self.depth = depth; + + self.generate_brep(); + } + + pub fn generate_brep(&mut self) { + self.clean_geometry(); + self.generate_geometry(); + } + + pub fn clean_geometry(&mut self) { + self.brep.clear(); + } + + #[wasm_bindgen] + pub fn generate_geometry(&mut self) { + let half_width = self.width / 2.0; + let half_height = self.height / 2.0; + let half_depth = self.depth / 2.0; + + let x_min = self.center.x - half_width; + let x_max = self.center.x + half_width; + let y_min = self.center.y - half_height; + let y_max = self.center.y + half_height; + let z_min = self.center.z - half_depth; + let z_max = self.center.z + half_depth; + + self.brep + .vertices + .push(Vertex::new(0, Vector3::new(x_min, y_min, z_min))); + self.brep + .vertices + .push(Vertex::new(1, Vector3::new(x_max, y_min, z_min))); + self.brep + .vertices + .push(Vertex::new(2, Vector3::new(x_min, y_max, z_min))); + self.brep + .vertices + .push(Vertex::new(3, Vector3::new(x_min, y_min, z_max))); + self.brep + .vertices + .push(Vertex::new(4, Vector3::new(x_max, y_min, z_max))); + self.brep + .vertices + .push(Vertex::new(5, Vector3::new(x_min, y_max, z_max))); + + self.brep.edges.push(Edge::new(0, 0, 1)); + self.brep.edges.push(Edge::new(1, 1, 4)); + self.brep.edges.push(Edge::new(2, 4, 3)); + self.brep.edges.push(Edge::new(3, 3, 0)); + self.brep.edges.push(Edge::new(4, 0, 2)); + self.brep.edges.push(Edge::new(5, 2, 5)); + self.brep.edges.push(Edge::new(6, 5, 3)); + self.brep.edges.push(Edge::new(7, 1, 2)); + self.brep.edges.push(Edge::new(8, 4, 5)); + + self.brep.faces.push(Face::new(0, vec![0, 1, 4, 3])); + self.brep.faces.push(Face::new(1, vec![0, 2, 1])); + self.brep.faces.push(Face::new(2, vec![3, 4, 5])); + self.brep.faces.push(Face::new(3, vec![0, 3, 5, 2])); + self.brep.faces.push(Face::new(4, vec![1, 2, 5, 4])); + } + + #[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 = Vec::new(); + let faces = self.brep.faces.clone(); + + for face in &faces { + let (face_vertices, holes_vertices) = + self.brep.get_vertices_and_holes_by_face_id(face.id); + + if face_vertices.len() < 3 { + continue; + } + + let triangles = triangulate_polygon_with_holes(&face_vertices, &holes_vertices); + let all_vertices: Vec = face_vertices + .into_iter() + .chain(holes_vertices.into_iter().flatten()) + .collect(); + + for triangle in triangles { + for vertex_index in triangle { + let vertex = &all_vertices[vertex_index]; + vertex_buffer.push(vertex.x); + vertex_buffer.push(vertex.y); + vertex_buffer.push(vertex.z); + } + } + } + + serde_json::to_string(&vertex_buffer).unwrap() + } + + #[wasm_bindgen] + pub fn get_outline_geometry_serialized(&self) -> String { + let mut vertex_buffer: Vec = Vec::new(); + + for edge in self.brep.edges.clone() { + let start_vertex = self.brep.vertices[edge.v1 as usize].clone(); + let end_vertex = self.brep.vertices[edge.v2 as usize].clone(); + + vertex_buffer.push(start_vertex.position.x); + vertex_buffer.push(start_vertex.position.y); + vertex_buffer.push(start_vertex.position.z); + vertex_buffer.push(end_vertex.position.x); + vertex_buffer.push(end_vertex.position.y); + vertex_buffer.push(end_vertex.position.z); + } + + serde_json::to_string(&vertex_buffer).unwrap() + } +} + +impl OGWedge { + pub fn brep(&self) -> &Brep { + &self.brep + } + + pub fn to_projected_scene2d(&self, camera: &CameraParameters, hlr: &HlrOptions) -> Scene2D { + project_brep_to_scene(&self.brep, camera, hlr) + } +} diff --git a/main/opengeometry/src/scenegraph.rs b/main/opengeometry/src/scenegraph.rs index 4db21ab..52ca72a 100644 --- a/main/opengeometry/src/scenegraph.rs +++ b/main/opengeometry/src/scenegraph.rs @@ -15,6 +15,7 @@ use crate::primitives::line::OGLine; use crate::primitives::polygon::OGPolygon; use crate::primitives::polyline::OGPolyline; use crate::primitives::rectangle::OGRectangle; +use crate::primitives::wedge::OGWedge; #[cfg(not(target_arch = "wasm32"))] use crate::export::pdf::{export_scene_to_pdf_with_config, PdfExportConfig}; @@ -162,12 +163,7 @@ impl OGSceneManager { kind: impl Into, brep: &Brep, ) -> Result<(), String> { - self.upsert_entity_brep( - scene_id, - entity_id.into(), - kind.into(), - brep.clone(), - ) + self.upsert_entity_brep(scene_id, entity_id.into(), kind.into(), brep.clone()) } pub fn add_line_to_scene_internal( @@ -233,6 +229,15 @@ impl OGSceneManager { self.add_brep_entity_to_scene_internal(scene_id, entity_id, "OGCylinder", cylinder.brep()) } + pub fn add_wedge_to_scene_internal( + &mut self, + scene_id: &str, + entity_id: impl Into, + wedge: &OGWedge, + ) -> Result<(), String> { + self.add_brep_entity_to_scene_internal(scene_id, entity_id, "OGWedge", wedge.brep()) + } + pub fn project_scene_to_2d( &self, scene_id: &str, @@ -253,7 +258,8 @@ impl OGSceneManager { config: &PdfExportConfig, ) -> Result<(), String> { let projected = self.project_scene_to_2d(scene_id, camera, hlr)?; - export_scene_to_pdf_with_config(&projected, file_path, config).map_err(|err| err.to_string()) + export_scene_to_pdf_with_config(&projected, file_path, config) + .map_err(|err| err.to_string()) } pub fn project_scene_to_2d_json( @@ -576,6 +582,29 @@ impl OGSceneManager { self.add_cylinder_to_scene(scene_id, entity_id, cylinder) } + #[wasm_bindgen(js_name = addWedgeToScene)] + pub fn add_wedge_to_scene( + &mut self, + scene_id: String, + entity_id: String, + wedge: &OGWedge, + ) -> Result<(), JsValue> { + self.add_wedge_to_scene_internal(&scene_id, entity_id, wedge) + .map_err(|err| JsValue::from_str(&err)) + } + + #[wasm_bindgen(js_name = addWedgeToCurrentScene)] + pub fn add_wedge_to_current_scene( + &mut self, + entity_id: String, + wedge: &OGWedge, + ) -> Result<(), JsValue> { + let scene_id = self + .scene_id_or_current(None) + .map_err(|err| JsValue::from_str(&err))?; + self.add_wedge_to_scene(scene_id, entity_id, wedge) + } + #[wasm_bindgen(js_name = projectTo2DCamera)] pub fn project_to_2d_camera( &self, @@ -583,7 +612,8 @@ impl OGSceneManager { camera_json: String, hlr_json: Option, ) -> Result { - let camera = Self::parse_camera_json(&camera_json).map_err(|err| JsValue::from_str(&err))?; + let camera = + Self::parse_camera_json(&camera_json).map_err(|err| JsValue::from_str(&err))?; let hlr = Self::parse_hlr_json(hlr_json).map_err(|err| JsValue::from_str(&err))?; self.project_scene_to_2d_json(&scene_id, &camera, &hlr) .map_err(|err| JsValue::from_str(&err)) @@ -596,7 +626,8 @@ impl OGSceneManager { camera_json: String, hlr_json: Option, ) -> Result { - let camera = Self::parse_camera_json(&camera_json).map_err(|err| JsValue::from_str(&err))?; + let camera = + Self::parse_camera_json(&camera_json).map_err(|err| JsValue::from_str(&err))?; let hlr = Self::parse_hlr_json(hlr_json).map_err(|err| JsValue::from_str(&err))?; self.project_scene_to_2d_json_pretty(&scene_id, &camera, &hlr) .map_err(|err| JsValue::from_str(&err)) @@ -609,7 +640,8 @@ impl OGSceneManager { camera_json: String, hlr_json: Option, ) -> Result { - let camera = Self::parse_camera_json(&camera_json).map_err(|err| JsValue::from_str(&err))?; + let camera = + Self::parse_camera_json(&camera_json).map_err(|err| JsValue::from_str(&err))?; let hlr = Self::parse_hlr_json(hlr_json).map_err(|err| JsValue::from_str(&err))?; self.project_scene_to_2d_lines_json(&scene_id, &camera, &hlr) .map_err(|err| JsValue::from_str(&err)) @@ -622,7 +654,8 @@ impl OGSceneManager { camera_json: String, hlr_json: Option, ) -> Result { - let camera = Self::parse_camera_json(&camera_json).map_err(|err| JsValue::from_str(&err))?; + let camera = + Self::parse_camera_json(&camera_json).map_err(|err| JsValue::from_str(&err))?; let hlr = Self::parse_hlr_json(hlr_json).map_err(|err| JsValue::from_str(&err))?; self.project_scene_to_2d_lines_json_pretty(&scene_id, &camera, &hlr) .map_err(|err| JsValue::from_str(&err)) @@ -661,7 +694,8 @@ impl OGSceneManager { hlr_json: Option, file_path: String, ) -> Result<(), JsValue> { - let camera = Self::parse_camera_json(&camera_json).map_err(|err| JsValue::from_str(&err))?; + let camera = + Self::parse_camera_json(&camera_json).map_err(|err| JsValue::from_str(&err))?; let hlr = Self::parse_hlr_json(hlr_json).map_err(|err| JsValue::from_str(&err))?; self.project_scene_to_pdf_with_camera( &scene_id, @@ -697,7 +731,11 @@ mod tests { .unwrap(); let scene_2d = manager - .project_scene_to_2d(&scene_id, &CameraParameters::default(), &HlrOptions::default()) + .project_scene_to_2d( + &scene_id, + &CameraParameters::default(), + &HlrOptions::default(), + ) .unwrap(); assert!(!scene_2d.is_empty()); From 3736e6c49f2511dd270615e33ac50a5caf9a5e5a Mon Sep 17 00:00:00 2001 From: blackboots <47943405+aka-blackboots@users.noreply.github.com> Date: Sun, 1 Mar 2026 13:28:39 +0100 Subject: [PATCH 2/2] add example --- main/opengeometry-three/examples/wedge.html | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/main/opengeometry-three/examples/wedge.html b/main/opengeometry-three/examples/wedge.html index c2be377..9c8e794 100644 --- a/main/opengeometry-three/examples/wedge.html +++ b/main/opengeometry-three/examples/wedge.html @@ -32,6 +32,15 @@
OpenGeometry Wedge in Three.js
+ +