diff --git a/.vscode/settings.json b/.vscode/settings.json index 1050f08..7e8d784 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,18 @@ { "rust-analyzer.linkedProjects": [ - "./main/opengeometry/Cargo.toml" - ] + "./main/opengeometry/Cargo.toml" + ], + + // "rust-analyzer.server.extraEnv": { + // "RA_MAX_MEMORY": "1536" + // }, + + // "rust-analyzer.cargo.allFeatures": false, + // "rust-analyzer.cargo.loadOutDirsFromCheck": false, + // "rust-analyzer.cargo.buildScripts.enable": false, + + // "rust-analyzer.procMacro.enable": false, + + // "rust-analyzer.checkOnSave": true, + // "rust-analyzer.checkOnSave.command": "check", } \ No newline at end of file diff --git a/main/opengeometry-three/examples-vite/index.html b/main/opengeometry-three/examples-vite/index.html index 9d8568b..f6529d7 100644 --- a/main/opengeometry-three/examples-vite/index.html +++ b/main/opengeometry-three/examples-vite/index.html @@ -352,7 +352,7 @@

Core examples

Primitives

-

5 core demos

+

6 core demos

@@ -377,6 +377,18 @@

Curve

Open example +
+
+

Elliptical Arc

+
+

Elliptical arc with independent X and Z radius, start, end, and segment controls.

+
+ Radius X + Radius Z + Angles +
+ Open example +

Line

diff --git a/main/opengeometry-three/examples-vite/primitives/elliptical_arc.html b/main/opengeometry-three/examples-vite/primitives/elliptical_arc.html new file mode 100644 index 0000000..309323c --- /dev/null +++ b/main/opengeometry-three/examples-vite/primitives/elliptical_arc.html @@ -0,0 +1,268 @@ + + + + OpenGeometry Elliptical Arc Example + + + + + + +
+ Back + Elliptical Arc +
+ + + +
+ + + + \ No newline at end of file diff --git a/main/opengeometry-three/src/editor/types.ts b/main/opengeometry-three/src/editor/types.ts index 7487efd..8c846f7 100644 --- a/main/opengeometry-three/src/editor/types.ts +++ b/main/opengeometry-three/src/editor/types.ts @@ -219,6 +219,7 @@ export type ParametricEntityType = | "polyline" | "arc" | "curve" + | "elliptical_arc" | "rectangle" | "polygon" | "cuboid" diff --git a/main/opengeometry-three/src/primitives/elliptical_arc.ts b/main/opengeometry-three/src/primitives/elliptical_arc.ts new file mode 100644 index 0000000..0bd6b09 --- /dev/null +++ b/main/opengeometry-three/src/primitives/elliptical_arc.ts @@ -0,0 +1,323 @@ +import { OGEllipticalArc, Vector3 } from "../../../opengeometry/pkg/opengeometry"; +import * as THREE from "three"; +import { getUUID } from "../utils/randomizer"; +import { Line2 } from 'three/examples/jsm/lines/Line2.js'; +import { LineMaterial } from 'three/examples/jsm/lines/LineMaterial.js'; +import { LineGeometry } from 'three/examples/jsm/lines/LineGeometry.js'; +import { + clonePlacement, + createParametricEditCapabilities, +} from "../editor"; +import { createFreeformGeometry } from "../freeform"; + +/** + * Construction options for an elliptical arc primitive. + */ +export interface IEllipticalArcOptions { + ogid?: string; + center: Vector3; + radiusX: number; + radiusZ: number; + startAngle: number; + endAngle: number; + segments: number; + color: number; + fatLines?: boolean; + width?: number; + translation?: Vector3; + rotation?: Vector3; + scale?: Vector3; +} + +/** + * Placement updates accepted by `EllipticalArc`. + */ +export interface EllipticalArcPlacementOptions { + translation?: Vector3; + rotation?: Vector3; + scale?: Vector3; +} + +/** + * Partial config payload accepted by `EllipticalArc.setConfig(...)`. + */ +export type EllipticalArcConfigUpdate = Partial>; + +/** + * Alias for `EllipticalArc` placement updates. + */ +export type EllipticalArcPlacementUpdate = EllipticalArcPlacementOptions; + +/** + * EllipticalArc wrapper backed by the kernel OGEllipticalArc primitive. + */ +export class EllipticalArc extends THREE.Line { + ogid: string; + options: IEllipticalArcOptions = { + center: new Vector3(0, 0, 0), + radiusX: 1.0, + radiusZ: 0.5, + startAngle: 0, + endAngle: Math.PI * 2, + segments: 32, + color: 0x00ff00, + fatLines: false, + width: 20, + translation: new Vector3(0, 0, 0), + rotation: new Vector3(0, 0, 0), + scale: new Vector3(1, 1, 1), + }; + + private ellipticalArc: OGEllipticalArc; + private fatLine: Line2 | null = null; + + set color(color: number) { + this.options.color = color; + if (this.material instanceof THREE.LineBasicMaterial) { + this.material.color.set(color); + } + if (this.fatLine && this.fatLine.material instanceof LineMaterial) { + this.fatLine.material.color.set(color); + } + } + + constructor(options?: IEllipticalArcOptions) { + super(); + + this.ogid = options?.ogid ?? getUUID(); + this.ellipticalArc = new OGEllipticalArc(this.ogid); + + this.options = { ...this.options, ...options }; + this.options.ogid = this.ogid; + + this.setConfig({ + center: this.options.center.clone(), + radiusX: this.options.radiusX, + radiusZ: this.options.radiusZ, + startAngle: this.options.startAngle, + endAngle: this.options.endAngle, + segments: this.options.segments, + color: this.options.color, + fatLines: this.options.fatLines, + width: this.options.width, + }); + this.setPlacement({ + translation: this.options.translation?.clone(), + rotation: this.options.rotation?.clone(), + scale: this.options.scale?.clone(), + }); + } + + validateOptions() { + if (!this.options) { + throw new Error("Options are not defined for EllipticalArc"); + } + } + + setConfig(options: EllipticalArcConfigUpdate) { + this.validateOptions(); + + const nextOptions = { ...this.options, ...options }; + const geometryChanged = + "center" in options || + "radiusX" in options || + "radiusZ" in options || + "segments" in options || + "startAngle" in options || + "endAngle" in options; + const renderChanged = + "color" in options || + "fatLines" in options || + "width" in options; + + this.options = nextOptions; + + if (geometryChanged) { + this.ellipticalArc.set_config( + nextOptions.center.clone(), + nextOptions.radiusX, + nextOptions.radiusZ, + nextOptions.startAngle, + nextOptions.endAngle, + nextOptions.segments + ); + this.generateGeometry(); + return; + } + + if (renderChanged) { + this.updateRenderStyle(); + } + } + + getAnchor() { + const anchor = this.ellipticalArc.get_anchor(); + return new Vector3(anchor.x, anchor.y, anchor.z); + } + + setPlacement(placement: EllipticalArcPlacementUpdate) { + this.options.translation = placement.translation?.clone() ?? this.options.translation; + this.options.rotation = placement.rotation?.clone() ?? this.options.rotation; + this.options.scale = placement.scale?.clone() ?? this.options.scale; + + this.ellipticalArc.set_transform( + this.options.translation?.clone() ?? new Vector3(0, 0, 0), + this.options.rotation?.clone() ?? new Vector3(0, 0, 0), + this.options.scale?.clone() ?? new Vector3(1, 1, 1) + ); + this.generateGeometry(); + } + + setTransform(translation: Vector3, rotation: Vector3, scale: Vector3) { + this.setPlacement({ translation, rotation, scale }); + } + + setTranslation(translation: Vector3) { + this.setPlacement({ translation }); + } + + setRotation(rotation: Vector3) { + this.setPlacement({ rotation }); + } + + setScale(scale: Vector3) { + this.setPlacement({ scale }); + } + + getConfig() { + return this.options; + } + + getPlacement() { + return clonePlacement({ + translation: this.options.translation, + rotation: this.options.rotation, + scale: this.options.scale, + }); + } + + getEditCapabilities() { + return createParametricEditCapabilities("elliptical_arc", "curve"); + } + + canConvertToFreeform() { + return true; + } + + toFreeform(id: string = this.ogid) { + if (!this.canConvertToFreeform()) { + throw new Error("This entity cannot be converted to freeform."); + } + return createFreeformGeometry(this.ellipticalArc.get_local_brep_serialized(), { + id, + placement: this.getPlacement(), + }); + } + + private getCurrentPositions() { + const attribute = this.geometry.getAttribute("position"); + if (!attribute || attribute.itemSize !== 3) { + return []; + } + + const positions = []; + for (let index = 0; index < attribute.count; index += 1) { + positions.push( + attribute.getX(index), + attribute.getY(index), + attribute.getZ(index) + ); + } + + return positions; + } + + private updateRenderStyle(bufferData?: number[]) { + const positions = bufferData ?? this.getCurrentPositions(); + + if (this.options.fatLines) { + if (!this.fatLine) { + this.fatLine = new Line2( + new LineGeometry(), + new LineMaterial({ + color: this.options.color, + linewidth: this.options.width, + resolution: new THREE.Vector2(window.innerWidth, window.innerHeight), + }) + ); + this.add(this.fatLine); + } + + this.fatLine.geometry.setPositions(positions); + (this.fatLine.material as LineMaterial).color.set(this.options.color); + (this.fatLine.material as LineMaterial).linewidth = this.options.width ?? 5; + (this.fatLine.material as LineMaterial).resolution.set( + window.innerWidth, + window.innerHeight + ); + this.fatLine.visible = true; + } else if (this.fatLine) { + this.fatLine.visible = false; + } + + if (this.material instanceof THREE.LineBasicMaterial) { + this.material.color.set(this.options.color); + this.material.visible = !this.options.fatLines; + } + } + + private generateGeometry() { + const bufferData = Array.from(this.ellipticalArc.get_geometry_buffer()); + + this.writePositionsToGeometry(this.geometry, bufferData); + this.geometry.computeBoundingBox(); + this.geometry.computeBoundingSphere(); + + if (this.material instanceof THREE.LineBasicMaterial) { + this.material.color.set(this.options.color); + } else { + if (Array.isArray(this.material)) { + this.material.forEach((material) => material.dispose()); + } else { + this.material.dispose(); + } + this.material = new THREE.LineBasicMaterial({ color: this.options.color }); + } + + this.updateRenderStyle(bufferData); + } + + getBrep() { + const brepData = this.ellipticalArc.get_brep_serialized(); + if (!brepData) { + throw new Error("Brep data is not available for EllipticalArc"); + } + return JSON.parse(brepData); + } + + discardGeometry() { + this.geometry.dispose(); + } + + private writePositionsToGeometry( + geometry: THREE.BufferGeometry, + positions: number[] + ) { + const existing = geometry.getAttribute("position"); + if ( + !(existing instanceof THREE.BufferAttribute) || + existing.itemSize !== 3 || + existing.count !== positions.length / 3 + ) { + geometry.setAttribute( + "position", + new THREE.Float32BufferAttribute(positions, 3) + ); + return; + } + + const array = existing.array as Float32Array; + array.set(positions); + existing.needsUpdate = true; + } +} \ No newline at end of file diff --git a/main/opengeometry-three/src/primitives/index.ts b/main/opengeometry-three/src/primitives/index.ts index 907bf4f..38ec9eb 100644 --- a/main/opengeometry-three/src/primitives/index.ts +++ b/main/opengeometry-three/src/primitives/index.ts @@ -6,3 +6,4 @@ export * from './polyline'; export * from './arc'; export * from './rectangle'; export * from './curve'; +export * from "./elliptical_arc"; \ No newline at end of file diff --git a/main/opengeometry/src/lib.rs b/main/opengeometry/src/lib.rs index 959caa5..6363a0c 100644 --- a/main/opengeometry/src/lib.rs +++ b/main/opengeometry/src/lib.rs @@ -20,6 +20,7 @@ pub mod primitives { pub mod cuboid; pub mod curve; pub mod cylinder; + pub mod elliptical_arc; pub mod line; pub mod polygon; pub mod polyline; diff --git a/main/opengeometry/src/primitives/elliptical_arc.rs b/main/opengeometry/src/primitives/elliptical_arc.rs new file mode 100644 index 0000000..ed771b5 --- /dev/null +++ b/main/opengeometry/src/primitives/elliptical_arc.rs @@ -0,0 +1,226 @@ +use serde::{Deserialize, Serialize}; +use wasm_bindgen::prelude::*; + +use crate::brep::{Brep, BrepBuilder}; +use crate::export::projection::{project_brep_to_scene, CameraParameters, HlrOptions, Scene2D}; +use crate::spatial::placement::Placement3D; +use openmaths::Vector3; +use uuid::Uuid; + +#[wasm_bindgen] +#[derive(Clone, Serialize, Deserialize)] +pub struct OGEllipticalArc { + id: String, + center: Vector3, + radius_x: f64, + radius_z: f64, + start_angle: f64, + end_angle: f64, + segments: u32, + placement: Placement3D, + brep: Brep, +} + +#[wasm_bindgen] +impl OGEllipticalArc { + #[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) -> OGEllipticalArc { + let internal_id = Uuid::new_v4(); + + OGEllipticalArc { + id, + center: Vector3::new(0.0, 0.0, 0.0), + radius_x: 1.0, + radius_z: 0.5, + start_angle: 0.0, + end_angle: 2.0 * std::f64::consts::PI, + segments: 32, + placement: Placement3D::new(), + brep: Brep::new(internal_id), + } + } + + #[wasm_bindgen] + pub fn set_config( + &mut self, + center: Vector3, + radius_x: f64, + radius_z: f64, + start_angle: f64, + end_angle: f64, + segments: u32, + ) -> Result<(), JsValue> { + self.center = center; + self.radius_x = radius_x.max(1.0e-6); + self.radius_z = radius_z.max(1.0e-6); + self.start_angle = start_angle; + self.end_angle = end_angle; + self.segments = segments.max(3); + self.placement.set_anchor(self.center); + self.generate_geometry() + } + + #[wasm_bindgen] + pub fn set_center(&mut self, center: Vector3) { + self.center = center; + self.placement.set_anchor(self.center); + } + + #[wasm_bindgen] + pub fn set_transform( + &mut self, + position: Vector3, + rotation: Vector3, + scale: Vector3, + ) -> Result<(), JsValue> { + self.placement + .set_transform(position, rotation, scale) + .map_err(|err| JsValue::from_str(&err)) + } + + #[wasm_bindgen] + pub fn set_translation(&mut self, translation: Vector3) { + self.placement.set_translation(translation); + } + + #[wasm_bindgen] + pub fn set_rotation(&mut self, rotation: Vector3) { + self.placement.set_rotation(rotation); + } + + #[wasm_bindgen] + pub fn set_scale(&mut self, scale: Vector3) -> Result<(), JsValue> { + self.placement + .set_scale(scale) + .map_err(|err| JsValue::from_str(&err)) + } + + #[wasm_bindgen] + pub fn generate_geometry(&mut self) -> Result<(), JsValue> { + let segment_count = self.segments.max(3); + let angle_step = (self.end_angle - self.start_angle) / segment_count as f64; + + let mut points = Vec::with_capacity((segment_count + 1) as usize); + let mut angle = self.start_angle; + for _ in 0..=segment_count { + // Key difference from OGArc: radius_x on X axis, radius_z on Z axis + let x = self.radius_x * angle.cos(); + let y = 0.0; + let z = self.radius_z * angle.sin(); + points.push(Vector3::new(x, y, z)); + angle += angle_step; + } + + let is_closed = + (self.end_angle - self.start_angle).abs() >= 2.0 * std::f64::consts::PI - 1.0e-9; + + // Deduplicate first and last point when closed + if is_closed && points.len() > 2 { + let first = points[0]; + let last = *points.last().unwrap(); + let dx = first.x - last.x; + let dy = first.y - last.y; + let dz = first.z - last.z; + if dx * dx + dy * dy + dz * dz <= 1.0e-12 { + points.pop(); + } + } + + let mut builder = BrepBuilder::new(self.brep.id); + builder.add_vertices(&points); + + if points.len() >= 2 { + let indices: Vec = (0..points.len() as u32).collect(); + builder + .add_wire(&indices, is_closed && points.len() > 2) + .map_err(|err| { + JsValue::from_str(&format!("Failed to build elliptical arc wire: {}", err)) + })?; + } + + self.brep = builder.build().map_err(|err| { + JsValue::from_str(&format!("Failed to finalize elliptical arc BREP: {}", err)) + })?; + + Ok(()) + } + + #[wasm_bindgen] + pub fn dispose_points(&mut self) { + self.brep.clear(); + } + + #[wasm_bindgen] + pub fn destroy(&mut self) { + self.brep.clear(); + self.id.clear(); + } + + #[wasm_bindgen] + pub fn get_brep_serialized(&self) -> String { + serde_json::to_string(&self.world_brep()).unwrap() + } + + #[wasm_bindgen] + pub fn get_local_brep_serialized(&self) -> String { + serde_json::to_string(&self.brep).unwrap() + } + + #[wasm_bindgen] + pub fn get_geometry_serialized(&self) -> String { + serde_json::to_string(&wire_geometry_buffer(&self.world_brep())).unwrap() + } + + #[wasm_bindgen] + pub fn get_local_geometry_serialized(&self) -> String { + serde_json::to_string(&wire_geometry_buffer(&self.brep)).unwrap() + } + + #[wasm_bindgen] + pub fn get_geometry_buffer(&self) -> Vec { + wire_geometry_buffer(&self.world_brep()) + } + + #[wasm_bindgen] + pub fn get_local_geometry_buffer(&self) -> Vec { + wire_geometry_buffer(&self.brep) + } + + #[wasm_bindgen] + pub fn get_anchor(&self) -> Vector3 { + self.placement.anchor + } +} + +impl OGEllipticalArc { + pub fn brep(&self) -> &Brep { + &self.brep + } + + pub fn world_brep(&self) -> Brep { + self.brep.transformed(&self.placement) + } + + pub fn to_projected_scene2d(&self, camera: &CameraParameters, hlr: &HlrOptions) -> Scene2D { + let world_brep = self.world_brep(); + project_brep_to_scene(&world_brep, camera, hlr) + } +} + +fn wire_geometry_buffer(brep: &Brep) -> Vec { + let Some(wire) = brep.wires.first() else { + return Vec::new(); + }; + + brep.get_wire_vertex_buffer(wire.id, wire.is_closed) +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index eb5eec5..7a42f83 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,13 +1,13 @@ { "name": "opengeometry", - "version": "2.0.3", + "version": "2.0.9", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "opengeometry", - "version": "2.0.3", - "license": "ISC", + "version": "2.0.9", + "license": "MPL-2.0", "dependencies": { "tsc": "^2.0.4", "uuid": "^10.0.0" @@ -28,7 +28,7 @@ "vite": "^6.2.2" }, "peerDependencies": { - "three": "^0.168.0" + "three": ">=0.168.0" } }, "node_modules/@ampproject/remapping": {