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..406878d --- /dev/null +++ b/AI-DOCs/opengeometry/2026-03-01-wedge-shape-support.md @@ -0,0 +1,31 @@ +# 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. + +## 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. Example output: + - `cd main/opengeometry && cargo run --example pdf_primitives_all -- wedge_demo` + - Confirm `wedge_demo_wedge.pdf` is generated. + +## 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/src/shapes/index.ts b/main/opengeometry-three/src/shapes/index.ts index b174bb4..2858c69 100644 --- a/main/opengeometry-three/src/shapes/index.ts +++ b/main/opengeometry-three/src/shapes/index.ts @@ -2,4 +2,5 @@ export * from './polygon'; export * from './cylinder'; export * from './cuboid'; export * from './opening'; +export * from './wedge'; export * from './sweep'; 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 4e34d67..ddb49c9 100644 --- a/main/opengeometry/src/lib.rs +++ b/main/opengeometry/src/lib.rs @@ -25,6 +25,7 @@ pub mod primitives { pub mod polygon; pub mod polyline; pub mod rectangle; + pub mod wedge; pub mod sweep; } 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 c672c8c..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}; @@ -228,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, @@ -572,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,