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": {