use std::sync::Mutex;
use ferrocad_curves::{
ANGULAR_TOLERANCE, Arc, ArcOfHyperbola, ArcOfParabola, BezierCurve, Circle, CurveError,
EdgeCurve, Ellipse, Hyperbola, Line, LineSegment, NurbsCurve, Parabola, TOLERANCE, Vector3,
polygon,
};
use ferrocad_geom::{ElementKind, Shape};
use ferrocad_occt::surface_ops;
use ferrocad_part::backend;
use ferrocad_types::Matrix4;
use pyo3::create_exception;
use pyo3::exceptions::{
PyException, PyIndexError, PyNotImplementedError, PyRuntimeError, PyTypeError, PyValueError,
};
use pyo3::prelude::*;
use pyo3::types::{PyAny, PyDict, PyModule, PySequence, PyTuple};
use pyo3::IntoPyObjectExt;
use crate::PartShape;
create_exception!(
Part,
OCCError,
PyException,
"A Part geometry operation failed (upstream's `Part.OCCError`)."
);
fn curve_error(error: CurveError) -> PyErr {
OCCError::new_err(error.to_string())
}
fn kernel_error(error: ferrocad_geom::GeomError) -> PyErr {
OCCError::new_err(error.to_string())
}
pub(crate) fn vector(value: &Bound<'_, PyAny>) -> PyResult<Vector3> {
if let Ok(vertex) = value.extract::<PyRef<'_, PyVertex>>() {
return Ok(vertex.point);
}
if let Ok((x, y, z)) = value.extract::<(f64, f64, f64)>() {
return Ok(Vector3::new(x, y, z));
}
if let Ok(sequence) = value.downcast::<PySequence>() {
if sequence.len().ok() == Some(3) {
let x: f64 = sequence.get_item(0)?.extract()?;
let y: f64 = sequence.get_item(1)?.extract()?;
let z: f64 = sequence.get_item(2)?.extract()?;
return Ok(Vector3::new(x, y, z));
}
}
let x: f64 = value.getattr("x")?.extract()?;
let y: f64 = value.getattr("y")?.extract()?;
let z: f64 = value.getattr("z")?.extract()?;
Ok(Vector3::new(x, y, z))
}
pub(crate) fn py_vector(py: Python<'_>, value: Vector3) -> PyResult<PyObject> {
let module = py.import("ferrocad")?;
Ok(module
.getattr("Vector")?
.call1((value.x, value.y, value.z))?
.unbind())
}
pub(crate) fn vector2d(value: &Bound<'_, PyAny>) -> PyResult<(f64, f64)> {
if let Ok((x, y)) = value.extract::<(f64, f64)>() {
return Ok((x, y));
}
if let Ok(sequence) = value.downcast::<PySequence>() {
if sequence.len().ok() == Some(2) {
let x: f64 = sequence.get_item(0)?.extract()?;
let y: f64 = sequence.get_item(1)?.extract()?;
return Ok((x, y));
}
}
let x: f64 = value.getattr("x")?.extract()?;
let y: f64 = value.getattr("y")?.extract()?;
Ok((x, y))
}
fn py_vector2d(py: Python<'_>, value: (f64, f64)) -> PyResult<PyObject> {
let module = py.import("ferrocad")?;
Ok(module.getattr("Vector2d")?.call1(value)?.unbind())
}
fn shape_from_curve(curve: &EdgeCurve) -> PyResult<Shape> {
backend().make_edge(curve).map_err(kernel_error)
}
fn curve_to_py(py: Python<'_>, curve: EdgeCurve) -> PyResult<PyObject> {
match curve {
EdgeCurve::Line(line) => Ok(Py::new(py, PyLine { curve: line })?.into_any()),
EdgeCurve::LineSegment(segment) => {
Ok(Py::new(py, PyLineSegment { curve: segment })?.into_any())
}
EdgeCurve::Circle(circle) => Ok(Py::new(py, PyCircle { curve: circle })?.into_any()),
EdgeCurve::Arc(arc) => Ok(Py::new(py, PyCircle { curve: arc.circle() })?.into_any()),
EdgeCurve::Ellipse(ellipse) => {
Ok(Py::new(py, PyEllipse { curve: ellipse })?.into_any())
}
}
}
#[pyclass(name = "LineSegment", module = "Part")]
#[derive(Clone)]
pub struct PyLineSegment {
curve: LineSegment,
}
#[pymethods]
impl PyLineSegment {
#[getter]
fn TypeId(&self) -> &'static str {
"Part::GeomLineSegment"
}
#[new]
fn new(start: &Bound<'_, PyAny>, end: &Bound<'_, PyAny>) -> PyResult<Self> {
let curve = LineSegment::new(vector(start)?, vector(end)?).map_err(curve_error)?;
Ok(Self { curve })
}
#[getter]
fn StartPoint(&self, py: Python<'_>) -> PyResult<PyObject> {
py_vector(py, self.curve.start())
}
#[getter]
fn EndPoint(&self, py: Python<'_>) -> PyResult<PyObject> {
py_vector(py, self.curve.end())
}
fn toShape(&self, py: Python<'_>) -> PyResult<Py<PyEdge>> {
Py::new(py, PyEdge::from_curve(EdgeCurve::LineSegment(self.curve))?)
}
fn __repr__(&self) -> String {
format!("Part.LineSegment({:?}, {:?})", self.curve.start(), self.curve.end())
}
}
#[pyclass(name = "Line", module = "Part")]
#[derive(Clone)]
pub struct PyLine {
curve: Line,
}
#[pymethods]
impl PyLine {
#[getter]
fn TypeId(&self) -> &'static str {
"Part::GeomLine"
}
#[new]
#[pyo3(signature = (origin = None, direction = None))]
fn new(
origin: Option<&Bound<'_, PyAny>>,
direction: Option<&Bound<'_, PyAny>>,
) -> PyResult<Self> {
let origin = origin.map(vector).transpose()?.unwrap_or_else(Vector3::zero);
let direction = direction
.map(vector)
.transpose()?
.unwrap_or_else(|| Vector3::new(0.0, 0.0, 1.0));
let curve = Line::new(origin, direction).map_err(curve_error)?;
Ok(Self { curve })
}
#[getter]
fn Location(&self, py: Python<'_>) -> PyResult<PyObject> {
py_vector(py, self.curve.origin())
}
#[getter]
fn Direction(&self, py: Python<'_>) -> PyResult<PyObject> {
py_vector(py, self.curve.direction())
}
fn projectPoint(&self, py: Python<'_>, point: &Bound<'_, PyAny>) -> PyResult<PyObject> {
let point = vector(point)?;
let origin = self.curve.origin();
let direction = self.curve.direction();
let projected = origin.add(&direction.scale(point.sub(&origin).dot(&direction)));
py_vector(py, projected)
}
fn toShape(&self, _py: Python<'_>) -> PyResult<Py<PyEdge>> {
Err(OCCError::new_err(
"an infinite Part.Line has no edge; use Part.LineSegment",
))
}
}
fn matrix_argument(value: &Bound<'_, PyAny>) -> PyResult<Matrix4> {
let mut values = [0.0; 16];
for row in 1..=4 {
for column in 1..=4 {
let name = format!("A{row}{column}");
values[(row - 1) * 4 + (column - 1)] = value.getattr(name)?.extract()?;
}
}
Ok(Matrix4::from_values(values))
}
fn transformed_frame(matrix: &Matrix4, normal: Vector3, x_axis: Vector3) -> PyResult<(Vector3, Vector3, f64)> {
let m = &matrix.m;
let linear = |v: Vector3| {
Vector3::new(
m[0] * v.x + m[1] * v.y + m[2] * v.z,
m[4] * v.x + m[5] * v.y + m[6] * v.z,
m[8] * v.x + m[9] * v.y + m[10] * v.z,
)
};
let column = |offset: usize| Vector3::new(m[offset], m[offset + 4], m[offset + 8]);
let (e1, e2, e3) = (column(0), column(1), column(2));
let scale = e1.length();
if !scale.is_finite() || scale <= 0.0 {
return Err(OCCError::new_err(
"a curve transform needs a non-singular matrix",
));
}
let tolerance = 1e-9 * scale * scale;
let similar = e1.dot(&e2).abs() <= tolerance
&& e1.dot(&e3).abs() <= tolerance
&& e2.dot(&e3).abs() <= tolerance
&& (e2.length() - scale).abs() <= 1e-9 * scale
&& (e3.length() - scale).abs() <= 1e-9 * scale;
if !similar {
return Err(OCCError::new_err(
"a curve transform needs a similarity matrix (rotation, mirror or uniform scale)",
));
}
let x_axis = linear(x_axis).scale(1.0 / scale);
let y_axis = linear(normal.cross(&x_axis)).scale(1.0 / scale);
Ok((x_axis, x_axis.cross(&y_axis), scale))
}
fn transformed_circle(circle: &Circle, matrix: &Matrix4) -> PyResult<Circle> {
let (x_axis, normal, scale) = transformed_frame(matrix, circle.normal(), circle.x_axis())?;
let center = matrix.transform(&circle.center());
Circle::with_axis(center, normal, x_axis, circle.radius() * scale).map_err(curve_error)
}
fn transformed_ellipse(ellipse: &Ellipse, matrix: &Matrix4) -> PyResult<Ellipse> {
let (x_axis, normal, scale) = transformed_frame(matrix, ellipse.normal(), ellipse.x_axis())?;
let center = matrix.transform(&ellipse.center());
Ellipse::with_axis(
center,
normal,
x_axis,
ellipse.major_radius() * scale,
ellipse.minor_radius() * scale,
)
.map_err(curve_error)
}
#[pyclass(name = "Circle", module = "Part")]
#[derive(Clone)]
pub struct PyCircle {
curve: Circle,
}
#[pymethods]
impl PyCircle {
#[getter]
fn TypeId(&self) -> &'static str {
"Part::GeomCircle"
}
#[new]
#[pyo3(signature = (center = None, normal = None, radius = 1.0))]
fn new(
center: Option<&Bound<'_, PyAny>>,
normal: Option<&Bound<'_, PyAny>>,
radius: f64,
) -> PyResult<Self> {
let center = center.map(vector).transpose()?.unwrap_or_else(Vector3::zero);
let normal = normal
.map(vector)
.transpose()?
.unwrap_or_else(|| Vector3::new(0.0, 0.0, 1.0));
let curve = Circle::new(center, normal, radius).map_err(curve_error)?;
Ok(Self { curve })
}
#[getter]
fn Center(&self, py: Python<'_>) -> PyResult<PyObject> {
py_vector(py, self.curve.center())
}
#[getter]
fn Axis(&self, py: Python<'_>) -> PyResult<PyObject> {
py_vector(py, self.curve.normal())
}
#[getter]
fn Radius(&self) -> f64 {
self.curve.radius()
}
#[setter]
fn set_Radius(&mut self, value: f64) -> PyResult<()> {
self.curve = Circle::with_axis(
self.curve.center(),
self.curve.normal(),
self.curve.x_axis(),
value,
)
.map_err(curve_error)?;
Ok(())
}
fn toShape(&self, py: Python<'_>) -> PyResult<Py<PyEdge>> {
Py::new(py, PyEdge::from_curve(EdgeCurve::Circle(self.curve))?)
}
fn transform(&mut self, matrix: &Bound<'_, PyAny>) -> PyResult<()> {
let matrix = matrix_argument(matrix)?;
self.curve = transformed_circle(&self.curve, &matrix)?;
Ok(())
}
fn value(&self, py: Python<'_>, angle: f64) -> PyResult<PyObject> {
py_vector(py, self.curve.point_at(angle))
}
#[pyo3(signature = (first = None, last = None))]
fn toNurbs(
&self,
py: Python<'_>,
first: Option<f64>,
last: Option<f64>,
) -> PyResult<Py<PyBSplineCurve>> {
let first = first.unwrap_or(0.0);
let last = last.unwrap_or(std::f64::consts::TAU);
conic_spline(py, &Conic::Circle(self.curve), first, last, true)
}
#[pyo3(signature = (first = None, last = None))]
fn toBSpline(
&self,
py: Python<'_>,
first: Option<f64>,
last: Option<f64>,
) -> PyResult<Py<PyBSplineCurve>> {
let first = first.unwrap_or(0.0);
let last = last.unwrap_or(std::f64::consts::TAU);
conic_spline(py, &Conic::Circle(self.curve), first, last, false)
}
#[pyo3(signature = (first = None, last = None))]
fn trim(
&self,
py: Python<'_>,
first: Option<f64>,
last: Option<f64>,
) -> PyResult<Py<PyArcOfCircle>> {
let first = first.unwrap_or(0.0);
let last = last.unwrap_or(std::f64::consts::TAU);
let curve = Arc::new(self.curve, first, last).map_err(curve_error)?;
Py::new(py, PyArcOfCircle { curve })
}
}
fn arc_through_three_points(p1: Vector3, p2: Vector3, p3: Vector3) -> PyResult<Arc> {
let a = p2.sub(&p1);
let b = p3.sub(&p1);
let normal = a.cross(&b);
let normal_length = normal.length();
if normal_length <= TOLERANCE {
return Err(OCCError::new_err(
"Three points are collinear or coincident",
));
}
let offset = b
.cross(&normal)
.scale(a.sqr())
.add(&normal.cross(&a).scale(b.sqr()))
.scale(1.0 / (2.0 * normal_length * normal_length));
let center = p1.add(&offset);
let x_axis = p1.sub(¢er);
let radius = x_axis.length();
if radius <= TOLERANCE {
return Err(OCCError::new_err("Three points are coincident"));
}
let circle = Circle::with_axis(center, normal, x_axis, radius).map_err(curve_error)?;
let y_axis = circle.y_axis();
let angle = |point: &Vector3| {
let offset = point.sub(¢er);
offset.dot(&y_axis).atan2(offset.dot(&circle.x_axis()))
};
let first = angle(&p1);
let through = (angle(&p2) - first).rem_euclid(std::f64::consts::TAU);
let end = (angle(&p3) - first).rem_euclid(std::f64::consts::TAU);
let last = if through < end {
first + end
} else {
first + end - std::f64::consts::TAU
};
Arc::new(circle, first, last).map_err(curve_error)
}
#[pyclass(name = "ArcOfCircle", module = "Part")]
#[derive(Clone)]
pub struct PyArcOfCircle {
curve: Arc,
}
#[pymethods]
impl PyArcOfCircle {
#[getter]
fn TypeId(&self) -> &'static str {
"Part::GeomArcOfCircle"
}
#[new]
#[pyo3(signature = (circle, first = None, last = None))]
fn new(
circle: &Bound<'_, PyAny>,
first: Option<&Bound<'_, PyAny>>,
last: Option<&Bound<'_, PyAny>>,
) -> PyResult<Self> {
if let Ok(circle) = circle.extract::<PyRef<'_, PyCircle>>() {
let (Some(first), Some(last)) = (first, last) else {
return Err(PyTypeError::new_err(
"ArcOfCircle(circle, first, last) needs both an angle range",
));
};
let curve = Arc::new(circle.curve, first.extract()?, last.extract()?)
.map_err(curve_error)?;
return Ok(Self { curve });
}
let (p1, p2, p3) = match (first, last) {
(Some(first), Some(last)) => (vector(circle)?, vector(first)?, vector(last)?),
(None, None) => {
let sequence = circle.downcast::<PySequence>().map_err(|_| {
PyTypeError::new_err(
"ArcOfCircle expects (circle, first, last) or three points",
)
})?;
if sequence.len().ok() != Some(3) {
return Err(PyTypeError::new_err(
"ArcOfCircle expects exactly three points",
));
}
(
vector(&sequence.get_item(0)?)?,
vector(&sequence.get_item(1)?)?,
vector(&sequence.get_item(2)?)?,
)
}
_ => {
return Err(PyTypeError::new_err(
"ArcOfCircle expects (circle, first, last) or three points",
));
}
};
let curve = arc_through_three_points(p1, p2, p3)?;
Ok(Self { curve })
}
#[getter]
fn Center(&self, py: Python<'_>) -> PyResult<PyObject> {
py_vector(py, self.curve.circle().center())
}
#[getter]
fn Radius(&self) -> f64 {
self.curve.circle().radius()
}
#[getter]
fn StartPoint(&self, py: Python<'_>) -> PyResult<PyObject> {
py_vector(py, self.curve.start())
}
#[getter]
fn EndPoint(&self, py: Python<'_>) -> PyResult<PyObject> {
py_vector(py, self.curve.end())
}
#[getter]
fn FirstParameter(&self) -> f64 {
self.curve.first()
}
#[getter]
fn LastParameter(&self) -> f64 {
self.curve.last()
}
fn value(&self, py: Python<'_>, angle: f64) -> PyResult<PyObject> {
py_vector(py, self.curve.point_at(angle))
}
#[pyo3(signature = (first = None, last = None))]
fn toNurbs(
&self,
py: Python<'_>,
first: Option<f64>,
last: Option<f64>,
) -> PyResult<Py<PyBSplineCurve>> {
let first = first.unwrap_or(self.curve.first());
let last = last.unwrap_or(self.curve.last());
conic_spline(py, &Conic::Circle(self.curve.circle()), first, last, true)
}
#[pyo3(signature = (first = None, last = None))]
fn toBSpline(
&self,
py: Python<'_>,
first: Option<f64>,
last: Option<f64>,
) -> PyResult<Py<PyBSplineCurve>> {
let first = first.unwrap_or(self.curve.first());
let last = last.unwrap_or(self.curve.last());
conic_spline(py, &Conic::Circle(self.curve.circle()), first, last, false)
}
fn toShape(&self, py: Python<'_>) -> PyResult<Py<PyEdge>> {
Py::new(py, PyEdge::from_curve(EdgeCurve::Arc(self.curve))?)
}
}
#[pyclass(name = "Ellipse", module = "Part")]
#[derive(Clone)]
pub struct PyEllipse {
curve: Ellipse,
}
#[pymethods]
impl PyEllipse {
#[getter]
fn TypeId(&self) -> &'static str {
"Part::GeomEllipse"
}
#[new]
#[pyo3(signature = (center = None, major_radius = 1.0, minor_radius = 0.5))]
fn new(
center: Option<&Bound<'_, PyAny>>,
major_radius: f64,
minor_radius: f64,
) -> PyResult<Self> {
if minor_radius > major_radius {
return Err(OCCError::new_err(
"Part.Ellipse needs the major radius to be at least the minor radius",
));
}
let center = center.map(vector).transpose()?.unwrap_or_else(Vector3::zero);
let curve = Ellipse::new(center, Vector3::new(0.0, 0.0, 1.0), major_radius, minor_radius)
.map_err(curve_error)?;
Ok(Self { curve })
}
#[getter]
fn Center(&self, py: Python<'_>) -> PyResult<PyObject> {
py_vector(py, self.curve.center())
}
#[getter]
fn Axis(&self, py: Python<'_>) -> PyResult<PyObject> {
py_vector(py, self.curve.normal())
}
#[getter]
fn MajorAxis(&self, py: Python<'_>) -> PyResult<PyObject> {
py_vector(py, self.curve.x_axis())
}
#[getter]
fn MinorAxis(&self, py: Python<'_>) -> PyResult<PyObject> {
py_vector(py, self.curve.y_axis())
}
#[getter]
fn MajorRadius(&self) -> f64 {
self.curve.major_radius()
}
#[setter]
fn set_MajorRadius(&mut self, value: f64) -> PyResult<()> {
if value < self.curve.minor_radius() {
return Err(OCCError::new_err(
"Part.Ellipse needs the major radius to be at least the minor radius",
));
}
self.curve = Ellipse::with_axis(
self.curve.center(),
self.curve.normal(),
self.curve.x_axis(),
value,
self.curve.minor_radius(),
)
.map_err(curve_error)?;
Ok(())
}
#[getter]
fn MinorRadius(&self) -> f64 {
self.curve.minor_radius()
}
#[setter]
fn set_MinorRadius(&mut self, value: f64) -> PyResult<()> {
if value > self.curve.major_radius() {
return Err(OCCError::new_err(
"Part.Ellipse needs the minor radius to be at most the major radius",
));
}
self.curve = Ellipse::with_axis(
self.curve.center(),
self.curve.normal(),
self.curve.x_axis(),
self.curve.major_radius(),
value,
)
.map_err(curve_error)?;
Ok(())
}
fn value(&self, py: Python<'_>, angle: f64) -> PyResult<PyObject> {
py_vector(py, self.curve.point_at(angle))
}
fn toShape(&self, py: Python<'_>) -> PyResult<Py<PyEdge>> {
Py::new(py, PyEdge::from_curve(EdgeCurve::Ellipse(self.curve))?)
}
fn transform(&mut self, matrix: &Bound<'_, PyAny>) -> PyResult<()> {
let matrix = matrix_argument(matrix)?;
self.curve = transformed_ellipse(&self.curve, &matrix)?;
Ok(())
}
#[pyo3(signature = (first = None, last = None))]
fn toNurbs(
&self,
py: Python<'_>,
first: Option<f64>,
last: Option<f64>,
) -> PyResult<Py<PyBSplineCurve>> {
let first = first.unwrap_or(0.0);
let last = last.unwrap_or(std::f64::consts::TAU);
conic_spline(py, &Conic::Ellipse(self.curve), first, last, true)
}
#[pyo3(signature = (first = None, last = None))]
fn toBSpline(
&self,
py: Python<'_>,
first: Option<f64>,
last: Option<f64>,
) -> PyResult<Py<PyBSplineCurve>> {
let first = first.unwrap_or(0.0);
let last = last.unwrap_or(std::f64::consts::TAU);
conic_spline(py, &Conic::Ellipse(self.curve), first, last, false)
}
#[pyo3(signature = (first = None, last = None))]
fn trim(
&self,
py: Python<'_>,
first: Option<f64>,
last: Option<f64>,
) -> PyResult<Py<PyArcOfEllipse>> {
let first = first.unwrap_or(0.0);
let last = last.unwrap_or(std::f64::consts::TAU);
if !first.is_finite()
|| !last.is_finite()
|| (last - first).abs() <= ANGULAR_TOLERANCE
{
return Err(OCCError::new_err("an ellipse arc needs a non-zero sweep"));
}
Py::new(
py,
PyArcOfEllipse {
ellipse: self.curve,
first,
last,
},
)
}
fn __repr__(&self) -> String {
format!(
"Part.Ellipse({:?}, {}, {})",
self.curve.center(),
self.curve.major_radius(),
self.curve.minor_radius()
)
}
}
#[pyclass(name = "ArcOfEllipse", module = "Part")]
#[derive(Clone)]
pub struct PyArcOfEllipse {
ellipse: Ellipse,
first: f64,
last: f64,
}
#[pymethods]
impl PyArcOfEllipse {
#[getter]
fn TypeId(&self) -> &'static str {
"Part::GeomArcOfEllipse"
}
#[new]
fn new(ellipse: &Bound<'_, PyAny>, first: f64, last: f64) -> PyResult<Self> {
let ellipse = ellipse.extract::<PyRef<'_, PyEllipse>>()?.curve;
if !first.is_finite()
|| !last.is_finite()
|| (last - first).abs() <= ANGULAR_TOLERANCE
{
return Err(OCCError::new_err("an ellipse arc needs a non-zero sweep"));
}
Ok(Self {
ellipse,
first,
last,
})
}
#[getter]
fn Ellipse(&self, py: Python<'_>) -> PyResult<Py<PyEllipse>> {
Py::new(
py,
PyEllipse {
curve: self.ellipse,
},
)
}
#[getter]
fn Center(&self, py: Python<'_>) -> PyResult<PyObject> {
py_vector(py, self.ellipse.center())
}
#[getter]
fn Axis(&self, py: Python<'_>) -> PyResult<PyObject> {
py_vector(py, self.ellipse.normal())
}
#[getter]
fn MajorAxis(&self, py: Python<'_>) -> PyResult<PyObject> {
py_vector(py, self.ellipse.x_axis())
}
#[getter]
fn MinorAxis(&self, py: Python<'_>) -> PyResult<PyObject> {
py_vector(py, self.ellipse.y_axis())
}
#[getter]
fn MajorRadius(&self) -> f64 {
self.ellipse.major_radius()
}
#[getter]
fn MinorRadius(&self) -> f64 {
self.ellipse.minor_radius()
}
#[getter]
fn StartPoint(&self, py: Python<'_>) -> PyResult<PyObject> {
py_vector(py, self.ellipse.point_at(self.first))
}
#[getter]
fn EndPoint(&self, py: Python<'_>) -> PyResult<PyObject> {
py_vector(py, self.ellipse.point_at(self.last))
}
#[getter]
fn FirstParameter(&self) -> f64 {
self.first
}
#[getter]
fn LastParameter(&self) -> f64 {
self.last
}
fn value(&self, py: Python<'_>, angle: f64) -> PyResult<PyObject> {
py_vector(py, self.ellipse.point_at(angle))
}
#[pyo3(signature = (first = None, last = None))]
fn toNurbs(
&self,
py: Python<'_>,
first: Option<f64>,
last: Option<f64>,
) -> PyResult<Py<PyBSplineCurve>> {
let first = first.unwrap_or(self.first);
let last = last.unwrap_or(self.last);
conic_spline(py, &Conic::Ellipse(self.ellipse), first, last, true)
}
#[pyo3(signature = (first = None, last = None))]
fn toBSpline(
&self,
py: Python<'_>,
first: Option<f64>,
last: Option<f64>,
) -> PyResult<Py<PyBSplineCurve>> {
let first = first.unwrap_or(self.first);
let last = last.unwrap_or(self.last);
conic_spline(py, &Conic::Ellipse(self.ellipse), first, last, false)
}
}
fn hyperbola_from_three_points(
s1: Vector3,
s2: Vector3,
center: Vector3,
) -> PyResult<Hyperbola> {
let major_axis = s1.sub(¢er);
let major = major_axis.length();
if major <= TOLERANCE {
return Err(OCCError::new_err("the hyperbola points are confused"));
}
let x_axis = major_axis.scale(1.0 / major);
let offset = s2.sub(¢er);
let minor_axis = offset.sub(&x_axis.scale(offset.dot(&x_axis)));
if minor_axis.length() <= TOLERANCE {
return Err(OCCError::new_err("the hyperbola points are collinear"));
}
Hyperbola::with_axis(center, x_axis.cross(&offset), x_axis, major, minor_axis.length())
.map_err(curve_error)
}
#[pyclass(name = "Hyperbola", module = "Part")]
#[derive(Clone)]
pub struct PyHyperbola {
curve: Hyperbola,
}
#[pymethods]
impl PyHyperbola {
#[getter]
fn TypeId(&self) -> &'static str {
"Part::GeomHyperbola"
}
#[new]
#[pyo3(signature = (*args))]
fn new(args: &Bound<'_, PyTuple>) -> PyResult<Self> {
match args.len() {
0 => Ok(Self {
curve: Hyperbola::with_axis(
Vector3::zero(),
Vector3::new(0.0, 0.0, 1.0),
Vector3::new(1.0, 0.0, 0.0),
2.0,
1.0,
)
.map_err(curve_error)?,
}),
1 => {
let other = args.get_item(0)?;
let other = other.extract::<PyRef<'_, PyHyperbola>>()?;
Ok(Self { curve: other.curve })
}
3 => {
let first = args.get_item(0)?;
let second = args.get_item(1)?;
let third = args.get_item(2)?;
if let (Ok(major), Ok(minor)) =
(second.extract::<f64>(), third.extract::<f64>())
{
let center = vector(&first)?;
return Ok(Self {
curve: Hyperbola::with_axis(
center,
Vector3::new(0.0, 0.0, 1.0),
Vector3::new(1.0, 0.0, 0.0),
major,
minor,
)
.map_err(curve_error)?,
});
}
Ok(Self {
curve: hyperbola_from_three_points(
vector(&first)?,
vector(&second)?,
vector(&third)?,
)?,
})
}
_ => Err(PyTypeError::new_err(
"Hyperbola constructor accepts:\n\
-- empty parameter list\n\
-- Hyperbola\n\
-- Point, double, double\n\
-- Point, Point, Point",
)),
}
}
#[getter]
fn Center(&self, py: Python<'_>) -> PyResult<PyObject> {
py_vector(py, self.curve.center())
}
#[getter]
fn Location(&self, py: Python<'_>) -> PyResult<PyObject> {
py_vector(py, self.curve.center())
}
#[getter]
fn Axis(&self, py: Python<'_>) -> PyResult<PyObject> {
py_vector(py, self.curve.normal())
}
#[getter]
fn XAxis(&self, py: Python<'_>) -> PyResult<PyObject> {
py_vector(py, self.curve.x_axis())
}
#[getter]
fn YAxis(&self, py: Python<'_>) -> PyResult<PyObject> {
py_vector(py, self.curve.y_axis())
}
#[getter]
fn MajorRadius(&self) -> f64 {
self.curve.major_radius()
}
#[setter]
fn set_MajorRadius(&mut self, value: f64) -> PyResult<()> {
self.curve = Hyperbola::with_axis(
self.curve.center(),
self.curve.normal(),
self.curve.x_axis(),
value,
self.curve.minor_radius(),
)
.map_err(curve_error)?;
Ok(())
}
#[getter]
fn MinorRadius(&self) -> f64 {
self.curve.minor_radius()
}
#[setter]
fn set_MinorRadius(&mut self, value: f64) -> PyResult<()> {
self.curve = Hyperbola::with_axis(
self.curve.center(),
self.curve.normal(),
self.curve.x_axis(),
self.curve.major_radius(),
value,
)
.map_err(curve_error)?;
Ok(())
}
#[getter]
fn Focal(&self) -> f64 {
self.curve.focal()
}
#[getter]
fn Focus1(&self, py: Python<'_>) -> PyResult<PyObject> {
py_vector(py, self.curve.focus1())
}
#[getter]
fn Focus2(&self, py: Python<'_>) -> PyResult<PyObject> {
py_vector(py, self.curve.focus2())
}
#[getter]
fn Eccentricity(&self) -> f64 {
self.curve.eccentricity()
}
#[getter]
fn FirstParameter(&self) -> f64 {
-2.0e100
}
#[getter]
fn LastParameter(&self) -> f64 {
2.0e100
}
fn value(&self, py: Python<'_>, u: f64) -> PyResult<PyObject> {
if !u.is_finite() {
return Err(OCCError::new_err("the hyperbola parameter must be finite"));
}
py_vector(py, self.curve.point_at(u))
}
#[pyo3(signature = (first = None, last = None))]
fn trim(
&self,
py: Python<'_>,
first: Option<f64>,
last: Option<f64>,
) -> PyResult<Py<PyArcOfHyperbola>> {
let (first, last) = required_range(first, last, "hyperbola")?;
let arc = self.curve.trim(first, last).map_err(curve_error)?;
Py::new(py, PyArcOfHyperbola { arc })
}
#[pyo3(signature = (first = None, last = None))]
fn toNurbs(
&self,
py: Python<'_>,
first: Option<f64>,
last: Option<f64>,
) -> PyResult<Py<PyBSplineCurve>> {
let (first, last) = required_range(first, last, "hyperbola")?;
let nurbs = self.curve.nurbs(first, last).map_err(curve_error)?;
spline_curve(py, &nurbs)
}
#[pyo3(signature = (first = None, last = None))]
fn toBSpline(
&self,
py: Python<'_>,
first: Option<f64>,
last: Option<f64>,
) -> PyResult<Py<PyBSplineCurve>> {
self.toNurbs(py, first, last)
}
#[pyo3(signature = (first = None, last = None))]
fn toShape(
&self,
py: Python<'_>,
first: Option<f64>,
last: Option<f64>,
) -> PyResult<Py<PyEdge>> {
let (first, last) = required_range(first, last, "hyperbola")?;
let nurbs = self.curve.nurbs(first, last).map_err(curve_error)?;
spline_edge(py, &nurbs)
}
fn __repr__(&self) -> String {
format!(
"Part.Hyperbola({:?}, {}, {})",
self.curve.center(),
self.curve.major_radius(),
self.curve.minor_radius()
)
}
}
#[pyclass(name = "ArcOfHyperbola", module = "Part")]
#[derive(Clone)]
pub struct PyArcOfHyperbola {
arc: ArcOfHyperbola,
}
#[pymethods]
impl PyArcOfHyperbola {
#[getter]
fn TypeId(&self) -> &'static str {
"Part::GeomArcOfHyperbola"
}
#[new]
#[pyo3(signature = (hyperbola, first, last, sense = true))]
fn new(
hyperbola: &Bound<'_, PyAny>,
first: f64,
last: f64,
sense: bool,
) -> PyResult<Self> {
let _ = sense;
let curve = hyperbola.extract::<PyRef<'_, PyHyperbola>>()?.curve;
let arc = curve.trim(first, last).map_err(curve_error)?;
Ok(Self { arc })
}
#[getter]
fn Hyperbola(&self, py: Python<'_>) -> PyResult<Py<PyHyperbola>> {
Py::new(
py,
PyHyperbola {
curve: self.arc.hyperbola(),
},
)
}
#[getter]
fn Center(&self, py: Python<'_>) -> PyResult<PyObject> {
py_vector(py, self.arc.hyperbola().center())
}
#[getter]
fn Location(&self, py: Python<'_>) -> PyResult<PyObject> {
py_vector(py, self.arc.hyperbola().center())
}
#[getter]
fn Axis(&self, py: Python<'_>) -> PyResult<PyObject> {
py_vector(py, self.arc.hyperbola().normal())
}
#[getter]
fn XAxis(&self, py: Python<'_>) -> PyResult<PyObject> {
py_vector(py, self.arc.hyperbola().x_axis())
}
#[getter]
fn YAxis(&self, py: Python<'_>) -> PyResult<PyObject> {
py_vector(py, self.arc.hyperbola().y_axis())
}
#[getter]
fn MajorRadius(&self) -> f64 {
self.arc.hyperbola().major_radius()
}
#[setter]
fn set_MajorRadius(&mut self, value: f64) -> PyResult<()> {
let hyperbola = Hyperbola::with_axis(
self.arc.hyperbola().center(),
self.arc.hyperbola().normal(),
self.arc.hyperbola().x_axis(),
value,
self.arc.hyperbola().minor_radius(),
)
.map_err(curve_error)?;
self.arc = ArcOfHyperbola::new(hyperbola, self.arc.first(), self.arc.last())
.map_err(curve_error)?;
Ok(())
}
#[getter]
fn MinorRadius(&self) -> f64 {
self.arc.hyperbola().minor_radius()
}
#[setter]
fn set_MinorRadius(&mut self, value: f64) -> PyResult<()> {
let hyperbola = Hyperbola::with_axis(
self.arc.hyperbola().center(),
self.arc.hyperbola().normal(),
self.arc.hyperbola().x_axis(),
self.arc.hyperbola().major_radius(),
value,
)
.map_err(curve_error)?;
self.arc = ArcOfHyperbola::new(hyperbola, self.arc.first(), self.arc.last())
.map_err(curve_error)?;
Ok(())
}
#[getter]
fn Focal(&self) -> f64 {
self.arc.hyperbola().focal()
}
#[getter]
fn Focus1(&self, py: Python<'_>) -> PyResult<PyObject> {
py_vector(py, self.arc.hyperbola().focus1())
}
#[getter]
fn Focus2(&self, py: Python<'_>) -> PyResult<PyObject> {
py_vector(py, self.arc.hyperbola().focus2())
}
#[getter]
fn FirstParameter(&self) -> f64 {
self.arc.first()
}
#[getter]
fn LastParameter(&self) -> f64 {
self.arc.last()
}
#[getter]
fn StartPoint(&self, py: Python<'_>) -> PyResult<PyObject> {
py_vector(py, self.arc.start())
}
#[getter]
fn EndPoint(&self, py: Python<'_>) -> PyResult<PyObject> {
py_vector(py, self.arc.end())
}
fn value(&self, py: Python<'_>, u: f64) -> PyResult<PyObject> {
py_vector(py, self.arc.point_at(u))
}
#[pyo3(signature = (first = None, last = None))]
fn toNurbs(
&self,
py: Python<'_>,
first: Option<f64>,
last: Option<f64>,
) -> PyResult<Py<PyBSplineCurve>> {
let first = first.unwrap_or(self.arc.first());
let last = last.unwrap_or(self.arc.last());
let nurbs = self
.arc
.hyperbola()
.nurbs(first, last)
.map_err(curve_error)?;
spline_curve(py, &nurbs)
}
#[pyo3(signature = (first = None, last = None))]
fn toBSpline(
&self,
py: Python<'_>,
first: Option<f64>,
last: Option<f64>,
) -> PyResult<Py<PyBSplineCurve>> {
self.toNurbs(py, first, last)
}
#[pyo3(signature = (first = None, last = None))]
fn toShape(
&self,
py: Python<'_>,
first: Option<f64>,
last: Option<f64>,
) -> PyResult<Py<PyEdge>> {
let first = first.unwrap_or(self.arc.first());
let last = last.unwrap_or(self.arc.last());
let nurbs = self
.arc
.hyperbola()
.nurbs(first, last)
.map_err(curve_error)?;
spline_edge(py, &nurbs)
}
fn __repr__(&self) -> String {
format!(
"Part.ArcOfHyperbola({}, {})",
self.arc.first(),
self.arc.last()
)
}
}
#[pyclass(name = "Parabola", module = "Part")]
#[derive(Clone)]
pub struct PyParabola {
curve: Parabola,
}
#[pymethods]
impl PyParabola {
#[getter]
fn TypeId(&self) -> &'static str {
"Part::GeomParabola"
}
#[new]
#[pyo3(signature = (*args))]
fn new(args: &Bound<'_, PyTuple>) -> PyResult<Self> {
match args.len() {
0 => Ok(Self {
curve: Parabola::with_axis(
Vector3::zero(),
Vector3::new(0.0, 0.0, 1.0),
Vector3::new(1.0, 0.0, 0.0),
1.0,
)
.map_err(curve_error)?,
}),
1 => {
let other = args.get_item(0)?;
let other = other.extract::<PyRef<'_, PyParabola>>()?;
Ok(Self { curve: other.curve })
}
3 => {
let focus = vector(&args.get_item(0)?)?;
let center = vector(&args.get_item(1)?)?;
let normal = vector(&args.get_item(2)?)?;
let x_axis = focus.sub(¢er);
let focal = x_axis.length();
Ok(Self {
curve: Parabola::with_axis(center, normal, x_axis, focal)
.map_err(curve_error)?,
})
}
_ => Err(PyTypeError::new_err(
"Parabola constructor accepts:\n\
-- empty parameter list\n\
-- Parabola\n\
-- Point, Point, Point",
)),
}
}
#[getter]
fn Center(&self, py: Python<'_>) -> PyResult<PyObject> {
py_vector(py, self.curve.vertex())
}
#[getter]
fn Location(&self, py: Python<'_>) -> PyResult<PyObject> {
py_vector(py, self.curve.vertex())
}
#[getter]
fn Axis(&self, py: Python<'_>) -> PyResult<PyObject> {
py_vector(py, self.curve.normal())
}
#[getter]
fn XAxis(&self, py: Python<'_>) -> PyResult<PyObject> {
py_vector(py, self.curve.x_axis())
}
#[getter]
fn YAxis(&self, py: Python<'_>) -> PyResult<PyObject> {
py_vector(py, self.curve.y_axis())
}
#[getter]
fn Focal(&self) -> f64 {
self.curve.focal()
}
#[setter]
fn set_Focal(&mut self, value: f64) -> PyResult<()> {
self.curve = Parabola::with_axis(
self.curve.vertex(),
self.curve.normal(),
self.curve.x_axis(),
value,
)
.map_err(curve_error)?;
Ok(())
}
#[getter]
fn Focus(&self, py: Python<'_>) -> PyResult<PyObject> {
py_vector(py, self.curve.focus())
}
#[getter]
fn Parameter(&self) -> f64 {
self.curve.parameter()
}
#[getter]
fn Eccentricity(&self) -> f64 {
self.curve.eccentricity()
}
#[getter]
fn FirstParameter(&self) -> f64 {
-2.0e100
}
#[getter]
fn LastParameter(&self) -> f64 {
2.0e100
}
fn value(&self, py: Python<'_>, u: f64) -> PyResult<PyObject> {
if !u.is_finite() {
return Err(OCCError::new_err("the parabola parameter must be finite"));
}
py_vector(py, self.curve.point_at(u))
}
fn compute(
&mut self,
p1: &Bound<'_, PyAny>,
p2: &Bound<'_, PyAny>,
p3: &Bound<'_, PyAny>,
) -> PyResult<()> {
let v1 = vector(p1)?;
let v2 = vector(p2)?;
let v3 = vector(p3)?;
if v1.sub(&v2).cross(&v3.sub(&v2)).length() < 0.0001 {
return Err(OCCError::new_err("Points are collinear"));
}
let determinant = |m: [[f64; 3]; 3]| -> f64 {
m[0][0] * (m[1][1] * m[2][2] - m[1][2] * m[2][1])
- m[0][1] * (m[1][0] * m[2][2] - m[1][2] * m[2][0])
+ m[0][2] * (m[1][0] * m[2][1] - m[1][1] * m[2][0])
};
let y = [v1.y, v2.y, v3.y];
let x = [v1.x, v2.x, v3.x];
let fit = determinant([
[y[0] * y[0], y[0], 1.0],
[y[1] * y[1], y[1], 1.0],
[y[2] * y[2], y[2], 1.0],
]);
if fit.abs() <= 1e-12 {
return Err(OCCError::new_err("Points are collinear"));
}
let a = determinant([[x[0], y[0], 1.0], [x[1], y[1], 1.0], [x[2], y[2], 1.0]]) / fit;
let b = determinant([
[y[0] * y[0], x[0], 1.0],
[y[1] * y[1], x[1], 1.0],
[y[2] * y[2], x[2], 1.0],
]) / fit;
let c = determinant([
[y[0] * y[0], y[0], x[0]],
[y[1] * y[1], y[1], x[1]],
[y[2] * y[2], y[2], x[2]],
]) / fit;
if !a.is_finite() || a.abs() <= 1e-12 {
return Err(OCCError::new_err("Points are collinear"));
}
let focal = 1.0 / (4.0 * a.abs());
let vertex_y = -b / (2.0 * a);
let vertex_x = a * vertex_y * vertex_y + b * vertex_y + c;
self.curve = Parabola::with_axis(
Vector3::new(vertex_x, vertex_y, v1.z),
Vector3::new(0.0, 0.0, 1.0),
Vector3::new(1.0, 0.0, 0.0),
focal,
)
.map_err(curve_error)?;
Ok(())
}
#[pyo3(signature = (first = None, last = None))]
fn trim(
&self,
py: Python<'_>,
first: Option<f64>,
last: Option<f64>,
) -> PyResult<Py<PyArcOfParabola>> {
let (first, last) = required_range(first, last, "parabola")?;
let arc = self.curve.trim(first, last).map_err(curve_error)?;
Py::new(py, PyArcOfParabola { arc })
}
#[pyo3(signature = (first = None, last = None))]
fn toNurbs(
&self,
py: Python<'_>,
first: Option<f64>,
last: Option<f64>,
) -> PyResult<Py<PyBSplineCurve>> {
let (first, last) = required_range(first, last, "parabola")?;
let nurbs = self.curve.nurbs(first, last).map_err(curve_error)?;
spline_curve(py, &nurbs)
}
#[pyo3(signature = (first = None, last = None))]
fn toBSpline(
&self,
py: Python<'_>,
first: Option<f64>,
last: Option<f64>,
) -> PyResult<Py<PyBSplineCurve>> {
self.toNurbs(py, first, last)
}
#[pyo3(signature = (first = None, last = None))]
fn toShape(
&self,
py: Python<'_>,
first: Option<f64>,
last: Option<f64>,
) -> PyResult<Py<PyEdge>> {
let (first, last) = required_range(first, last, "parabola")?;
let nurbs = self.curve.nurbs(first, last).map_err(curve_error)?;
spline_edge(py, &nurbs)
}
fn __repr__(&self) -> String {
format!(
"Part.Parabola({:?}, {})",
self.curve.vertex(),
self.curve.focal()
)
}
}
#[pyclass(name = "ArcOfParabola", module = "Part")]
#[derive(Clone)]
pub struct PyArcOfParabola {
arc: ArcOfParabola,
}
#[pymethods]
impl PyArcOfParabola {
#[getter]
fn TypeId(&self) -> &'static str {
"Part::GeomArcOfParabola"
}
#[new]
#[pyo3(signature = (parabola, first, last, sense = true))]
fn new(
parabola: &Bound<'_, PyAny>,
first: f64,
last: f64,
sense: bool,
) -> PyResult<Self> {
let _ = sense;
let curve = parabola.extract::<PyRef<'_, PyParabola>>()?.curve;
let arc = curve.trim(first, last).map_err(curve_error)?;
Ok(Self { arc })
}
#[getter]
fn Parabola(&self, py: Python<'_>) -> PyResult<Py<PyParabola>> {
Py::new(
py,
PyParabola {
curve: self.arc.parabola(),
},
)
}
#[getter]
fn Center(&self, py: Python<'_>) -> PyResult<PyObject> {
py_vector(py, self.arc.parabola().vertex())
}
#[getter]
fn Location(&self, py: Python<'_>) -> PyResult<PyObject> {
py_vector(py, self.arc.parabola().vertex())
}
#[getter]
fn Axis(&self, py: Python<'_>) -> PyResult<PyObject> {
py_vector(py, self.arc.parabola().normal())
}
#[getter]
fn XAxis(&self, py: Python<'_>) -> PyResult<PyObject> {
py_vector(py, self.arc.parabola().x_axis())
}
#[getter]
fn YAxis(&self, py: Python<'_>) -> PyResult<PyObject> {
py_vector(py, self.arc.parabola().y_axis())
}
#[getter]
fn Focal(&self) -> f64 {
self.arc.parabola().focal()
}
#[setter]
fn set_Focal(&mut self, value: f64) -> PyResult<()> {
let parabola = Parabola::with_axis(
self.arc.parabola().vertex(),
self.arc.parabola().normal(),
self.arc.parabola().x_axis(),
value,
)
.map_err(curve_error)?;
self.arc = ArcOfParabola::new(parabola, self.arc.first(), self.arc.last())
.map_err(curve_error)?;
Ok(())
}
#[getter]
fn Focus(&self, py: Python<'_>) -> PyResult<PyObject> {
py_vector(py, self.arc.parabola().focus())
}
#[getter]
fn Parameter(&self) -> f64 {
self.arc.parabola().parameter()
}
#[getter]
fn FirstParameter(&self) -> f64 {
self.arc.first()
}
#[getter]
fn LastParameter(&self) -> f64 {
self.arc.last()
}
#[getter]
fn StartPoint(&self, py: Python<'_>) -> PyResult<PyObject> {
py_vector(py, self.arc.start())
}
#[getter]
fn EndPoint(&self, py: Python<'_>) -> PyResult<PyObject> {
py_vector(py, self.arc.end())
}
fn value(&self, py: Python<'_>, u: f64) -> PyResult<PyObject> {
py_vector(py, self.arc.point_at(u))
}
#[pyo3(signature = (first = None, last = None))]
fn toNurbs(
&self,
py: Python<'_>,
first: Option<f64>,
last: Option<f64>,
) -> PyResult<Py<PyBSplineCurve>> {
let first = first.unwrap_or(self.arc.first());
let last = last.unwrap_or(self.arc.last());
let nurbs = self
.arc
.parabola()
.nurbs(first, last)
.map_err(curve_error)?;
spline_curve(py, &nurbs)
}
#[pyo3(signature = (first = None, last = None))]
fn toBSpline(
&self,
py: Python<'_>,
first: Option<f64>,
last: Option<f64>,
) -> PyResult<Py<PyBSplineCurve>> {
self.toNurbs(py, first, last)
}
#[pyo3(signature = (first = None, last = None))]
fn toShape(
&self,
py: Python<'_>,
first: Option<f64>,
last: Option<f64>,
) -> PyResult<Py<PyEdge>> {
let first = first.unwrap_or(self.arc.first());
let last = last.unwrap_or(self.arc.last());
let nurbs = self
.arc
.parabola()
.nurbs(first, last)
.map_err(curve_error)?;
spline_edge(py, &nurbs)
}
fn __repr__(&self) -> String {
format!(
"Part.ArcOfParabola({}, {})",
self.arc.first(),
self.arc.last()
)
}
}
#[pyclass(name = "BezierCurve", module = "Part")]
pub struct PyBezierCurve {
curve: Mutex<BezierCurve>,
}
impl PyBezierCurve {
fn curve(&self) -> BezierCurve {
self.curve.lock().unwrap_or_else(|e| e.into_inner()).clone()
}
fn store(&self, curve: BezierCurve) {
*self.curve.lock().unwrap_or_else(|e| e.into_inner()) = curve;
}
}
#[pymethods]
impl PyBezierCurve {
#[getter]
fn TypeId(&self) -> &'static str {
"Part::GeomBezierCurve"
}
#[new]
#[pyo3(signature = (poles = None, weights = None))]
fn new(
poles: Option<&Bound<'_, PyAny>>,
weights: Option<&Bound<'_, PyAny>>,
) -> PyResult<Self> {
let Some(poles) = poles else {
return Ok(Self {
curve: Mutex::new(BezierCurve::default()),
});
};
if let Ok(other) = poles.extract::<PyRef<'_, PyBezierCurve>>() {
return Ok(Self {
curve: Mutex::new(other.curve()),
});
}
let points = parse_points(poles)?;
let weights = match weights {
Some(weights) => parse_floats(weights)?,
None => vec![1.0; points.len()],
};
let curve = BezierCurve::new(points, weights).map_err(curve_error)?;
Ok(Self {
curve: Mutex::new(curve),
})
}
#[getter]
fn Degree(&self) -> i32 {
self.curve().degree()
}
#[getter]
fn MaxDegree(&self) -> i32 {
25
}
#[getter]
fn NbPoles(&self) -> usize {
self.curve().poles().len()
}
#[getter]
fn StartPoint(&self, py: Python<'_>) -> PyResult<PyObject> {
py_vector(py, self.curve().start())
}
#[getter]
fn EndPoint(&self, py: Python<'_>) -> PyResult<PyObject> {
py_vector(py, self.curve().end())
}
#[getter]
fn FirstParameter(&self) -> f64 {
self.curve().first()
}
#[getter]
fn LastParameter(&self) -> f64 {
self.curve().last()
}
fn isRational(&self) -> bool {
self.curve().is_rational()
}
fn isPeriodic(&self) -> bool {
false
}
fn isClosed(&self) -> bool {
self.curve().is_closed()
}
fn getPole(&self, py: Python<'_>, index: i32) -> PyResult<PyObject> {
let curve = self.curve();
let position = index_at(curve.poles().len(), index)? as usize - 1;
py_vector(py, curve.poles()[position])
}
fn getPoles(&self, py: Python<'_>) -> PyResult<Vec<PyObject>> {
let curve = self.curve();
curve
.poles()
.iter()
.map(|pole| py_vector(py, *pole))
.collect()
}
#[pyo3(signature = (index, pole, weight = None))]
fn setPole(
&self,
index: i32,
pole: &Bound<'_, PyAny>,
weight: Option<f64>,
) -> PyResult<()> {
let mut curve = self.curve();
let position = index_at(curve.poles().len(), index)? as usize - 1;
let weight = weight.filter(|weight| *weight >= 0.0);
curve
.set_pole(position, vector(pole)?, weight)
.map_err(curve_error)?;
self.store(curve);
Ok(())
}
fn setPoles(&self, poles: &Bound<'_, PyAny>) -> PyResult<()> {
let mut curve = self.curve();
curve.set_poles(parse_points(poles)?).map_err(curve_error)?;
self.store(curve);
Ok(())
}
fn getWeight(&self, index: i32) -> PyResult<f64> {
let curve = self.curve();
let position = index_at(curve.weights().len(), index)? as usize - 1;
Ok(curve.weights()[position])
}
fn setWeight(&self, index: i32, weight: f64) -> PyResult<()> {
let mut curve = self.curve();
let position = index_at(curve.weights().len(), index)? as usize - 1;
curve.set_weight(position, weight).map_err(curve_error)?;
self.store(curve);
Ok(())
}
fn getWeights(&self) -> Vec<f64> {
self.curve().weights().to_vec()
}
fn value(&self, py: Python<'_>, u: f64) -> PyResult<PyObject> {
if !u.is_finite() {
return Err(OCCError::new_err("the Bezier parameter must be finite"));
}
py_vector(py, self.curve().point_at(u))
}
#[pyo3(signature = (first = None, last = None))]
fn trim(
&self,
py: Python<'_>,
first: Option<f64>,
last: Option<f64>,
) -> PyResult<Py<PyBezierCurve>> {
let curve = self.curve();
let first = first.unwrap_or(curve.first());
let last = last.unwrap_or(curve.last());
let part = curve.trim(first, last).map_err(curve_error)?;
Py::new(
py,
PyBezierCurve {
curve: Mutex::new(part),
},
)
}
fn segment(&self, first: f64, last: f64) -> PyResult<()> {
let part = self.curve().trim(first, last).map_err(curve_error)?;
self.store(part);
Ok(())
}
#[pyo3(signature = (first = None, last = None))]
fn toNurbs(
&self,
py: Python<'_>,
first: Option<f64>,
last: Option<f64>,
) -> PyResult<Py<PyBSplineCurve>> {
let curve = self.curve();
let first = first.unwrap_or(curve.first());
let last = last.unwrap_or(curve.last());
let part = curve.trim(first, last).map_err(curve_error)?;
spline_curve(py, &part.nurbs())
}
#[pyo3(signature = (first = None, last = None))]
fn toBSpline(
&self,
py: Python<'_>,
first: Option<f64>,
last: Option<f64>,
) -> PyResult<Py<PyBSplineCurve>> {
self.toNurbs(py, first, last)
}
#[pyo3(signature = (first = None, last = None))]
fn toShape(
&self,
py: Python<'_>,
first: Option<f64>,
last: Option<f64>,
) -> PyResult<Py<PyEdge>> {
let curve = self.curve();
let first = first.unwrap_or(curve.first());
let last = last.unwrap_or(curve.last());
let part = curve.trim(first, last).map_err(curve_error)?;
spline_edge(py, &part.nurbs())
}
fn __repr__(&self) -> String {
let curve = self.curve();
format!(
"Part.BezierCurve({} poles, degree {})",
curve.poles().len(),
curve.degree()
)
}
}
#[pyclass(name = "Plane", module = "Part")]
#[derive(Clone)]
pub struct PyPlane {
location: Vector3,
normal: Vector3,
}
impl PyPlane {
pub(crate) fn from_parts(origin: Vector3, normal: Vector3) -> Self {
Self {
location: origin,
normal: normal.normalize(),
}
}
}
#[pymethods]
impl PyPlane {
#[getter]
fn TypeId(&self) -> &'static str {
"Part::GeomPlane"
}
#[new]
#[pyo3(signature = (origin = None, normal = None))]
fn new(origin: Option<&Bound<'_, PyAny>>, normal: Option<&Bound<'_, PyAny>>) -> PyResult<Self> {
let location = origin.map(vector).transpose()?.unwrap_or_else(Vector3::zero);
let normal = normal
.map(vector)
.transpose()?
.unwrap_or_else(|| Vector3::new(0.0, 0.0, 1.0));
if normal.is_null() {
return Err(OCCError::new_err("Part.Plane needs a non-zero normal"));
}
Ok(Self {
location,
normal: normal.normalize(),
})
}
#[getter]
fn Location(&self, py: Python<'_>) -> PyResult<PyObject> {
py_vector(py, self.location)
}
#[getter]
fn Axis(&self, py: Python<'_>) -> PyResult<PyObject> {
py_vector(py, self.normal)
}
fn intersect(&self, py: Python<'_>, other: PyRef<'_, PyPlane>) -> PyResult<Vec<Py<PyLine>>> {
let cross = self.normal.cross(&other.normal);
if cross.is_null() {
return Ok(Vec::new());
}
let d1 = self.normal.dot(&self.location);
let d2 = other.normal.dot(&other.location);
let point = other
.normal
.cross(&cross)
.scale(d1)
.add(&cross.cross(&self.normal).scale(d2))
.scale(1.0 / cross.sqr());
let line = Line::new(point, cross).map_err(curve_error)?;
Ok(vec![Py::new(py, PyLine { curve: line })?])
}
#[pyo3(signature = (u1 = None, u2 = None, v1 = None, v2 = None))]
fn toShape(
&self,
py: Python<'_>,
u1: Option<f64>,
u2: Option<f64>,
v1: Option<f64>,
v2: Option<f64>,
) -> PyResult<Py<PyFace>> {
let (Some(u1), Some(u2), Some(v1), Some(v2)) = (u1, u2, v1, v2) else {
return Err(OCCError::new_err(
"Part.Plane.toShape() needs the finite parameter rectangle \
(u1, u2, v1, v2); an infinite plane has no natural bounds",
));
};
plane_face(py, self, u1, u2, v1, v2)
}
fn __repr__(&self) -> String {
format!("Part.Plane({:?}, {:?})", self.location, self.normal)
}
}
#[pyclass(name = "Cylinder", module = "Part")]
#[derive(Clone)]
pub struct PyCylinder {
radius: f64,
center: Vector3,
axis: Vector3,
}
#[pymethods]
impl PyCylinder {
#[getter]
fn TypeId(&self) -> &'static str {
"Part::GeomCylinder"
}
#[new]
#[pyo3(signature = (radius = 1.0, center = None, axis = None))]
fn new(
radius: f64,
center: Option<&Bound<'_, PyAny>>,
axis: Option<&Bound<'_, PyAny>>,
) -> PyResult<Self> {
if !(radius > 0.0) {
return Err(OCCError::new_err("Part.Cylinder needs a positive radius"));
}
let center = center.map(vector).transpose()?.unwrap_or_else(Vector3::zero);
let axis = axis
.map(vector)
.transpose()?
.unwrap_or_else(|| Vector3::new(0.0, 0.0, 1.0));
if axis.is_null() {
return Err(OCCError::new_err("Part.Cylinder needs a non-zero axis"));
}
Ok(Self {
radius,
center,
axis: axis.normalize(),
})
}
#[getter]
fn Radius(&self) -> f64 {
self.radius
}
#[getter]
fn Center(&self, py: Python<'_>) -> PyResult<PyObject> {
py_vector(py, self.center)
}
#[getter]
fn Axis(&self, py: Python<'_>) -> PyResult<PyObject> {
py_vector(py, self.axis)
}
fn __repr__(&self) -> String {
format!(
"Part.Cylinder({}, {:?}, {:?})",
self.radius, self.center, self.axis
)
}
}
#[pyclass(name = "Sphere", module = "Part")]
#[derive(Clone)]
pub struct PySphere {
radius: f64,
center: Vector3,
axis: Vector3,
}
#[pymethods]
impl PySphere {
#[getter]
fn TypeId(&self) -> &'static str {
"Part::GeomSphere"
}
#[new]
#[pyo3(signature = (radius = 1.0, center = None, axis = None))]
fn new(
radius: f64,
center: Option<&Bound<'_, PyAny>>,
axis: Option<&Bound<'_, PyAny>>,
) -> PyResult<Self> {
if !(radius > 0.0) {
return Err(OCCError::new_err("Part.Sphere needs a positive radius"));
}
let center = center.map(vector).transpose()?.unwrap_or_else(Vector3::zero);
let axis = axis
.map(vector)
.transpose()?
.unwrap_or_else(|| Vector3::new(0.0, 0.0, 1.0));
if axis.is_null() {
return Err(OCCError::new_err("Part.Sphere needs a non-zero axis"));
}
Ok(Self {
radius,
center,
axis: axis.normalize(),
})
}
#[getter]
fn Radius(&self) -> f64 {
self.radius
}
#[getter]
fn Center(&self, py: Python<'_>) -> PyResult<PyObject> {
py_vector(py, self.center)
}
#[getter]
fn Axis(&self, py: Python<'_>) -> PyResult<PyObject> {
py_vector(py, self.axis)
}
fn __repr__(&self) -> String {
format!(
"Part.Sphere({}, {:?}, {:?})",
self.radius, self.center, self.axis
)
}
}
#[derive(Clone)]
enum TrimmedBasis {
Plane(PyPlane),
Cylinder(PyCylinder),
Sphere(PySphere),
}
#[pyclass(name = "RectangularTrimmedSurface", module = "Part")]
#[derive(Clone)]
pub struct PyRectangularTrimmedSurface {
basis: TrimmedBasis,
u1: f64,
u2: f64,
v1: f64,
v2: f64,
}
#[pymethods]
impl PyRectangularTrimmedSurface {
#[getter]
fn TypeId(&self) -> &'static str {
"Part::GeomRectangularTrimmedSurface"
}
#[new]
fn new(
surface: &Bound<'_, PyAny>,
u1: f64,
u2: f64,
v1: f64,
v2: f64,
) -> PyResult<Self> {
let basis = if let Ok(plane) = surface.downcast::<PyPlane>() {
TrimmedBasis::Plane(plane.borrow().clone())
} else if let Ok(cylinder) = surface.downcast::<PyCylinder>() {
TrimmedBasis::Cylinder(cylinder.borrow().clone())
} else if let Ok(sphere) = surface.downcast::<PySphere>() {
TrimmedBasis::Sphere(sphere.borrow().clone())
} else {
return Err(PyTypeError::new_err(
"Part.RectangularTrimmedSurface expects a Part surface \
(Part.Plane, Part.Cylinder or Part.Sphere)",
));
};
if !(u1 < u2) || !(v1 < v2) {
return Err(OCCError::new_err(
"Part.RectangularTrimmedSurface needs u1 < u2 and v1 < v2",
));
}
Ok(Self {
basis,
u1,
u2,
v1,
v2,
})
}
#[getter]
fn BasisSurface(&self, py: Python<'_>) -> PyResult<PyObject> {
Ok(match &self.basis {
TrimmedBasis::Plane(plane) => Py::new(py, plane.clone())?.into_any(),
TrimmedBasis::Cylinder(cylinder) => Py::new(py, cylinder.clone())?.into_any(),
TrimmedBasis::Sphere(sphere) => Py::new(py, sphere.clone())?.into_any(),
})
}
#[getter]
fn U1(&self) -> f64 {
self.u1
}
#[getter]
fn U2(&self) -> f64 {
self.u2
}
#[getter]
fn V1(&self) -> f64 {
self.v1
}
#[getter]
fn V2(&self) -> f64 {
self.v2
}
fn toShape(&self, py: Python<'_>) -> PyResult<Py<PyFace>> {
let TrimmedBasis::Plane(plane) = &self.basis else {
return Err(OCCError::new_err(
"Part.RectangularTrimmedSurface.toShape() only builds a face for a \
Part.Plane basis in this slice (a trimmed cylinder or sphere has no \
B-Rep builder yet)",
));
};
plane_face(py, plane, self.u1, self.u2, self.v1, self.v2)
}
fn __repr__(&self) -> String {
format!(
"Part.RectangularTrimmedSurface({}, {}, {}, {}, {})",
self.basis_name(),
self.u1,
self.u2,
self.v1,
self.v2
)
}
}
impl PyRectangularTrimmedSurface {
fn basis_name(&self) -> &'static str {
match &self.basis {
TrimmedBasis::Plane(_) => "Part.Plane()",
TrimmedBasis::Cylinder(_) => "Part.Cylinder()",
TrimmedBasis::Sphere(_) => "Part.Sphere()",
}
}
}
fn plane_frame(normal: &Vector3) -> (Vector3, Vector3) {
let reference = if normal.x.abs() < 0.9 {
Vector3::new(1.0, 0.0, 0.0)
} else {
Vector3::new(0.0, 1.0, 0.0)
};
let u = reference
.sub(&normal.scale(normal.dot(&reference)))
.normalize();
(u, normal.cross(&u))
}
fn plane_face(
py: Python<'_>,
plane: &PyPlane,
u1: f64,
u2: f64,
v1: f64,
v2: f64,
) -> PyResult<Py<PyFace>> {
if !(u1 < u2) || !(v1 < v2) || ![u1, u2, v1, v2].iter().all(|value| value.is_finite()) {
return Err(OCCError::new_err(
"a planar face needs a finite parameter rectangle with u1 < u2 and v1 < v2",
));
}
let (u_dir, v_dir) = plane_frame(&plane.normal);
let corner = |u: f64, v: f64| {
plane
.location
.add(&u_dir.scale(u))
.add(&v_dir.scale(v))
};
let corners = [
corner(u1, v1),
corner(u2, v1),
corner(u2, v2),
corner(u1, v2),
];
let mut edges = Vec::with_capacity(4);
for index in 0..4 {
let segment = LineSegment::new(corners[index], corners[(index + 1) % 4])
.map_err(curve_error)?;
edges.push(shape_from_curve(&EdgeCurve::LineSegment(segment))?);
}
let wire = backend().make_wire(&edges).map_err(kernel_error)?;
let face = backend().make_face(&[wire]).map_err(kernel_error)?;
Py::new(py, PyFace::initializer(face))
}
#[pyclass(name = "GeomSurface", module = "Part")]
pub struct PySurface {
type_id: String,
}
impl PySurface {
pub(crate) fn new(type_id: String) -> Self {
Self { type_id }
}
}
#[pymethods]
impl PySurface {
#[getter]
fn TypeId(&self) -> String {
self.type_id.clone()
}
fn __repr__(&self) -> String {
format!("Part.{}()", self.type_id.trim_start_matches("Part::"))
}
}
#[pyclass(name = "BSplineCurve", module = "Part")]
pub struct PyBSplineCurve {
state: Mutex<Option<SplineState>>,
}
#[derive(Clone)]
struct SplineState {
degree: i32,
max_degree: i32,
periodic: bool,
rational: bool,
start: Vector3,
end: Vector3,
first_parameter: f64,
last_parameter: f64,
first_u_knot: i32,
last_u_knot: i32,
continuity: i32,
knot_sequence: Vec<f64>,
knots: Vec<f64>,
mults: Vec<i32>,
poles: Vec<Vector3>,
weights: Vec<f64>,
}
struct StateReader<'a> {
data: &'a [f64],
at: usize,
}
impl<'a> StateReader<'a> {
fn new(data: &'a [f64]) -> Self {
Self { data, at: 0 }
}
fn number(&mut self) -> Option<f64> {
let value = *self.data.get(self.at)?;
self.at += 1;
Some(value)
}
fn integer(&mut self) -> Option<i32> {
Some(self.number()? as i32)
}
fn count(&mut self) -> Option<usize> {
Some(self.integer()?.max(0) as usize)
}
fn point(&mut self) -> Option<Vector3> {
Some(Vector3::new(self.number()?, self.number()?, self.number()?))
}
fn numbers(&mut self, count: usize) -> Option<Vec<f64>> {
let end = self.at.checked_add(count)?;
let values = self.data.get(self.at..end)?;
self.at = end;
Some(values.to_vec())
}
}
impl SplineState {
fn from_blob(blob: &[f64]) -> Option<Self> {
let mut reader = StateReader::new(blob);
let degree = reader.integer()?;
let max_degree = reader.integer()?;
let _nb_poles = reader.integer()?;
let _nb_knots = reader.integer()?;
let periodic = reader.number()? != 0.0;
let rational = reader.number()? != 0.0;
let start = reader.point()?;
let end = reader.point()?;
let first_parameter = reader.number()?;
let last_parameter = reader.number()?;
let first_u_knot = reader.integer()?;
let last_u_knot = reader.integer()?;
let continuity = reader.integer()?;
let sequence_length = reader.count()?;
let knot_sequence = reader.numbers(sequence_length)?;
let knot_count = reader.count()?;
let knots = reader.numbers(knot_count)?;
let mult_count = reader.count()?;
let mults = reader
.numbers(mult_count)?
.into_iter()
.map(|value| value as i32)
.collect();
let pole_count = reader.count()?;
let mut poles = Vec::with_capacity(pole_count);
for _ in 0..pole_count {
poles.push(reader.point()?);
}
let weight_count = reader.count()?;
let weights = reader.numbers(weight_count)?;
Some(Self {
degree,
max_degree,
periodic,
rational,
start,
end,
first_parameter,
last_parameter,
first_u_knot,
last_u_knot,
continuity,
knot_sequence,
knots,
mults,
poles,
weights,
})
}
fn check_rational(&self) -> bool {
!self.rational
}
fn continuity_name(&self) -> &'static str {
match self.continuity {
0 => "C0",
1 => "G1",
2 => "C1",
3 => "G2",
4 => "C2",
5 => "C3",
_ => "CN",
}
}
}
fn occt() -> ferrocad_occt::OcctBackend {
ferrocad_occt::OcctBackend::new()
}
enum Conic {
Circle(Circle),
Ellipse(Ellipse),
}
impl Conic {
fn frame(&self) -> (i32, Vector3, Vector3, Vector3, f64, f64) {
match self {
Conic::Circle(circle) => (
2,
circle.center(),
circle.normal(),
circle.x_axis(),
circle.radius(),
circle.radius(),
),
Conic::Ellipse(ellipse) => (
3,
ellipse.center(),
ellipse.normal(),
ellipse.x_axis(),
ellipse.major_radius(),
ellipse.minor_radius(),
),
}
}
}
fn is_full_conic(first: f64, last: f64) -> bool {
first == 0.0 && last == std::f64::consts::TAU
}
fn full_conic_nurbs(conic: &Conic) -> PyResult<SplineState> {
let (_, center, normal, x_axis, major, minor) = conic.frame();
let y_axis = normal.cross(&x_axis);
let local = [
(major, 0.0),
(major, 2.0 * minor),
(-major, 2.0 * minor),
(-major, 0.0),
(-major, -2.0 * minor),
(major, -2.0 * minor),
(major, 0.0),
];
let poles: Vec<Vector3> = local
.iter()
.map(|(x, y)| center.add(&x_axis.scale(*x)).add(&y_axis.scale(*y)))
.collect();
let weights = vec![3.0, 1.0, 1.0, 3.0, 1.0, 1.0, 3.0];
let (knots, mults) = match conic {
Conic::Circle(_) => (
vec![0.0, std::f64::consts::PI, std::f64::consts::TAU],
vec![4, 3, 4],
),
Conic::Ellipse(_) => (vec![0.0, 1.0, 2.0], vec![4, 3, 4]),
};
let blob = occt()
.bspline_state(&poles, &weights, &knots, &mults, 3, false, true)
.ok_or_else(|| OCCError::new_err("conversion to a NURBS failed"))?;
SplineState::from_blob(&blob)
.ok_or_else(|| OCCError::new_err("conversion to a NURBS failed"))
}
fn conic_spline(
py: Python<'_>,
conic: &Conic,
first: f64,
last: f64,
nurbs: bool,
) -> PyResult<Py<PyBSplineCurve>> {
if nurbs && is_full_conic(first, last) {
return Py::new(py, PyBSplineCurve::from_state(full_conic_nurbs(conic)?));
}
let (kind, center, normal, x_axis, major, minor) = conic.frame();
let blob = occt()
.conic_spline_state(kind, center, normal, x_axis, major, minor, first, last, nurbs)
.ok_or_else(|| OCCError::new_err("conversion to a B-spline failed"))?;
let state = SplineState::from_blob(&blob)
.ok_or_else(|| OCCError::new_err("conversion to a B-spline failed"))?;
Py::new(py, PyBSplineCurve::from_state(state))
}
fn nurbs_state(curve: &NurbsCurve) -> PyResult<SplineState> {
let blob = occt()
.bspline_state(
curve.poles(),
curve.weights(),
curve.knots(),
curve.mults(),
curve.degree(),
false,
!curve.is_rational(),
)
.ok_or_else(|| OCCError::new_err("conversion to a B-spline failed"))?;
SplineState::from_blob(&blob).ok_or_else(|| OCCError::new_err("conversion to a B-spline failed"))
}
fn spline_curve(py: Python<'_>, curve: &NurbsCurve) -> PyResult<Py<PyBSplineCurve>> {
Py::new(py, PyBSplineCurve::from_state(nurbs_state(curve)?))
}
fn spline_edge(py: Python<'_>, curve: &NurbsCurve) -> PyResult<Py<PyEdge>> {
let state = nurbs_state(curve)?;
let shape = occt()
.make_bspline_poles_mults_knots(
&state.poles,
&state.weights,
&state.knots,
&state.mults,
state.degree,
state.periodic,
state.check_rational(),
)
.map_err(kernel_error)?;
Py::new(py, PyEdge::from_bspline(shape, state))
}
fn required_range(first: Option<f64>, last: Option<f64>, what: &str) -> PyResult<(f64, f64)> {
match (first, last) {
(Some(first), Some(last)) => Ok((first, last)),
_ => Err(OCCError::new_err(format!(
"a {what} is unbounded; pass a finite parameter range"
))),
}
}
fn parse_points(points: &Bound<'_, PyAny>) -> PyResult<Vec<Vector3>> {
let mut parsed = Vec::new();
for point in points.try_iter()? {
parsed.push(vector(&point?)?);
}
Ok(parsed)
}
fn parse_floats(values: &Bound<'_, PyAny>) -> PyResult<Vec<f64>> {
let mut parsed = Vec::new();
for value in values.try_iter()? {
parsed.push(value?.extract::<f64>()?);
}
Ok(parsed)
}
fn parse_integers(values: &Bound<'_, PyAny>) -> PyResult<Vec<i32>> {
let mut parsed = Vec::new();
for value in values.try_iter()? {
parsed.push(value?.extract::<i32>()?);
}
Ok(parsed)
}
fn build_spline(
poles: &Bound<'_, PyAny>,
mults: Option<&Bound<'_, PyAny>>,
knots: Option<&Bound<'_, PyAny>>,
periodic: bool,
mut degree: i32,
weights: Option<&Bound<'_, PyAny>>,
check_rational: bool,
) -> PyResult<SplineState> {
let poles = parse_points(poles)?;
if poles.len() < 2 {
return Err(OCCError::new_err("need two or more poles"));
}
let given_mults = mults.map(parse_integers).transpose()?;
let given_knots = knots.map(parse_floats).transpose()?;
let knot_count = match (&given_mults, &given_knots) {
(Some(mults), Some(knots)) => {
if mults.len() != knots.len() {
return Err(OCCError::new_err("number of knots and mults mismatch"));
}
mults.len()
}
(Some(mults), None) => mults.len(),
(None, Some(knots)) => knots.len(),
(None, None) => {
let count = poles.len() as i32;
if periodic {
if count < degree {
degree = count;
}
(count + 1) as usize
}
else {
if count <= degree {
degree = count - 1;
}
(count - degree + 1) as usize
}
}
};
if knot_count == 0 || degree < 1 {
return Err(OCCError::new_err("failed to create spline"));
}
let mults = match given_mults {
Some(mults) => mults,
None => {
let mut mults = vec![1; knot_count];
if !periodic {
mults[0] = degree + 1;
mults[knot_count - 1] = degree + 1;
}
mults
}
};
let sum_of_mults: i32 = if periodic {
mults[..mults.len().saturating_sub(1)].iter().sum()
}
else {
mults.iter().sum()
};
if mults.len() >= 3 {
for multiplicity in &mults[1..mults.len() - 1] {
if *multiplicity > degree {
return Err(OCCError::new_err(
"multiplicity of inner knot higher than degree",
));
}
}
}
let knots = match given_knots {
Some(knots) => knots,
None => {
if knot_count == 1 {
vec![0.0]
}
else {
(0..knot_count)
.map(|index| index as f64 / (knot_count - 1) as f64)
.collect()
}
}
};
let weights = match weights.map(parse_floats).transpose()? {
Some(weights) => {
if weights.len() != poles.len() {
return Err(OCCError::new_err("number of poles and weights mismatch"));
}
weights
}
None => vec![1.0; poles.len()],
};
let pole_count = poles.len() as i32;
let consistent = if periodic {
sum_of_mults == pole_count
}
else {
sum_of_mults - degree - 1 == pole_count
};
if !consistent {
return Err(OCCError::new_err("number of poles and sum of mults mismatch"));
}
let blob = occt()
.bspline_state(&poles, &weights, &knots, &mults, degree, periodic, check_rational)
.ok_or_else(|| OCCError::new_err("failed to create spline"))?;
SplineState::from_blob(&blob).ok_or_else(|| OCCError::new_err("failed to create spline"))
}
#[pymethods]
impl PyBSplineCurve {
#[getter]
fn TypeId(&self) -> &'static str {
"Part::GeomBSplineCurve"
}
#[new]
#[pyo3(signature = (*args))]
fn new(args: &Bound<'_, PyTuple>) -> PyResult<Self> {
if args.len() == 0 {
return Ok(Self {
state: Mutex::new(None),
});
}
if args.len() > 7 {
return Err(PyTypeError::new_err(
"B-spline constructor accepts:\n\
-- poles, [mults, knots, periodic, degree, weights, CheckRational]\n\
-- empty parameter list",
));
}
let optional = |index: usize| -> PyResult<Option<Bound<'_, PyAny>>> {
match args.get_item(index) {
Ok(value) if !value.is_none() => Ok(Some(value)),
_ => Ok(None),
}
};
let poles = args.get_item(0)?;
let mults = optional(1)?;
let knots = optional(2)?;
let periodic = args
.get_item(3)
.ok()
.and_then(|value| value.extract::<bool>().ok())
.unwrap_or(false);
let degree = args
.get_item(4)
.ok()
.and_then(|value| value.extract::<i32>().ok())
.unwrap_or(3);
let weights = optional(5)?;
let check_rational = args
.get_item(6)
.ok()
.and_then(|value| value.extract::<bool>().ok())
.unwrap_or(true);
let state = build_spline(
&poles,
mults.as_ref(),
knots.as_ref(),
periodic,
degree,
weights.as_ref(),
check_rational,
)?;
Ok(Self {
state: Mutex::new(Some(state)),
})
}
#[pyo3(signature = (poles, mults = None, knots = None, periodic = false, degree = 3, weights = None, CheckRational = true))]
fn buildFromPolesMultsKnots(
&self,
poles: &Bound<'_, PyAny>,
mults: Option<&Bound<'_, PyAny>>,
knots: Option<&Bound<'_, PyAny>>,
periodic: bool,
degree: i32,
weights: Option<&Bound<'_, PyAny>>,
CheckRational: bool,
) -> PyResult<()> {
let state = build_spline(poles, mults, knots, periodic, degree, weights, CheckRational)?;
*self.state.lock().unwrap_or_else(|e| e.into_inner()) = Some(state);
Ok(())
}
#[pyo3(signature = (poles, periodic = false, degree = 3, interpolate = false))]
fn buildFromPoles(
&self,
poles: &Bound<'_, PyAny>,
periodic: bool,
degree: i32,
interpolate: bool,
) -> PyResult<()> {
let parsed = parse_points(poles)?;
if parsed.len() < 2 {
return Err(OCCError::new_err("a B-spline needs at least two poles"));
}
let degree = degree.min(parsed.len() as i32 - 1).max(1);
let mult = if periodic {
if interpolate {
degree
}
else {
1
}
}
else {
degree + 1
};
let knot_count = if periodic {
if interpolate {
parsed.len() as i32 - degree + 2
}
else {
parsed.len() as i32 + 1
}
}
else {
parsed.len() as i32 - degree + 1
};
let knot_count = knot_count.max(2) as usize;
let knots: Vec<f64> = (0..knot_count)
.map(|index| index as f64 / (knot_count - 1) as f64)
.collect();
let mut mults = vec![1; knot_count];
mults[0] = mult;
mults[knot_count - 1] = mult;
let weights = vec![1.0; parsed.len()];
let blob = occt()
.bspline_state(
&parsed,
&weights,
&knots,
&mults,
degree,
periodic,
true,
)
.ok_or_else(|| OCCError::new_err("failed to create spline"))?;
let state =
SplineState::from_blob(&blob).ok_or_else(|| OCCError::new_err("failed to create spline"))?;
*self.state.lock().unwrap_or_else(|e| e.into_inner()) = Some(state);
Ok(())
}
#[pyo3(signature = (points, PeriodicFlag = false, Tolerance = 1e-7))]
fn interpolate(
&self,
points: &Bound<'_, PyAny>,
PeriodicFlag: bool,
Tolerance: f64,
) -> PyResult<()> {
let _ = Tolerance;
let parsed = parse_points(points)?;
if parsed.len() < 2 {
return Err(OCCError::new_err(
"a B-spline needs at least two interpolation points",
));
}
let blob = occt()
.bspline_interpolated_state(&parsed, PeriodicFlag)
.ok_or_else(|| OCCError::new_err("failed to interpolate the B-spline"))?;
let state = SplineState::from_blob(&blob)
.ok_or_else(|| OCCError::new_err("failed to interpolate the B-spline"))?;
*self.state.lock().unwrap_or_else(|e| e.into_inner()) = Some(state);
Ok(())
}
fn toShape(&self, py: Python<'_>) -> PyResult<Py<PyEdge>> {
let state = self.state()?;
let shape = occt()
.make_bspline_poles_mults_knots(
&state.poles,
&state.weights,
&state.knots,
&state.mults,
state.degree,
state.periodic,
state.check_rational(),
)
.map_err(kernel_error)?;
Py::new(
py,
PyEdge::from_entry(EdgeEntry {
shape,
curve: None,
}),
)
}
fn value(&self, py: Python<'_>, u: f64) -> PyResult<PyObject> {
let state = self.state()?;
let point = occt()
.bspline_point(
&state.poles,
&state.weights,
&state.knots,
&state.mults,
state.degree,
state.periodic,
state.check_rational(),
u,
)
.ok_or_else(|| OCCError::new_err("failed to evaluate the B-spline"))?;
py_vector(py, point)
}
#[getter]
fn Degree(&self) -> PyResult<i32> {
Ok(self.state()?.degree)
}
#[getter]
fn MaxDegree(&self) -> PyResult<i32> {
Ok(self.state()?.max_degree)
}
#[getter]
fn NbPoles(&self) -> PyResult<usize> {
Ok(self.state()?.poles.len())
}
#[getter]
fn NbKnots(&self) -> PyResult<usize> {
Ok(self.state()?.knots.len())
}
#[getter]
fn StartPoint(&self, py: Python<'_>) -> PyResult<PyObject> {
py_vector(py, self.state()?.start)
}
#[getter]
fn EndPoint(&self, py: Python<'_>) -> PyResult<PyObject> {
py_vector(py, self.state()?.end)
}
#[getter]
fn FirstParameter(&self) -> PyResult<f64> {
Ok(self.state()?.first_parameter)
}
#[getter]
fn LastParameter(&self) -> PyResult<f64> {
Ok(self.state()?.last_parameter)
}
#[getter]
fn FirstUKnotIndex(&self) -> PyResult<i32> {
Ok(self.state()?.first_u_knot)
}
#[getter]
fn LastUKnotIndex(&self) -> PyResult<i32> {
Ok(self.state()?.last_u_knot)
}
#[getter]
fn KnotSequence(&self) -> PyResult<Vec<f64>> {
Ok(self.state()?.knot_sequence)
}
#[getter]
fn Continuity(&self) -> PyResult<&'static str> {
Ok(self.state()?.continuity_name())
}
fn isPeriodic(&self) -> PyResult<bool> {
Ok(self.state()?.periodic)
}
fn isRational(&self) -> PyResult<bool> {
Ok(self.state()?.rational)
}
fn getKnot(&self, index: i32) -> PyResult<f64> {
let state = self.state()?;
knot_at(&state, index)
}
fn getKnots(&self) -> PyResult<Vec<f64>> {
Ok(self.state()?.knots)
}
fn getMultiplicity(&self, index: i32) -> PyResult<i32> {
let state = self.state()?;
let position = index_at(state.mults.len(), index)? as usize;
Ok(state.mults[position - 1])
}
fn getMultiplicities(&self) -> PyResult<Vec<i32>> {
Ok(self.state()?.mults)
}
fn getPole(&self, py: Python<'_>, index: i32) -> PyResult<PyObject> {
let state = self.state()?;
let position = index_at(state.poles.len(), index)? as usize - 1;
py_vector(py, state.poles[position])
}
fn getPoles(&self, py: Python<'_>) -> PyResult<Vec<PyObject>> {
let state = self.state()?;
state
.poles
.iter()
.map(|pole| py_vector(py, *pole))
.collect()
}
fn getPolesAndWeights(&self) -> PyResult<Vec<(f64, f64, f64, f64)>> {
let state = self.state()?;
Ok(state
.poles
.iter()
.zip(state.weights.iter())
.map(|(pole, weight)| (pole.x, pole.y, pole.z, *weight))
.collect())
}
fn getWeight(&self, index: i32) -> PyResult<f64> {
let state = self.state()?;
let position = index_at(state.weights.len(), index)? as usize - 1;
Ok(state.weights[position])
}
fn getWeights(&self) -> PyResult<Vec<f64>> {
Ok(self.state()?.weights)
}
fn getResolution(&self, tolerance: f64) -> PyResult<f64> {
let state = self.state()?;
occt()
.bspline_resolution(
&state.poles,
&state.weights,
&state.knots,
&state.mults,
state.degree,
state.periodic,
state.check_rational(),
tolerance,
)
.ok_or_else(|| OCCError::new_err("failed to compute the spline resolution"))
}
#[pyo3(signature = (index, value, multiplicity = None))]
fn setKnot(&self, index: i32, value: f64, multiplicity: Option<i32>) -> PyResult<()> {
let mut state = self.state()?;
let position = index_at(state.knots.len(), index)? as usize - 1;
if position > 0 && value <= state.knots[position - 1] {
return Err(OCCError::new_err("knot value is not increasing"));
}
if position + 1 < state.knots.len() && value >= state.knots[position + 1] {
return Err(OCCError::new_err("knot value is not increasing"));
}
state.knots[position] = value;
if let Some(multiplicity) = multiplicity {
if multiplicity < state.mults[position] || multiplicity > state.degree {
return Err(OCCError::new_err("invalid knot multiplicity"));
}
state.mults[position] = multiplicity;
}
*self.state.lock().unwrap_or_else(|e| e.into_inner()) = Some(state);
Ok(())
}
#[pyo3(signature = (index, point, weight = None))]
fn setPole(
&self,
index: i32,
point: &Bound<'_, PyAny>,
weight: Option<f64>,
) -> PyResult<()> {
let mut state = self.state()?;
let position = index_at(state.poles.len(), index)? as usize - 1;
state.poles[position] = vector(point)?;
if let Some(weight) = weight {
if !(weight > 0.0) {
return Err(OCCError::new_err("weight must be positive"));
}
state.weights[position] = weight;
state.rational = true;
}
*self.state.lock().unwrap_or_else(|e| e.into_inner()) = Some(state);
Ok(())
}
fn setPeriodic(&self) -> PyResult<()> {
self.set_periodicity(true)
}
fn setNotPeriodic(&self) -> PyResult<()> {
self.set_periodicity(false)
}
fn __copy__(&self) -> Self {
Self {
state: Mutex::new(self.state.lock().unwrap_or_else(|e| e.into_inner()).clone()),
}
}
fn __repr__(&self) -> String {
match &*self.state.lock().unwrap_or_else(|e| e.into_inner()) {
Some(state) => format!(
"Part.BSplineCurve({} poles, degree {})",
state.poles.len(),
state.degree
),
None => "Part.BSplineCurve()".to_string(),
}
}
}
impl PyBSplineCurve {
fn from_state(state: SplineState) -> Self {
Self {
state: Mutex::new(Some(state)),
}
}
fn state(&self) -> PyResult<SplineState> {
self.state
.lock()
.unwrap_or_else(|e| e.into_inner())
.clone()
.ok_or_else(|| OCCError::new_err("the B-spline is empty"))
}
fn set_periodicity(&self, target: bool) -> PyResult<()> {
let state = self.state()?;
let blob = occt()
.bspline_periodic_state(
&state.poles,
&state.weights,
&state.knots,
&state.mults,
state.degree,
state.periodic,
state.check_rational(),
target,
)
.ok_or_else(|| OCCError::new_err("failed to change the spline's periodicity"))?;
let state = SplineState::from_blob(&blob)
.ok_or_else(|| OCCError::new_err("failed to change the spline's periodicity"))?;
*self.state.lock().unwrap_or_else(|e| e.into_inner()) = Some(state);
Ok(())
}
}
fn index_at(length: usize, index: i32) -> PyResult<i32> {
if index < 1 || index as usize > length {
return Err(OCCError::new_err("index out of range"));
}
Ok(index)
}
fn knot_at(state: &SplineState, index: i32) -> PyResult<f64> {
let position = index_at(state.knots.len(), index)? as usize;
Ok(state.knots[position - 1])
}
const GEOM2D_INFINITE: f64 = 1.0e100;
fn xyz(x: f64, y: f64) -> Vector3 {
Vector3::new(x, y, 0.0)
}
fn xy(point: Vector3) -> (f64, f64) {
(point.x, point.y)
}
fn unit_normal() -> Vector3 {
Vector3::new(0.0, 0.0, 1.0)
}
fn unit_x() -> Vector3 {
Vector3::new(1.0, 0.0, 0.0)
}
fn unit2(vector: (f64, f64)) -> Option<(f64, f64)> {
let length = (vector.0 * vector.0 + vector.1 * vector.1).sqrt();
if length.is_finite() && length > TOLERANCE {
Some((vector.0 / length, vector.1 / length))
} else {
None
}
}
fn cross2(a: (f64, f64), b: (f64, f64)) -> f64 {
a.0 * b.1 - a.1 * b.0
}
fn distance2(a: (f64, f64), b: (f64, f64)) -> f64 {
((a.0 - b.0).powi(2) + (a.1 - b.1).powi(2)).sqrt()
}
fn geom2d_not_implemented(what: &str) -> PyErr {
PyNotImplementedError::new_err(format!(
"Part.Geom2d.{what} is not implemented in this slice: the 2D seam has no \
such capability"
))
}
#[derive(Clone)]
pub(crate) enum Geom2dData {
Line(Line),
Segment {
origin: (f64, f64),
direction: (f64, f64),
first: f64,
last: f64,
},
Circle(Circle),
Arc(Arc),
Ellipse(Ellipse),
ArcOfEllipse {
ellipse: Ellipse,
first: f64,
last: f64,
},
Hyperbola(Hyperbola),
ArcOfHyperbola(ArcOfHyperbola),
Parabola(Parabola),
ArcOfParabola(ArcOfParabola),
Bezier(BezierCurve),
BSpline(Vec<(f64, f64)>),
Offset {
basis: Box<Geom2dData>,
offset: f64,
},
}
impl Geom2dData {
fn kind(&self) -> &'static str {
match self {
Self::Line(_) => "Line2d",
Self::Segment { .. } => "Line2dSegment",
Self::Circle(_) => "Circle2d",
Self::Arc(_) => "ArcOfCircle2d",
Self::Ellipse(_) => "Ellipse2d",
Self::ArcOfEllipse { .. } => "ArcOfEllipse2d",
Self::Hyperbola(_) => "Hyperbola2d",
Self::ArcOfHyperbola(_) => "ArcOfHyperbola2d",
Self::Parabola(_) => "Parabola2d",
Self::ArcOfParabola(_) => "ArcOfParabola2d",
Self::Bezier(_) => "BezierCurve2d",
Self::BSpline(_) => "BSplineCurve2d",
Self::Offset { .. } => "OffsetCurve2d",
}
}
fn point(&self, u: f64) -> Option<(f64, f64)> {
Some(match self {
Self::Line(line) => xy(line.point_at(u)),
Self::Segment {
origin, direction, ..
} => (
origin.0 + u * direction.0,
origin.1 + u * direction.1,
),
Self::Circle(circle) => xy(circle.point_at(u)),
Self::Arc(arc) => xy(arc.point_at(u)),
Self::Ellipse(ellipse) => xy(ellipse.point_at(u)),
Self::ArcOfEllipse { ellipse, .. } => xy(ellipse.point_at(u)),
Self::Hyperbola(hyperbola) => xy(hyperbola.point_at(u)),
Self::ArcOfHyperbola(arc) => xy(arc.point_at(u)),
Self::Parabola(parabola) => xy(parabola.point_at(u)),
Self::ArcOfParabola(arc) => xy(arc.point_at(u)),
Self::Bezier(curve) => xy(curve.point_at(u)),
Self::BSpline(_) => return None,
Self::Offset { basis, offset } => {
let point = basis.point(u)?;
let tangent = basis.tangent(u)?;
(point.0 - offset * tangent.1, point.1 + offset * tangent.0)
}
})
}
fn tangent(&self, u: f64) -> Option<(f64, f64)> {
Some(match self {
Self::Line(line) => xy(line.tangent_at(u)),
Self::Segment { direction, .. } => *direction,
Self::Circle(circle) => xy(circle.tangent_at(u)),
Self::Arc(arc) => xy(arc.tangent_at(u)),
Self::Ellipse(ellipse) => xy(ellipse.tangent_at(u)),
Self::ArcOfEllipse { ellipse, .. } => xy(ellipse.tangent_at(u)),
Self::Hyperbola(hyperbola) => xy(hyperbola.tangent_at(u)),
Self::ArcOfHyperbola(arc) => xy(arc.hyperbola().tangent_at(u)),
Self::Parabola(parabola) => xy(parabola.tangent_at(u)),
Self::ArcOfParabola(arc) => xy(arc.parabola().tangent_at(u)),
Self::Bezier(curve) => xy(curve.tangent_at(u)),
Self::BSpline(_) => return None,
Self::Offset { .. } => {
let step = 1e-6 * (1.0 + u.abs());
let before = self.point(u - step)?;
let after = self.point(u + step)?;
unit2((after.0 - before.0, after.1 - before.1))?
}
})
}
fn speed(&self, u: f64) -> Option<f64> {
Some(match self {
Self::Line(_) | Self::Segment { .. } => 1.0,
Self::Circle(circle) => circle.radius(),
Self::Arc(arc) => arc.circle().radius(),
Self::Ellipse(ellipse) => {
let (a, b) = (ellipse.major_radius(), ellipse.minor_radius());
(a * a * u.sin().powi(2) + b * b * u.cos().powi(2)).sqrt()
}
Self::ArcOfEllipse { ellipse, .. } => {
let (a, b) = (ellipse.major_radius(), ellipse.minor_radius());
(a * a * u.sin().powi(2) + b * b * u.cos().powi(2)).sqrt()
}
Self::Hyperbola(hyperbola) => {
let (a, b) = (hyperbola.major_radius(), hyperbola.minor_radius());
(a * a * u.sinh().powi(2) + b * b * u.cosh().powi(2)).sqrt()
}
Self::ArcOfHyperbola(arc) => {
let hyperbola = arc.hyperbola();
let (a, b) = (hyperbola.major_radius(), hyperbola.minor_radius());
(a * a * u.sinh().powi(2) + b * b * u.cosh().powi(2)).sqrt()
}
Self::Parabola(parabola) => {
let f = parabola.focal();
(u * u / (4.0 * f * f) + 1.0).sqrt()
}
Self::ArcOfParabola(arc) => {
let f = arc.parabola().focal();
(u * u / (4.0 * f * f) + 1.0).sqrt()
}
Self::Bezier(_) | Self::Offset { .. } => {
let step = 1e-6 * (1.0 + u.abs());
let before = self.point(u - step)?;
let after = self.point(u + step)?;
distance2(before, after) / (2.0 * step)
}
Self::BSpline(_) => return None,
})
}
fn first(&self) -> Option<f64> {
Some(match self {
Self::Line(_) | Self::Hyperbola(_) | Self::Parabola(_) => -GEOM2D_INFINITE,
Self::Segment { first, .. } => *first,
Self::Circle(_) | Self::Ellipse(_) => 0.0,
Self::Arc(arc) => arc.first(),
Self::ArcOfEllipse { first, .. } => *first,
Self::ArcOfHyperbola(arc) => arc.first(),
Self::ArcOfParabola(arc) => arc.first(),
Self::Bezier(curve) => curve.first(),
Self::BSpline(_) => return None,
Self::Offset { basis, .. } => basis.first()?,
})
}
fn last(&self) -> Option<f64> {
Some(match self {
Self::Line(_) | Self::Hyperbola(_) | Self::Parabola(_) => GEOM2D_INFINITE,
Self::Segment { last, .. } => *last,
Self::Circle(_) | Self::Ellipse(_) => std::f64::consts::TAU,
Self::Arc(arc) => arc.last(),
Self::ArcOfEllipse { last, .. } => *last,
Self::ArcOfHyperbola(arc) => arc.last(),
Self::ArcOfParabola(arc) => arc.last(),
Self::Bezier(curve) => curve.last(),
Self::BSpline(_) => return None,
Self::Offset { basis, .. } => basis.last()?,
})
}
fn is_closed(&self) -> bool {
match self {
Self::Circle(_) | Self::Ellipse(_) => true,
Self::Line(_) | Self::Hyperbola(_) | Self::Parabola(_) | Self::BSpline(_) => false,
Self::Bezier(curve) => curve.is_closed(),
Self::Offset { basis, .. } => basis.is_closed(),
_ => match (self.first(), self.last()) {
(Some(first), Some(last)) if first != last => {
match (self.point(first), self.point(last)) {
(Some(start), Some(end)) => distance2(start, end) <= TOLERANCE,
_ => false,
}
}
_ => false,
},
}
}
fn is_periodic(&self) -> bool {
matches!(self, Self::Circle(_) | Self::Ellipse(_))
}
fn continuity(&self) -> &'static str {
match self {
Self::Segment { .. }
| Self::Arc(_)
| Self::ArcOfEllipse { .. }
| Self::ArcOfHyperbola(_)
| Self::ArcOfParabola(_)
| Self::BSpline(_)
| Self::Offset { .. } => "C0",
_ => "CN",
}
}
fn location(&self) -> Option<(f64, f64)> {
Some(match self {
Self::Line(line) => xy(line.origin()),
Self::Segment { origin, .. } => *origin,
Self::Circle(circle) => xy(circle.center()),
Self::Arc(arc) => xy(arc.circle().center()),
Self::Ellipse(ellipse) => xy(ellipse.center()),
Self::ArcOfEllipse { ellipse, .. } => xy(ellipse.center()),
Self::Hyperbola(hyperbola) => xy(hyperbola.center()),
Self::ArcOfHyperbola(arc) => xy(arc.hyperbola().center()),
Self::Parabola(parabola) => xy(parabola.vertex()),
Self::ArcOfParabola(arc) => xy(arc.parabola().vertex()),
_ => return None,
})
}
fn normal(&self) -> Option<Vector3> {
Some(match self {
Self::Line(_) | Self::Segment { .. } | Self::Bezier(_) => unit_normal(),
Self::Circle(circle) => circle.normal(),
Self::Arc(arc) => arc.circle().normal(),
Self::Ellipse(ellipse) => ellipse.normal(),
Self::ArcOfEllipse { ellipse, .. } => ellipse.normal(),
Self::Hyperbola(hyperbola) => hyperbola.normal(),
Self::ArcOfHyperbola(arc) => arc.hyperbola().normal(),
Self::Parabola(parabola) => parabola.normal(),
Self::ArcOfParabola(arc) => arc.parabola().normal(),
_ => return None,
})
}
fn x_axis(&self) -> Option<(f64, f64)> {
Some(match self {
Self::Line(line) => xy(line.direction()),
Self::Segment { direction, .. } => *direction,
Self::Circle(circle) => xy(circle.x_axis()),
Self::Arc(arc) => xy(arc.circle().x_axis()),
Self::Ellipse(ellipse) => xy(ellipse.x_axis()),
Self::ArcOfEllipse { ellipse, .. } => xy(ellipse.x_axis()),
Self::Hyperbola(hyperbola) => xy(hyperbola.x_axis()),
Self::ArcOfHyperbola(arc) => xy(arc.hyperbola().x_axis()),
Self::Parabola(parabola) => xy(parabola.x_axis()),
Self::ArcOfParabola(arc) => xy(arc.parabola().x_axis()),
_ => return None,
})
}
fn y_axis(&self) -> Option<(f64, f64)> {
Some(match self {
Self::Line(line) => xy(line.direction()),
Self::Segment { direction, .. } => *direction,
Self::Circle(circle) => xy(circle.y_axis()),
Self::Arc(arc) => xy(arc.circle().y_axis()),
Self::Ellipse(ellipse) => xy(ellipse.y_axis()),
Self::ArcOfEllipse { ellipse, .. } => xy(ellipse.y_axis()),
Self::Hyperbola(hyperbola) => xy(hyperbola.y_axis()),
Self::ArcOfHyperbola(arc) => xy(arc.hyperbola().y_axis()),
Self::Parabola(parabola) => xy(parabola.y_axis()),
Self::ArcOfParabola(arc) => xy(arc.parabola().y_axis()),
_ => return None,
})
}
fn eccentricity(&self) -> Option<f64> {
Some(match self {
Self::Circle(_) | Self::Arc(_) => 0.0,
Self::Ellipse(ellipse) => {
let (a, b) = (ellipse.major_radius(), ellipse.minor_radius());
(1.0 - (b * b) / (a * a)).max(0.0).sqrt()
}
Self::ArcOfEllipse { ellipse, .. } => {
let (a, b) = (ellipse.major_radius(), ellipse.minor_radius());
(1.0 - (b * b) / (a * a)).max(0.0).sqrt()
}
Self::Hyperbola(hyperbola) => hyperbola.eccentricity(),
Self::ArcOfHyperbola(arc) => arc.hyperbola().eccentricity(),
Self::Parabola(_) | Self::ArcOfParabola(_) => 1.0,
_ => return None,
})
}
fn with_location(&self, center: (f64, f64)) -> Option<Self> {
let point = xyz(center.0, center.1);
Some(match self {
Self::Line(line) => Self::Line(Line::new(point, line.direction()).ok()?),
Self::Segment {
direction,
first,
last,
..
} => Self::Segment {
origin: center,
direction: *direction,
first: *first,
last: *last,
},
Self::Circle(circle) => Self::Circle(
Circle::with_axis(point, circle.normal(), circle.x_axis(), circle.radius())
.ok()?,
),
Self::Arc(arc) => {
let circle = Circle::with_axis(
point,
arc.circle().normal(),
arc.circle().x_axis(),
arc.circle().radius(),
)
.ok()?;
Self::Arc(Arc::new(circle, arc.first(), arc.last()).ok()?)
}
Self::Ellipse(ellipse) => Self::Ellipse(
Ellipse::with_axis(
point,
ellipse.normal(),
ellipse.x_axis(),
ellipse.major_radius(),
ellipse.minor_radius(),
)
.ok()?,
),
Self::ArcOfEllipse {
ellipse,
first,
last,
} => Self::ArcOfEllipse {
ellipse: Ellipse::with_axis(
point,
ellipse.normal(),
ellipse.x_axis(),
ellipse.major_radius(),
ellipse.minor_radius(),
)
.ok()?,
first: *first,
last: *last,
},
Self::Hyperbola(hyperbola) => Self::Hyperbola(
Hyperbola::with_axis(
point,
hyperbola.normal(),
hyperbola.x_axis(),
hyperbola.major_radius(),
hyperbola.minor_radius(),
)
.ok()?,
),
Self::ArcOfHyperbola(arc) => {
let hyperbola = Hyperbola::with_axis(
point,
arc.hyperbola().normal(),
arc.hyperbola().x_axis(),
arc.hyperbola().major_radius(),
arc.hyperbola().minor_radius(),
)
.ok()?;
Self::ArcOfHyperbola(ArcOfHyperbola::new(hyperbola, arc.first(), arc.last()).ok()?)
}
Self::Parabola(parabola) => Self::Parabola(
Parabola::with_axis(point, parabola.normal(), parabola.x_axis(), parabola.focal())
.ok()?,
),
Self::ArcOfParabola(arc) => {
let parabola = Parabola::with_axis(
point,
arc.parabola().normal(),
arc.parabola().x_axis(),
arc.parabola().focal(),
)
.ok()?;
Self::ArcOfParabola(ArcOfParabola::new(parabola, arc.first(), arc.last()).ok()?)
}
_ => return None,
})
}
fn with_x_axis(&self, direction: (f64, f64)) -> Option<Self> {
let direction = unit2(direction)?;
let axis = xyz(direction.0, direction.1);
Some(match self {
Self::Line(line) => Self::Line(Line::new(line.origin(), axis).ok()?),
Self::Segment {
origin,
first,
last,
..
} => Self::Segment {
origin: *origin,
direction,
first: *first,
last: *last,
},
Self::Circle(circle) => Self::Circle(
Circle::with_axis(circle.center(), circle.normal(), axis, circle.radius()).ok()?,
),
Self::Arc(arc) => {
let circle = Circle::with_axis(
arc.circle().center(),
arc.circle().normal(),
axis,
arc.circle().radius(),
)
.ok()?;
Self::Arc(Arc::new(circle, arc.first(), arc.last()).ok()?)
}
Self::Ellipse(ellipse) => Self::Ellipse(
Ellipse::with_axis(
ellipse.center(),
ellipse.normal(),
axis,
ellipse.major_radius(),
ellipse.minor_radius(),
)
.ok()?,
),
Self::ArcOfEllipse {
ellipse,
first,
last,
} => Self::ArcOfEllipse {
ellipse: Ellipse::with_axis(
ellipse.center(),
ellipse.normal(),
axis,
ellipse.major_radius(),
ellipse.minor_radius(),
)
.ok()?,
first: *first,
last: *last,
},
Self::Hyperbola(hyperbola) => Self::Hyperbola(
Hyperbola::with_axis(
hyperbola.center(),
hyperbola.normal(),
axis,
hyperbola.major_radius(),
hyperbola.minor_radius(),
)
.ok()?,
),
Self::ArcOfHyperbola(arc) => {
let hyperbola = Hyperbola::with_axis(
arc.hyperbola().center(),
arc.hyperbola().normal(),
axis,
arc.hyperbola().major_radius(),
arc.hyperbola().minor_radius(),
)
.ok()?;
Self::ArcOfHyperbola(ArcOfHyperbola::new(hyperbola, arc.first(), arc.last()).ok()?)
}
Self::Parabola(parabola) => Self::Parabola(
Parabola::with_axis(parabola.vertex(), parabola.normal(), axis, parabola.focal())
.ok()?,
),
Self::ArcOfParabola(arc) => {
let parabola = Parabola::with_axis(
arc.parabola().vertex(),
arc.parabola().normal(),
axis,
arc.parabola().focal(),
)
.ok()?;
Self::ArcOfParabola(ArcOfParabola::new(parabola, arc.first(), arc.last()).ok()?)
}
_ => return None,
})
}
fn with_y_axis(&self, direction: (f64, f64)) -> Option<Self> {
let direction = unit2(direction)?;
let normal = self.normal()?;
let y = xyz(direction.0, direction.1);
let x = y.cross(&normal);
self.with_x_axis((x.x, x.y))
}
fn reversed(&self) -> Option<Self> {
Some(match self {
Self::Line(line) => {
Self::Line(Line::new(line.origin(), line.direction().scale(-1.0)).ok()?)
}
Self::Segment {
origin,
direction,
first,
last,
} => {
let start = (
origin.0 + last * direction.0,
origin.1 + last * direction.1,
);
Self::Segment {
origin: start,
direction: (-direction.0, -direction.1),
first: 0.0,
last: (last - first).abs(),
}
}
Self::Circle(circle) => Self::Circle(
Circle::with_axis(
circle.center(),
circle.normal().scale(-1.0),
circle.x_axis(),
circle.radius(),
)
.ok()?,
),
Self::Arc(arc) => Self::Arc(Arc::new(arc.circle(), arc.last(), arc.first()).ok()?),
Self::Ellipse(ellipse) => Self::Ellipse(
Ellipse::with_axis(
ellipse.center(),
ellipse.normal().scale(-1.0),
ellipse.x_axis(),
ellipse.major_radius(),
ellipse.minor_radius(),
)
.ok()?,
),
Self::ArcOfEllipse {
ellipse,
first,
last,
} => Self::ArcOfEllipse {
ellipse: *ellipse,
first: *last,
last: *first,
},
Self::Hyperbola(hyperbola) => Self::Hyperbola(
Hyperbola::with_axis(
hyperbola.center(),
hyperbola.normal().scale(-1.0),
hyperbola.x_axis(),
hyperbola.major_radius(),
hyperbola.minor_radius(),
)
.ok()?,
),
Self::ArcOfHyperbola(arc) => {
let hyperbola = Hyperbola::with_axis(
arc.hyperbola().center(),
arc.hyperbola().normal().scale(-1.0),
arc.hyperbola().x_axis(),
arc.hyperbola().major_radius(),
arc.hyperbola().minor_radius(),
)
.ok()?;
Self::ArcOfHyperbola(
ArcOfHyperbola::new(hyperbola, -arc.last(), -arc.first()).ok()?,
)
}
Self::Parabola(parabola) => Self::Parabola(
Parabola::with_axis(
parabola.vertex(),
parabola.normal().scale(-1.0),
parabola.x_axis(),
parabola.focal(),
)
.ok()?,
),
Self::ArcOfParabola(arc) => {
let parabola = Parabola::with_axis(
arc.parabola().vertex(),
arc.parabola().normal().scale(-1.0),
arc.parabola().x_axis(),
arc.parabola().focal(),
)
.ok()?;
Self::ArcOfParabola(
ArcOfParabola::new(parabola, -arc.last(), -arc.first()).ok()?,
)
}
Self::Bezier(curve) => {
let poles: Vec<Vector3> = curve.poles().iter().rev().copied().collect();
let weights: Vec<f64> = curve.weights().iter().rev().copied().collect();
Self::Bezier(
BezierCurve::with_range(poles, weights, curve.first(), curve.last()).ok()?,
)
}
Self::BSpline(points) => Self::BSpline(points.iter().rev().copied().collect()),
Self::Offset { basis, offset } => Self::Offset {
basis: Box::new(basis.reversed()?),
offset: *offset,
},
})
}
fn restricted(&self, first: f64, last: f64) -> PyResult<Self> {
if !first.is_finite() || !last.is_finite() {
return Err(OCCError::new_err("a 2D curve range must be finite"));
}
if (last - first).abs() <= ANGULAR_TOLERANCE {
return Err(OCCError::new_err(
"a 2D curve range needs a non-zero sweep",
));
}
Ok(match self {
Self::Line(line) => Self::Segment {
origin: xy(line.origin()),
direction: xy(line.direction()),
first,
last,
},
Self::Segment {
origin, direction, ..
} => Self::Segment {
origin: *origin,
direction: *direction,
first,
last,
},
Self::Circle(circle) => {
Self::Arc(Arc::new(*circle, first, last).map_err(curve_error)?)
}
Self::Arc(arc) => {
Self::Arc(Arc::new(arc.circle(), first, last).map_err(curve_error)?)
}
Self::Ellipse(ellipse) => Self::ArcOfEllipse {
ellipse: *ellipse,
first,
last,
},
Self::ArcOfEllipse { ellipse, .. } => Self::ArcOfEllipse {
ellipse: *ellipse,
first,
last,
},
Self::Hyperbola(hyperbola) => Self::ArcOfHyperbola(
ArcOfHyperbola::new(*hyperbola, first, last).map_err(curve_error)?,
),
Self::ArcOfHyperbola(arc) => Self::ArcOfHyperbola(
ArcOfHyperbola::new(arc.hyperbola(), first, last).map_err(curve_error)?,
),
Self::Parabola(parabola) => Self::ArcOfParabola(
ArcOfParabola::new(*parabola, first, last).map_err(curve_error)?,
),
Self::ArcOfParabola(arc) => Self::ArcOfParabola(
ArcOfParabola::new(arc.parabola(), first, last).map_err(curve_error)?,
),
Self::Bezier(curve) => Self::Bezier(curve.trim(first, last).map_err(curve_error)?),
Self::BSpline(_) => {
return Err(geom2d_not_implemented(
"BSplineCurve2d.toShape (this slice binds no 2D B-spline kernel)",
))
}
Self::Offset { .. } => self.clone(),
})
}
fn closest_parameter(&self, point: (f64, f64)) -> Option<f64> {
match self {
Self::Line(line) => {
let direction = xy(line.direction());
let origin = xy(line.origin());
Some((point.0 - origin.0) * direction.0 + (point.1 - origin.1) * direction.1)
}
Self::Segment {
origin,
direction,
first,
last,
} => {
let projected =
(point.0 - origin.0) * direction.0 + (point.1 - origin.1) * direction.1;
Some(projected.clamp(*first, *last))
}
_ => {
let (first, last) = (self.first()?, self.last()?);
if !first.is_finite() || !last.is_finite() || last <= first || last - first > 1.0e6 {
return None;
}
let samples = 200usize;
let step = (last - first) / samples as f64;
let mut best = first;
let mut best_distance = f64::INFINITY;
for index in 0..=samples {
let u = first + index as f64 * step;
let candidate = self.point(u)?;
let distance = distance2(candidate, point);
if distance < best_distance {
best_distance = distance;
best = u;
}
}
let mut low = (best - step).max(first);
let mut high = (best + step).min(last);
for _ in 0..60 {
let left = low + 0.382 * (high - low);
let right = low + 0.618 * (high - low);
let left_distance = distance2(self.point(left)?, point);
let right_distance = distance2(self.point(right)?, point);
if left_distance <= right_distance {
high = right;
} else {
low = left;
}
}
Some(0.5 * (low + high))
}
}
}
}
fn curvature_of(data: &Geom2dData, u: f64) -> PyResult<f64> {
match data {
Geom2dData::Line(_) | Geom2dData::Segment { .. } => Ok(0.0),
Geom2dData::Circle(circle) => Ok(1.0 / circle.radius()),
Geom2dData::Arc(arc) => Ok(1.0 / arc.circle().radius()),
_ => {
let step = 1e-6 * (1.0 + u.abs());
let before = data
.tangent(u - step)
.ok_or_else(|| geom2d_not_implemented("BSplineCurve2d.curvature"))?;
let after = data
.tangent(u + step)
.ok_or_else(|| geom2d_not_implemented("BSplineCurve2d.curvature"))?;
let rate = distance2(before, after) / (2.0 * step);
let speed = data
.speed(u)
.ok_or_else(|| geom2d_not_implemented("BSplineCurve2d.curvature"))?;
Ok(rate / speed)
}
}
}
fn geometry_data(value: &Bound<'_, PyAny>) -> PyResult<Geom2dData> {
let geometry = value
.downcast::<PyGeometry2d>()
.map_err(|_| PyTypeError::new_err("expected a Part.Geom2d geometry"))?;
Ok(geometry.borrow().snapshot())
}
fn replace_geometry_data(value: &Bound<'_, PyAny>, data: Geom2dData) -> PyResult<()> {
let geometry = value
.downcast::<PyGeometry2d>()
.map_err(|_| PyTypeError::new_err("expected a Part.Geom2d geometry"))?;
*geometry
.borrow()
.data
.lock()
.unwrap_or_else(|error| error.into_inner()) = data;
Ok(())
}
#[pyclass(name = "Geometry2d", module = "Part.Geom2d", subclass)]
pub struct PyGeometry2d {
data: Mutex<Geom2dData>,
}
impl PyGeometry2d {
fn with_data(data: Geom2dData) -> Self {
Self {
data: Mutex::new(data),
}
}
fn snapshot(&self) -> Geom2dData {
self.data
.lock()
.unwrap_or_else(|error| error.into_inner())
.clone()
}
}
#[pymethods]
impl PyGeometry2d {
#[new]
#[pyo3(signature = (*_args))]
fn new(_args: &Bound<'_, PyTuple>) -> PyResult<Self> {
Err(PyRuntimeError::new_err(
"You cannot create an instance of the abstract class 'Geometry'.",
))
}
fn copy(&self, py: Python<'_>) -> PyResult<PyObject> {
geometry2d_to_py(py, self.snapshot())
}
fn transform(&self, _matrix: &Bound<'_, PyAny>) -> PyResult<()> {
Err(geom2d_not_implemented("Geometry2d.transform"))
}
#[pyo3(signature = (point, axis = None))]
fn mirror(&self, point: &Bound<'_, PyAny>, axis: Option<&Bound<'_, PyAny>>) -> PyResult<()> {
let _ = (point, axis);
Err(geom2d_not_implemented("Geometry2d.mirror"))
}
fn rotate(&self, _point: &Bound<'_, PyAny>, _angle: f64) -> PyResult<()> {
Err(geom2d_not_implemented("Geometry2d.rotate"))
}
fn scale(&self, _point: &Bound<'_, PyAny>, _factor: f64) -> PyResult<()> {
Err(geom2d_not_implemented("Geometry2d.scale"))
}
fn translate(&self, _offset: &Bound<'_, PyAny>) -> PyResult<()> {
Err(geom2d_not_implemented("Geometry2d.translate"))
}
fn __repr__(&self) -> String {
format!("<Geometry2d object ({})>", self.snapshot().kind())
}
}
#[pyclass(name = "Curve2d", module = "Part.Geom2d", extends = PyGeometry2d, subclass)]
pub struct PyCurve2d;
#[pymethods]
impl PyCurve2d {
#[new]
#[pyo3(signature = (*_args))]
fn new(_args: &Bound<'_, PyTuple>) -> PyResult<PyClassInitializer<Self>> {
Err(PyRuntimeError::new_err(
"You cannot create an instance of the abstract class 'Curve2d'.",
))
}
#[getter]
fn Continuity(slf: &Bound<'_, Self>) -> PyResult<String> {
Ok(geometry_data(slf.as_any())?.continuity().to_string())
}
#[getter]
fn Closed(slf: &Bound<'_, Self>) -> PyResult<bool> {
Ok(geometry_data(slf.as_any())?.is_closed())
}
#[getter]
fn Periodic(slf: &Bound<'_, Self>) -> PyResult<bool> {
Ok(geometry_data(slf.as_any())?.is_periodic())
}
#[getter]
fn FirstParameter(slf: &Bound<'_, Self>) -> PyResult<f64> {
geometry_data(slf.as_any())?
.first()
.ok_or_else(|| geom2d_not_implemented("BSplineCurve2d.FirstParameter"))
}
#[getter]
fn LastParameter(slf: &Bound<'_, Self>) -> PyResult<f64> {
geometry_data(slf.as_any())?
.last()
.ok_or_else(|| geom2d_not_implemented("BSplineCurve2d.LastParameter"))
}
fn value(slf: &Bound<'_, Self>, py: Python<'_>, u: f64) -> PyResult<PyObject> {
let data = geometry_data(slf.as_any())?;
let point = data
.point(u)
.ok_or_else(|| geom2d_not_implemented("BSplineCurve2d.value"))?;
py_vector2d(py, point)
}
fn tangent(slf: &Bound<'_, Self>, py: Python<'_>, u: f64) -> PyResult<PyObject> {
let data = geometry_data(slf.as_any())?;
let tangent = data
.tangent(u)
.ok_or_else(|| geom2d_not_implemented("BSplineCurve2d.tangent"))?;
py_vector2d(py, tangent)
}
fn normal(slf: &Bound<'_, Self>, py: Python<'_>, u: f64) -> PyResult<PyObject> {
let data = geometry_data(slf.as_any())?;
let tangent = data
.tangent(u)
.ok_or_else(|| geom2d_not_implemented("BSplineCurve2d.normal"))?;
py_vector2d(py, (-tangent.1, tangent.0))
}
fn curvature(slf: &Bound<'_, Self>, u: f64) -> PyResult<f64> {
curvature_of(&geometry_data(slf.as_any())?, u)
}
fn centerOfCurvature(slf: &Bound<'_, Self>, py: Python<'_>, u: f64) -> PyResult<PyObject> {
let data = geometry_data(slf.as_any())?;
let point = data
.point(u)
.ok_or_else(|| geom2d_not_implemented("BSplineCurve2d.centerOfCurvature"))?;
let tangent = data
.tangent(u)
.ok_or_else(|| geom2d_not_implemented("BSplineCurve2d.centerOfCurvature"))?;
let curvature = curvature_of(&data, u)?;
if curvature.abs() <= TOLERANCE {
return Err(OCCError::new_err(
"the centre of curvature is undefined where the curvature is zero",
));
}
let normal = (-tangent.1, tangent.0);
py_vector2d(
py,
(
point.0 + normal.0 / curvature,
point.1 + normal.1 / curvature,
),
)
}
fn parameter(slf: &Bound<'_, Self>, point: &Bound<'_, PyAny>) -> PyResult<f64> {
let data = geometry_data(slf.as_any())?;
data.closest_parameter(vector2d(point)?)
.ok_or_else(|| geom2d_not_implemented("Curve2d.parameter on an unbounded curve"))
}
#[pyo3(signature = (uMin = None, uMax = None, tolerance = None))]
fn length(
slf: &Bound<'_, Self>,
uMin: Option<f64>,
uMax: Option<f64>,
tolerance: Option<f64>,
) -> PyResult<f64> {
let _ = tolerance;
let data = geometry_data(slf.as_any())?;
let (first, last) = match (uMin, uMax) {
(Some(first), Some(last)) => (first, last),
(None, None) => (
data.first()
.ok_or_else(|| geom2d_not_implemented("BSplineCurve2d.length"))?,
data.last()
.ok_or_else(|| geom2d_not_implemented("BSplineCurve2d.length"))?,
),
_ => {
return Err(PyTypeError::new_err(
"length takes either no range or both uMin and uMax",
))
}
};
let span = last - first;
if !span.is_finite() || span.abs() > 1.0e6 {
return Err(OCCError::new_err(
"the length of an unbounded 2D curve needs a finite parameter range",
));
}
let intervals = 512usize;
let step = span / intervals as f64;
let mut sum = data
.speed(first)
.ok_or_else(|| geom2d_not_implemented("BSplineCurve2d.length"))?
+ data
.speed(last)
.ok_or_else(|| geom2d_not_implemented("BSplineCurve2d.length"))?;
for index in 1..intervals {
let weight = if index % 2 == 0 { 2.0 } else { 4.0 };
let speed = data
.speed(first + index as f64 * step)
.ok_or_else(|| geom2d_not_implemented("BSplineCurve2d.length"))?;
sum += weight * speed;
}
Ok(sum * step / 3.0)
}
fn reverse(slf: &Bound<'_, Self>) -> PyResult<()> {
let data = geometry_data(slf.as_any())?;
let reversed = data
.reversed()
.ok_or_else(|| geom2d_not_implemented("Curve2d.reverse on this curve"))?;
replace_geometry_data(slf.as_any(), reversed)
}
#[pyo3(signature = (*args))]
fn toShape(
slf: &Bound<'_, Self>,
py: Python<'_>,
args: &Bound<'_, PyTuple>,
) -> PyResult<Py<PyEdge>> {
let data = geometry_data(slf.as_any())?;
let (_has_surface, first, last) = parse_geometry2d_to_shape_args(args)?;
curve2d_edge(py, &data, first, last)
}
#[pyo3(signature = (*_args, **_kwargs))]
fn discretize(
&self,
_args: &Bound<'_, PyTuple>,
_kwargs: Option<&Bound<'_, PyDict>>,
) -> PyResult<Vec<f64>> {
Err(geom2d_not_implemented("Curve2d.discretize"))
}
#[pyo3(signature = (*_args))]
fn toBSpline(&self, _args: &Bound<'_, PyTuple>) -> PyResult<PyObject> {
Err(geom2d_not_implemented(
"Curve2d.toBSpline (this slice binds no 2D B-spline kernel; use toShape())",
))
}
#[pyo3(signature = (*_args))]
fn approximateBSpline(&self, _args: &Bound<'_, PyTuple>) -> PyResult<PyObject> {
Err(geom2d_not_implemented("Curve2d.approximateBSpline"))
}
#[pyo3(signature = (*_args))]
fn intersectCC(&self, _args: &Bound<'_, PyTuple>) -> PyResult<Vec<f64>> {
Err(geom2d_not_implemented("Curve2d.intersectCC"))
}
#[pyo3(signature = (*_args))]
fn parameterAtDistance(&self, _args: &Bound<'_, PyTuple>) -> PyResult<f64> {
Err(geom2d_not_implemented("Curve2d.parameterAtDistance"))
}
fn __repr__(slf: &Bound<'_, Self>) -> PyResult<String> {
let data = geometry_data(slf.as_any())?;
Ok(match data.location() {
Some(location) => format!(
"Part.Geom2d.{}({:?}, {})",
data.kind(),
location,
data.first().unwrap_or(0.0)
),
None => format!("Part.Geom2d.{}(...)", data.kind()),
})
}
}
#[pyclass(name = "Conic2d", module = "Part.Geom2d", extends = PyCurve2d, subclass)]
pub struct PyConic2d;
#[pymethods]
impl PyConic2d {
#[new]
#[pyo3(signature = (*_args))]
fn new(_args: &Bound<'_, PyTuple>) -> PyResult<PyClassInitializer<Self>> {
Err(PyRuntimeError::new_err(
"You cannot create an instance of the abstract class 'Conic2d'.",
))
}
#[getter]
fn Location(slf: &Bound<'_, Self>, py: Python<'_>) -> PyResult<PyObject> {
let location = geometry_data(slf.as_any())?
.location()
.ok_or_else(|| geom2d_not_implemented("Conic2d.Location"))?;
py_vector2d(py, location)
}
#[setter]
fn set_Location(slf: &Bound<'_, Self>, value: &Bound<'_, PyAny>) -> PyResult<()> {
let location = vector2d(value)?;
let data = geometry_data(slf.as_any())?;
let updated = data
.with_location(location)
.ok_or_else(|| geom2d_not_implemented("Conic2d.Location"))?;
replace_geometry_data(slf.as_any(), updated)
}
#[getter]
fn XAxis(slf: &Bound<'_, Self>, py: Python<'_>) -> PyResult<PyObject> {
let axis = geometry_data(slf.as_any())?
.x_axis()
.ok_or_else(|| geom2d_not_implemented("Conic2d.XAxis"))?;
py_vector2d(py, axis)
}
#[setter]
fn set_XAxis(slf: &Bound<'_, Self>, value: &Bound<'_, PyAny>) -> PyResult<()> {
let axis = vector2d(value)?;
let data = geometry_data(slf.as_any())?;
let updated = data
.with_x_axis(axis)
.ok_or_else(|| geom2d_not_implemented("Conic2d.XAxis"))?;
replace_geometry_data(slf.as_any(), updated)
}
#[getter]
fn YAxis(slf: &Bound<'_, Self>, py: Python<'_>) -> PyResult<PyObject> {
let axis = geometry_data(slf.as_any())?
.y_axis()
.ok_or_else(|| geom2d_not_implemented("Conic2d.YAxis"))?;
py_vector2d(py, axis)
}
#[setter]
fn set_YAxis(slf: &Bound<'_, Self>, value: &Bound<'_, PyAny>) -> PyResult<()> {
let axis = vector2d(value)?;
let data = geometry_data(slf.as_any())?;
let updated = data
.with_y_axis(axis)
.ok_or_else(|| geom2d_not_implemented("Conic2d.YAxis"))?;
replace_geometry_data(slf.as_any(), updated)
}
#[getter]
fn Eccentricity(slf: &Bound<'_, Self>) -> PyResult<f64> {
geometry_data(slf.as_any())?
.eccentricity()
.ok_or_else(|| geom2d_not_implemented("Conic2d.Eccentricity"))
}
}
#[pyclass(name = "ArcOfConic2d", module = "Part.Geom2d", extends = PyCurve2d, subclass)]
pub struct PyArcOfConic2d;
#[pymethods]
impl PyArcOfConic2d {
#[new]
#[pyo3(signature = (*_args))]
fn new(_args: &Bound<'_, PyTuple>) -> PyResult<PyClassInitializer<Self>> {
Err(PyRuntimeError::new_err(
"You cannot create an instance of the abstract class 'ArcOfConic2d'.",
))
}
#[getter]
fn Location(slf: &Bound<'_, Self>, py: Python<'_>) -> PyResult<PyObject> {
let location = geometry_data(slf.as_any())?
.location()
.ok_or_else(|| geom2d_not_implemented("ArcOfConic2d.Location"))?;
py_vector2d(py, location)
}
#[setter]
fn set_Location(slf: &Bound<'_, Self>, value: &Bound<'_, PyAny>) -> PyResult<()> {
let location = vector2d(value)?;
let data = geometry_data(slf.as_any())?;
let updated = data
.with_location(location)
.ok_or_else(|| geom2d_not_implemented("ArcOfConic2d.Location"))?;
replace_geometry_data(slf.as_any(), updated)
}
#[getter]
fn XAxis(slf: &Bound<'_, Self>, py: Python<'_>) -> PyResult<PyObject> {
let axis = geometry_data(slf.as_any())?
.x_axis()
.ok_or_else(|| geom2d_not_implemented("ArcOfConic2d.XAxis"))?;
py_vector2d(py, axis)
}
#[setter]
fn set_XAxis(slf: &Bound<'_, Self>, value: &Bound<'_, PyAny>) -> PyResult<()> {
let axis = vector2d(value)?;
let data = geometry_data(slf.as_any())?;
let updated = data
.with_x_axis(axis)
.ok_or_else(|| geom2d_not_implemented("ArcOfConic2d.XAxis"))?;
replace_geometry_data(slf.as_any(), updated)
}
#[getter]
fn YAxis(slf: &Bound<'_, Self>, py: Python<'_>) -> PyResult<PyObject> {
let axis = geometry_data(slf.as_any())?
.y_axis()
.ok_or_else(|| geom2d_not_implemented("ArcOfConic2d.YAxis"))?;
py_vector2d(py, axis)
}
#[setter]
fn set_YAxis(slf: &Bound<'_, Self>, value: &Bound<'_, PyAny>) -> PyResult<()> {
let axis = vector2d(value)?;
let data = geometry_data(slf.as_any())?;
let updated = data
.with_y_axis(axis)
.ok_or_else(|| geom2d_not_implemented("ArcOfConic2d.YAxis"))?;
replace_geometry_data(slf.as_any(), updated)
}
#[getter]
fn Eccentricity(slf: &Bound<'_, Self>) -> PyResult<f64> {
geometry_data(slf.as_any())?
.eccentricity()
.ok_or_else(|| geom2d_not_implemented("ArcOfConic2d.Eccentricity"))
}
#[getter]
fn StartPoint(slf: &Bound<'_, Self>, py: Python<'_>) -> PyResult<PyObject> {
let data = geometry_data(slf.as_any())?;
let first = data
.first()
.ok_or_else(|| geom2d_not_implemented("ArcOfConic2d.StartPoint"))?;
let point = data
.point(first)
.ok_or_else(|| geom2d_not_implemented("ArcOfConic2d.StartPoint"))?;
py_vector2d(py, point)
}
#[getter]
fn EndPoint(slf: &Bound<'_, Self>, py: Python<'_>) -> PyResult<PyObject> {
let data = geometry_data(slf.as_any())?;
let last = data
.last()
.ok_or_else(|| geom2d_not_implemented("ArcOfConic2d.EndPoint"))?;
let point = data
.point(last)
.ok_or_else(|| geom2d_not_implemented("ArcOfConic2d.EndPoint"))?;
py_vector2d(py, point)
}
}
macro_rules! curve2d_initializer {
($class:ident) => {
impl $class {
fn initializer(data: Geom2dData) -> PyClassInitializer<Self> {
PyClassInitializer::from(PyGeometry2d::with_data(data))
.add_subclass(PyCurve2d)
.add_subclass(Self)
}
}
};
}
macro_rules! conic2d_initializer {
($class:ident) => {
impl $class {
fn initializer(data: Geom2dData) -> PyClassInitializer<Self> {
PyClassInitializer::from(PyGeometry2d::with_data(data))
.add_subclass(PyCurve2d)
.add_subclass(PyConic2d)
.add_subclass(Self)
}
}
};
}
macro_rules! arc_of_conic2d_initializer {
($class:ident) => {
impl $class {
fn initializer(data: Geom2dData) -> PyClassInitializer<Self> {
PyClassInitializer::from(PyGeometry2d::with_data(data))
.add_subclass(PyCurve2d)
.add_subclass(PyArcOfConic2d)
.add_subclass(Self)
}
}
};
}
fn line_data(origin: (f64, f64), direction: (f64, f64)) -> PyResult<Geom2dData> {
let direction = unit2(direction)
.ok_or_else(|| OCCError::new_err("the line direction must not be null"))?;
Ok(Geom2dData::Line(
Line::new(xyz(origin.0, origin.1), xyz(direction.0, direction.1))
.map_err(curve_error)?,
))
}
fn segment_data(start: (f64, f64), end: (f64, f64)) -> PyResult<Geom2dData> {
let delta = (end.0 - start.0, end.1 - start.1);
let length = (delta.0 * delta.0 + delta.1 * delta.1).sqrt();
if !length.is_finite() || length <= TOLERANCE {
return Err(OCCError::new_err("Both points are equal"));
}
Ok(Geom2dData::Segment {
origin: start,
direction: (delta.0 / length, delta.1 / length),
first: 0.0,
last: length,
})
}
fn circle_data(center: (f64, f64), radius: f64) -> PyResult<Geom2dData> {
if !radius.is_finite() || radius <= TOLERANCE {
return Err(OCCError::new_err("the circle radius must be positive"));
}
Ok(Geom2dData::Circle(
Circle::with_axis(xyz(center.0, center.1), unit_normal(), unit_x(), radius)
.map_err(curve_error)?,
))
}
fn ellipse_data(
center: (f64, f64),
x_axis: (f64, f64),
major: f64,
minor: f64,
) -> PyResult<Geom2dData> {
if !major.is_finite() || !minor.is_finite() || major <= TOLERANCE || minor <= TOLERANCE {
return Err(OCCError::new_err("the ellipse radii must be positive"));
}
if major < minor {
return Err(OCCError::new_err(
"the ellipse major radius must not be smaller than the minor radius",
));
}
Ok(Geom2dData::Ellipse(
Ellipse::with_axis(
xyz(center.0, center.1),
unit_normal(),
xyz(x_axis.0, x_axis.1),
major,
minor,
)
.map_err(curve_error)?,
))
}
fn hyperbola_data(
center: (f64, f64),
x_axis: (f64, f64),
major: f64,
minor: f64,
) -> PyResult<Geom2dData> {
if !major.is_finite() || !minor.is_finite() || major <= TOLERANCE || minor <= TOLERANCE {
return Err(OCCError::new_err("the hyperbola radii must be positive"));
}
Ok(Geom2dData::Hyperbola(
Hyperbola::with_axis(
xyz(center.0, center.1),
unit_normal(),
xyz(x_axis.0, x_axis.1),
major,
minor,
)
.map_err(curve_error)?,
))
}
fn parabola_data(vertex: (f64, f64), x_axis: (f64, f64), focal: f64) -> PyResult<Geom2dData> {
if !focal.is_finite() || focal <= TOLERANCE {
return Err(OCCError::new_err(
"the parabola focal length must be positive",
));
}
Ok(Geom2dData::Parabola(
Parabola::with_axis(
xyz(vertex.0, vertex.1),
unit_normal(),
xyz(x_axis.0, x_axis.1),
focal,
)
.map_err(curve_error)?,
))
}
fn arc_of_circle_data(circle: Circle, first: f64, last: f64) -> PyResult<Geom2dData> {
if !first.is_finite() || !last.is_finite() || (last - first).abs() <= ANGULAR_TOLERANCE {
return Err(OCCError::new_err(
"an arc of a circle needs a non-zero, finite parameter range",
));
}
Ok(Geom2dData::Arc(
Arc::new(circle, first, last).map_err(curve_error)?,
))
}
fn arc_range(first: f64, last: f64, periodic: bool, sense: bool) -> PyResult<(f64, f64)> {
if !first.is_finite() || !last.is_finite() {
return Err(OCCError::new_err("an arc parameter range must be finite"));
}
if (last - first).abs() <= ANGULAR_TOLERANCE {
return Err(OCCError::new_err(
"an arc needs a non-zero parameter range",
));
}
if !periodic {
return Ok((first, last));
}
let counter_clockwise = (last - first).rem_euclid(std::f64::consts::TAU);
Ok(if sense {
(first, first + counter_clockwise)
} else {
(first, first - (std::f64::consts::TAU - counter_clockwise))
})
}
fn arc_of_ellipse_data(ellipse: Ellipse, first: f64, last: f64) -> PyResult<Geom2dData> {
if !first.is_finite() || !last.is_finite() || (last - first).abs() <= ANGULAR_TOLERANCE {
return Err(OCCError::new_err(
"an arc of an ellipse needs a non-zero, finite parameter range",
));
}
Ok(Geom2dData::ArcOfEllipse {
ellipse,
first,
last,
})
}
fn arc_of_hyperbola_data(
hyperbola: Hyperbola,
first: f64,
last: f64,
sense: bool,
) -> PyResult<Geom2dData> {
let (first, last) = if sense {
if last <= first {
return Err(OCCError::new_err(
"a forward arc of a hyperbola needs last > first",
));
}
(first, last)
} else {
(last.min(first), last.max(first))
};
Ok(Geom2dData::ArcOfHyperbola(
ArcOfHyperbola::new(hyperbola, first, last).map_err(curve_error)?,
))
}
fn arc_of_parabola_data(
parabola: Parabola,
first: f64,
last: f64,
sense: bool,
) -> PyResult<Geom2dData> {
let (first, last) = if sense {
if last <= first {
return Err(OCCError::new_err(
"a forward arc of a parabola needs last > first",
));
}
(first, last)
} else {
(last.min(first), last.max(first))
};
Ok(Geom2dData::ArcOfParabola(
ArcOfParabola::new(parabola, first, last).map_err(curve_error)?,
))
}
fn arc_through_points(
p1: (f64, f64),
p2: (f64, f64),
p3: (f64, f64),
) -> PyResult<Geom2dData> {
let center = circle_center(p1, p2, p3)?;
let radius = distance2(center, p1);
let x_axis = unit2((p1.0 - center.0, p1.1 - center.1))
.ok_or_else(|| OCCError::new_err("Two points are coincident"))?;
let circle = Circle::with_axis(
xyz(center.0, center.1),
unit_normal(),
xyz(x_axis.0, x_axis.1),
radius,
)
.map_err(curve_error)?;
let angle = |point: (f64, f64)| (point.1 - center.1).atan2(point.0 - center.0);
let first = angle(p1);
let through = (angle(p2) - first).rem_euclid(std::f64::consts::TAU);
let end = (angle(p3) - first).rem_euclid(std::f64::consts::TAU);
let last = if through < end {
first + end
} else {
first + end - std::f64::consts::TAU
};
arc_of_circle_data(circle, first, last)
}
fn ellipse_through_points(
s1: (f64, f64),
s2: (f64, f64),
center: (f64, f64),
) -> PyResult<Geom2dData> {
let axis = (s1.0 - center.0, s1.1 - center.1);
let major = (axis.0 * axis.0 + axis.1 * axis.1).sqrt();
let direction = unit2(axis)
.ok_or_else(|| OCCError::new_err("The first point must not be the centre"))?;
let minor = cross2((s2.0 - center.0, s2.1 - center.1), direction).abs();
ellipse_data(center, direction, major, minor)
}
fn hyperbola_through_points(
s1: (f64, f64),
s2: (f64, f64),
center: (f64, f64),
) -> PyResult<Geom2dData> {
let axis = (s1.0 - center.0, s1.1 - center.1);
let major = (axis.0 * axis.0 + axis.1 * axis.1).sqrt();
let direction = unit2(axis)
.ok_or_else(|| OCCError::new_err("The first point must not be the centre"))?;
let minor = cross2((s2.0 - center.0, s2.1 - center.1), direction).abs();
hyperbola_data(center, direction, major, minor)
}
fn parse_geometry2d_to_shape_args(
args: &Bound<'_, PyTuple>,
) -> PyResult<(bool, Option<f64>, Option<f64>)> {
match args.len() {
0 => Ok((false, None, None)),
1 => {
require_surface_or_face(&args.get_item(0)?)?;
Ok((true, None, None))
}
2 => {
let first: f64 = args
.get_item(0)?
.extract()
.map_err(|_| to_shape_error())?;
let last: f64 = args
.get_item(1)?
.extract()
.map_err(|_| to_shape_error())?;
Ok((false, Some(first), Some(last)))
}
3 => {
require_surface_or_face(&args.get_item(0)?)?;
let first: f64 = args
.get_item(1)?
.extract()
.map_err(|_| to_shape_error())?;
let last: f64 = args
.get_item(2)?
.extract()
.map_err(|_| to_shape_error())?;
Ok((true, Some(first), Some(last)))
}
_ => Err(to_shape_error()),
}
}
fn to_shape_error() -> PyErr {
PyTypeError::new_err(
"toShape accepts (), (first, last), (surface), (surface, first, last), \
(face) or (face, first, last)",
)
}
fn curve2d_entry(shape: Shape, record: &Geom2dData) -> PyClassInitializer<PyEdge> {
PyEdge::from_entry_with_curve2d(
EdgeEntry {
shape,
curve: None,
},
record.clone(),
)
}
fn curve2d_edge(
py: Python<'_>,
data: &Geom2dData,
first: Option<f64>,
last: Option<f64>,
) -> PyResult<Py<PyEdge>> {
let record = match (first, last) {
(Some(first), Some(last)) => data.restricted(first, last)?,
_ => data.clone(),
};
match &record {
Geom2dData::Line(_) => Err(OCCError::new_err(
"an infinite 2D line has no edge; pass a parameter range to toShape",
)),
Geom2dData::Segment {
origin,
direction,
first,
last,
} => {
let start = xyz(
origin.0 + first * direction.0,
origin.1 + first * direction.1,
);
let end = xyz(
origin.0 + last * direction.0,
origin.1 + last * direction.1,
);
let segment = LineSegment::new(start, end).map_err(curve_error)?;
let shape = shape_from_curve(&EdgeCurve::LineSegment(segment))?;
Ok(Py::new(py, curve2d_entry(shape, &record))?)
}
Geom2dData::Circle(circle) => {
let shape = shape_from_curve(&EdgeCurve::Circle(*circle))?;
Ok(Py::new(py, curve2d_entry(shape, &record))?)
}
Geom2dData::Arc(arc) => {
let shape = shape_from_curve(&EdgeCurve::Arc(*arc))?;
Ok(Py::new(py, curve2d_entry(shape, &record))?)
}
Geom2dData::Ellipse(ellipse) => {
let shape = shape_from_curve(&EdgeCurve::Ellipse(*ellipse))?;
Ok(Py::new(py, curve2d_entry(shape, &record))?)
}
Geom2dData::ArcOfEllipse {
ellipse,
first,
last,
} => {
let state = conic_arc_state(ellipse, *first, *last)?;
let shape = spline_state_shape(&state)?;
Ok(Py::new(
py,
PyEdge::from_bspline_with_curve2d(shape, state, record),
)?)
}
Geom2dData::Hyperbola(_) => Err(OCCError::new_err(
"a hyperbola is unbounded; pass a finite parameter range to toShape",
)),
Geom2dData::Parabola(_) => Err(OCCError::new_err(
"a parabola is unbounded; pass a finite parameter range to toShape",
)),
Geom2dData::ArcOfHyperbola(arc) => {
spline_curve2d_edge(py, &arc.nurbs().map_err(curve_error)?, record)
}
Geom2dData::ArcOfParabola(arc) => {
spline_curve2d_edge(py, &arc.nurbs().map_err(curve_error)?, record)
}
Geom2dData::Bezier(curve) => spline_curve2d_edge(py, &curve.nurbs(), record),
Geom2dData::Offset { .. } => Err(geom2d_not_implemented(
"OffsetCurve2d.toShape (no exact offset edge is bound in this slice)",
)),
Geom2dData::BSpline(_) => Err(geom2d_not_implemented(
"BSplineCurve2d.toShape (this slice binds no 2D B-spline kernel)",
)),
}
}
fn conic_arc_state(ellipse: &Ellipse, first: f64, last: f64) -> PyResult<SplineState> {
let blob = occt()
.conic_spline_state(
3,
ellipse.center(),
ellipse.normal(),
ellipse.x_axis(),
ellipse.major_radius(),
ellipse.minor_radius(),
first,
last,
true,
)
.ok_or_else(|| OCCError::new_err("conversion of the elliptical arc to a NURBS failed"))?;
SplineState::from_blob(&blob)
.ok_or_else(|| OCCError::new_err("conversion of the elliptical arc to a NURBS failed"))
}
fn spline_state_shape(state: &SplineState) -> PyResult<Shape> {
occt()
.make_bspline_poles_mults_knots(
&state.poles,
&state.weights,
&state.knots,
&state.mults,
state.degree,
state.periodic,
state.check_rational(),
)
.map_err(kernel_error)
}
fn spline_curve2d_edge(
py: Python<'_>,
curve: &NurbsCurve,
record: Geom2dData,
) -> PyResult<Py<PyEdge>> {
let state = nurbs_state(curve)?;
let shape = spline_state_shape(&state)?;
Py::new(py, PyEdge::from_bspline_with_curve2d(shape, state, record))
}
#[pyclass(name = "Line2d", module = "Part.Geom2d", extends = PyCurve2d)]
pub struct PyLine2d;
curve2d_initializer!(PyLine2d);
#[pymethods]
impl PyLine2d {
#[new]
#[pyo3(signature = (*args))]
fn new(args: &Bound<'_, PyTuple>) -> PyResult<PyClassInitializer<Self>> {
let data = match args.len() {
0 => line_data((0.0, 0.0), (1.0, 0.0))?,
1 => {
let data = geometry_data(&args.get_item(0)?)?;
if !matches!(data, Geom2dData::Line(_)) {
return Err(PyTypeError::new_err("Line2d copy expects a Line2d"));
}
data
}
2 => line_data(
vector2d(&args.get_item(0)?)?,
vector2d(&args.get_item(1)?)?,
)?,
_ => {
return Err(PyTypeError::new_err(
"Line2d constructor accepts:\n\
-- empty parameter list\n\
-- Line2d\n\
-- Point, Point",
))
}
};
Ok(Self::initializer(data))
}
#[getter]
fn Location(slf: &Bound<'_, Self>, py: Python<'_>) -> PyResult<PyObject> {
let data = geometry_data(slf.as_any())?;
let location = data
.location()
.ok_or_else(|| OCCError::new_err("the object is not a Line2d"))?;
py_vector2d(py, location)
}
#[setter]
fn set_Location(slf: &Bound<'_, Self>, value: &Bound<'_, PyAny>) -> PyResult<()> {
let location = vector2d(value)?;
let data = geometry_data(slf.as_any())?;
let updated = data
.with_location(location)
.ok_or_else(|| OCCError::new_err("the object is not a Line2d"))?;
replace_geometry_data(slf.as_any(), updated)
}
#[getter]
fn Direction(slf: &Bound<'_, Self>, py: Python<'_>) -> PyResult<PyObject> {
let data = geometry_data(slf.as_any())?;
let axis = data
.x_axis()
.ok_or_else(|| OCCError::new_err("the object is not a Line2d"))?;
py_vector2d(py, axis)
}
#[setter]
fn set_Direction(slf: &Bound<'_, Self>, value: &Bound<'_, PyAny>) -> PyResult<()> {
let direction = vector2d(value)?;
let data = geometry_data(slf.as_any())?;
let updated = data
.with_x_axis(direction)
.ok_or_else(|| OCCError::new_err("the direction must not be null"))?;
replace_geometry_data(slf.as_any(), updated)
}
}
#[pyclass(name = "Line2dSegment", module = "Part.Geom2d", extends = PyCurve2d)]
pub struct PyLine2dSegment;
curve2d_initializer!(PyLine2dSegment);
#[pymethods]
impl PyLine2dSegment {
#[new]
#[pyo3(signature = (*args))]
fn new(args: &Bound<'_, PyTuple>) -> PyResult<PyClassInitializer<Self>> {
let data = match args.len() {
0 => Geom2dData::Segment {
origin: (0.0, 0.0),
direction: (0.0, 1.0),
first: 0.0,
last: 1.0,
},
1 => {
let data = geometry_data(&args.get_item(0)?)?;
if !matches!(data, Geom2dData::Segment { .. }) {
return Err(PyTypeError::new_err(
"Line2dSegment copy expects a Line2dSegment",
));
}
data
}
2 => segment_data(
vector2d(&args.get_item(0)?)?,
vector2d(&args.get_item(1)?)?,
)?,
_ => {
return Err(PyTypeError::new_err(
"Line2dSegment constructor accepts:\n\
-- empty parameter list\n\
-- Line2dSegment\n\
-- Point1, Point2",
))
}
};
Ok(Self::initializer(data))
}
#[getter]
fn StartPoint(slf: &Bound<'_, Self>, py: Python<'_>) -> PyResult<PyObject> {
let data = geometry_data(slf.as_any())?;
let first = data
.first()
.ok_or_else(|| OCCError::new_err("the object is not a Line2dSegment"))?;
let point = data
.point(first)
.ok_or_else(|| OCCError::new_err("the object is not a Line2dSegment"))?;
py_vector2d(py, point)
}
#[setter]
fn set_StartPoint(slf: &Bound<'_, Self>, value: &Bound<'_, PyAny>) -> PyResult<()> {
let start = vector2d(value)?;
let data = geometry_data(slf.as_any())?;
let Geom2dData::Segment {
origin,
direction,
last,
..
} = data
else {
return Err(OCCError::new_err("the object is not a Line2dSegment"));
};
let end = (
origin.0 + last * direction.0,
origin.1 + last * direction.1,
);
replace_geometry_data(slf.as_any(), segment_data(start, end)?)
}
#[getter]
fn EndPoint(slf: &Bound<'_, Self>, py: Python<'_>) -> PyResult<PyObject> {
let data = geometry_data(slf.as_any())?;
let last = data
.last()
.ok_or_else(|| OCCError::new_err("the object is not a Line2dSegment"))?;
let point = data
.point(last)
.ok_or_else(|| OCCError::new_err("the object is not a Line2dSegment"))?;
py_vector2d(py, point)
}
#[setter]
fn set_EndPoint(slf: &Bound<'_, Self>, value: &Bound<'_, PyAny>) -> PyResult<()> {
let end = vector2d(value)?;
let data = geometry_data(slf.as_any())?;
let start = data
.point(data.first().unwrap_or(0.0))
.ok_or_else(|| OCCError::new_err("the object is not a Line2dSegment"))?;
replace_geometry_data(slf.as_any(), segment_data(start, end)?)
}
fn setParameterRange(slf: &Bound<'_, Self>, first: f64, last: f64) -> PyResult<()> {
if !first.is_finite() || !last.is_finite() {
return Err(OCCError::new_err(
"a Line2dSegment parameter range must be finite",
));
}
let data = geometry_data(slf.as_any())?;
let Geom2dData::Segment {
origin, direction, ..
} = data
else {
return Err(OCCError::new_err("the object is not a Line2dSegment"));
};
replace_geometry_data(
slf.as_any(),
Geom2dData::Segment {
origin,
direction,
first,
last,
},
)
}
}
#[pyclass(name = "Circle2d", module = "Part.Geom2d", extends = PyConic2d)]
pub struct PyCircle2d;
conic2d_initializer!(PyCircle2d);
#[pymethods]
impl PyCircle2d {
#[new]
#[pyo3(signature = (*args, **kwargs))]
fn new(
args: &Bound<'_, PyTuple>,
kwargs: Option<&Bound<'_, PyDict>>,
) -> PyResult<PyClassInitializer<Self>> {
if let Some(kwargs) = kwargs {
if !kwargs.is_empty() {
if !args.is_empty() {
return Err(PyTypeError::new_err(
"Circle2d takes either positional or keyword arguments, not both",
));
}
let center = dict_value(kwargs, &["Center", "center"])?;
let radius = dict_value(kwargs, &["Radius", "radius"])?;
let (Some(center), Some(radius)) = (center, radius) else {
return Err(PyTypeError::new_err(
"Circle2d keyword arguments are Center and Radius",
));
};
return Ok(Self::initializer(circle_data(
vector2d(¢er)?,
radius.extract()?,
)?));
}
}
let data = match args.len() {
0 => circle_data((0.0, 0.0), 1.0)?,
1 => {
let data = geometry_data(&args.get_item(0)?)?;
if !matches!(data, Geom2dData::Circle(_)) {
return Err(PyTypeError::new_err("Circle2d copy expects a Circle2d"));
}
data
}
2 => {
let first = args.get_item(0)?;
let second = args.get_item(1)?;
if let Ok(Geom2dData::Circle(circle)) = geometry_data(&first) {
let distance: f64 = second.extract().map_err(|_| {
PyTypeError::new_err("Circle2d(Circle, Distance) expects a float")
})?;
circle_data(xy(circle.center()), circle.radius() + distance)?
} else {
circle_data(
vector2d(&first)?,
second.extract().map_err(|_| {
PyTypeError::new_err("Circle2d(Center, Radius) expects a float")
})?,
)?
}
}
3 => {
let p1 = vector2d(&args.get_item(0)?)?;
let p2 = vector2d(&args.get_item(1)?)?;
let p3 = vector2d(&args.get_item(2)?)?;
let center = circle_center(p1, p2, p3)?;
let radius = distance2(center, p1);
let x_axis = unit2((p1.0 - center.0, p1.1 - center.1))
.ok_or_else(|| OCCError::new_err("Two points are coincident"))?;
Geom2dData::Circle(
Circle::with_axis(
xyz(center.0, center.1),
unit_normal(),
xyz(x_axis.0, x_axis.1),
radius,
)
.map_err(curve_error)?,
)
}
_ => {
return Err(PyTypeError::new_err(
"Circle2d constructor accepts:\n\
-- empty parameter list\n\
-- Circle2d\n\
-- Circle2d, Distance\n\
-- Center, Radius\n\
-- Point1, Point2, Point3",
))
}
};
Ok(Self::initializer(data))
}
#[getter]
fn Radius(slf: &Bound<'_, Self>) -> PyResult<f64> {
match geometry_data(slf.as_any())? {
Geom2dData::Circle(circle) => Ok(circle.radius()),
_ => Err(OCCError::new_err("the object is not a Circle2d")),
}
}
#[setter]
fn set_Radius(slf: &Bound<'_, Self>, value: f64) -> PyResult<()> {
let data = geometry_data(slf.as_any())?;
let Geom2dData::Circle(circle) = data else {
return Err(OCCError::new_err("the object is not a Circle2d"));
};
replace_geometry_data(slf.as_any(), circle_data(xy(circle.center()), value)?)
}
#[staticmethod]
fn getCircleCenter(
py: Python<'_>,
p1: &Bound<'_, PyAny>,
p2: &Bound<'_, PyAny>,
p3: &Bound<'_, PyAny>,
) -> PyResult<PyObject> {
let center = circle_center(vector2d(p1)?, vector2d(p2)?, vector2d(p3)?)?;
py_vector2d(py, center)
}
}
fn dict_value<'a>(
kwargs: &'a Bound<'a, PyDict>,
names: &[&str],
) -> PyResult<Option<Bound<'a, PyAny>>> {
for name in names {
if let Some(value) = kwargs.get_item(*name)? {
return Ok(Some(value));
}
}
Ok(None)
}
#[pyclass(name = "ArcOfCircle2d", module = "Part.Geom2d", extends = PyArcOfConic2d)]
pub struct PyArcOfCircle2d;
arc_of_conic2d_initializer!(PyArcOfCircle2d);
#[pymethods]
impl PyArcOfCircle2d {
#[new]
#[pyo3(signature = (*args))]
fn new(args: &Bound<'_, PyTuple>) -> PyResult<PyClassInitializer<Self>> {
let data = match args.len() {
3 | 4 => {
let first_arg = args.get_item(0)?;
if let Ok(Geom2dData::Circle(circle)) = geometry_data(&first_arg) {
let first: f64 = args.get_item(1)?.extract()?;
let last: f64 = args.get_item(2)?.extract()?;
let sense = if args.len() == 4 {
args.get_item(3)?.extract::<bool>()?
} else {
true
};
let (first, last) = arc_range(first, last, true, sense)?;
arc_of_circle_data(circle, first, last)?
} else {
if args.len() != 3 {
return Err(PyTypeError::new_err(
"ArcOfCircle2d constructor expects a circle curve and a \
parameter range or three points",
));
}
arc_through_points(
vector2d(&args.get_item(0)?)?,
vector2d(&args.get_item(1)?)?,
vector2d(&args.get_item(2)?)?,
)?
}
}
_ => {
return Err(PyTypeError::new_err(
"ArcOfCircle2d constructor expects a circle curve and a \
parameter range or three points",
))
}
};
Ok(Self::initializer(data))
}
#[getter]
fn Radius(slf: &Bound<'_, Self>) -> PyResult<f64> {
match geometry_data(slf.as_any())? {
Geom2dData::Arc(arc) => Ok(arc.circle().radius()),
_ => Err(OCCError::new_err("the object is not an ArcOfCircle2d")),
}
}
#[setter]
fn set_Radius(slf: &Bound<'_, Self>, value: f64) -> PyResult<()> {
let data = geometry_data(slf.as_any())?;
let Geom2dData::Arc(arc) = data else {
return Err(OCCError::new_err("the object is not an ArcOfCircle2d"));
};
let circle = Circle::with_axis(
arc.circle().center(),
arc.circle().normal(),
arc.circle().x_axis(),
value,
)
.map_err(curve_error)?;
replace_geometry_data(
slf.as_any(),
arc_of_circle_data(circle, arc.first(), arc.last())?,
)
}
#[getter]
fn Circle(slf: &Bound<'_, Self>, py: Python<'_>) -> PyResult<Py<PyCircle2d>> {
match geometry_data(slf.as_any())? {
Geom2dData::Arc(arc) => {
Py::new(py, PyCircle2d::initializer(Geom2dData::Circle(arc.circle())))
}
_ => Err(OCCError::new_err("the object is not an ArcOfCircle2d")),
}
}
}
#[pyclass(name = "Ellipse2d", module = "Part.Geom2d", extends = PyConic2d)]
pub struct PyEllipse2d;
conic2d_initializer!(PyEllipse2d);
#[pymethods]
impl PyEllipse2d {
#[new]
#[pyo3(signature = (*args))]
fn new(args: &Bound<'_, PyTuple>) -> PyResult<PyClassInitializer<Self>> {
let data = match args.len() {
0 => ellipse_data((0.0, 0.0), (1.0, 0.0), 2.0, 1.0)?,
1 => {
let data = geometry_data(&args.get_item(0)?)?;
if !matches!(data, Geom2dData::Ellipse(_)) {
return Err(PyTypeError::new_err("Ellipse2d copy expects an Ellipse2d"));
}
data
}
3 => {
let major: Result<f64, _> = args.get_item(1)?.extract();
let minor: Result<f64, _> = args.get_item(2)?.extract();
match (major, minor) {
(Ok(major), Ok(minor)) => {
let center = vector2d(&args.get_item(0)?)?;
ellipse_data(center, (0.0, 1.0), major, minor)?
}
_ => ellipse_through_points(
vector2d(&args.get_item(0)?)?,
vector2d(&args.get_item(1)?)?,
vector2d(&args.get_item(2)?)?,
)?,
}
}
_ => {
return Err(PyTypeError::new_err(
"Ellipse2d constructor accepts:\n\
-- empty parameter list\n\
-- Ellipse2d\n\
-- Center, MajorRadius, MinorRadius\n\
-- Point, Point, Point",
))
}
};
Ok(Self::initializer(data))
}
#[getter]
fn MajorRadius(slf: &Bound<'_, Self>) -> PyResult<f64> {
match geometry_data(slf.as_any())? {
Geom2dData::Ellipse(ellipse) => Ok(ellipse.major_radius()),
_ => Err(OCCError::new_err("the object is not an Ellipse2d")),
}
}
#[setter]
fn set_MajorRadius(slf: &Bound<'_, Self>, value: f64) -> PyResult<()> {
let data = geometry_data(slf.as_any())?;
let Geom2dData::Ellipse(ellipse) = data else {
return Err(OCCError::new_err("the object is not an Ellipse2d"));
};
replace_geometry_data(
slf.as_any(),
ellipse_data(
xy(ellipse.center()),
xy(ellipse.x_axis()),
value,
ellipse.minor_radius(),
)?,
)
}
#[getter]
fn MinorRadius(slf: &Bound<'_, Self>) -> PyResult<f64> {
match geometry_data(slf.as_any())? {
Geom2dData::Ellipse(ellipse) => Ok(ellipse.minor_radius()),
_ => Err(OCCError::new_err("the object is not an Ellipse2d")),
}
}
#[setter]
fn set_MinorRadius(slf: &Bound<'_, Self>, value: f64) -> PyResult<()> {
let data = geometry_data(slf.as_any())?;
let Geom2dData::Ellipse(ellipse) = data else {
return Err(OCCError::new_err("the object is not an Ellipse2d"));
};
replace_geometry_data(
slf.as_any(),
ellipse_data(
xy(ellipse.center()),
xy(ellipse.x_axis()),
ellipse.major_radius(),
value,
)?,
)
}
#[getter]
fn Focal(slf: &Bound<'_, Self>) -> PyResult<f64> {
match geometry_data(slf.as_any())? {
Geom2dData::Ellipse(ellipse) => {
let (a, b) = (ellipse.major_radius(), ellipse.minor_radius());
Ok((a * a - b * b).max(0.0).sqrt())
}
_ => Err(OCCError::new_err("the object is not an Ellipse2d")),
}
}
#[getter]
fn Focus1(slf: &Bound<'_, Self>, py: Python<'_>) -> PyResult<PyObject> {
match geometry_data(slf.as_any())? {
Geom2dData::Ellipse(ellipse) => {
let (a, b) = (ellipse.major_radius(), ellipse.minor_radius());
let focal = (a * a - b * b).max(0.0).sqrt();
let x = xy(ellipse.x_axis());
let center = xy(ellipse.center());
py_vector2d(py, (center.0 + focal * x.0, center.1 + focal * x.1))
}
_ => Err(OCCError::new_err("the object is not an Ellipse2d")),
}
}
#[getter]
fn Focus2(slf: &Bound<'_, Self>, py: Python<'_>) -> PyResult<PyObject> {
match geometry_data(slf.as_any())? {
Geom2dData::Ellipse(ellipse) => {
let (a, b) = (ellipse.major_radius(), ellipse.minor_radius());
let focal = (a * a - b * b).max(0.0).sqrt();
let x = xy(ellipse.x_axis());
let center = xy(ellipse.center());
py_vector2d(py, (center.0 - focal * x.0, center.1 - focal * x.1))
}
_ => Err(OCCError::new_err("the object is not an Ellipse2d")),
}
}
}
#[pyclass(name = "ArcOfEllipse2d", module = "Part.Geom2d", extends = PyArcOfConic2d)]
pub struct PyArcOfEllipse2d;
arc_of_conic2d_initializer!(PyArcOfEllipse2d);
#[pymethods]
impl PyArcOfEllipse2d {
#[new]
#[pyo3(signature = (*args))]
fn new(args: &Bound<'_, PyTuple>) -> PyResult<PyClassInitializer<Self>> {
if args.len() < 3 || args.len() > 4 {
return Err(PyTypeError::new_err(
"ArcOfEllipse2d constructor expects an ellipse curve and a \
parameter range",
));
}
let Geom2dData::Ellipse(ellipse) = geometry_data(&args.get_item(0)?)? else {
return Err(PyTypeError::new_err(
"ArcOfEllipse2d expects an Ellipse2d",
));
};
let first: f64 = args.get_item(1)?.extract()?;
let last: f64 = args.get_item(2)?.extract()?;
let sense = if args.len() == 4 {
args.get_item(3)?.extract::<bool>()?
} else {
true
};
let (first, last) = arc_range(first, last, true, sense)?;
Ok(Self::initializer(arc_of_ellipse_data(
ellipse, first, last,
)?))
}
#[getter]
fn MajorRadius(slf: &Bound<'_, Self>) -> PyResult<f64> {
match geometry_data(slf.as_any())? {
Geom2dData::ArcOfEllipse { ellipse, .. } => Ok(ellipse.major_radius()),
_ => Err(OCCError::new_err("the object is not an ArcOfEllipse2d")),
}
}
#[setter]
fn set_MajorRadius(slf: &Bound<'_, Self>, value: f64) -> PyResult<()> {
let data = geometry_data(slf.as_any())?;
let Geom2dData::ArcOfEllipse {
ellipse,
first,
last,
} = data
else {
return Err(OCCError::new_err("the object is not an ArcOfEllipse2d"));
};
replace_geometry_data(
slf.as_any(),
Geom2dData::ArcOfEllipse {
ellipse: Ellipse::with_axis(
ellipse.center(),
ellipse.normal(),
ellipse.x_axis(),
value,
ellipse.minor_radius(),
)
.map_err(curve_error)?,
first,
last,
},
)
}
#[getter]
fn MinorRadius(slf: &Bound<'_, Self>) -> PyResult<f64> {
match geometry_data(slf.as_any())? {
Geom2dData::ArcOfEllipse { ellipse, .. } => Ok(ellipse.minor_radius()),
_ => Err(OCCError::new_err("the object is not an ArcOfEllipse2d")),
}
}
#[setter]
fn set_MinorRadius(slf: &Bound<'_, Self>, value: f64) -> PyResult<()> {
let data = geometry_data(slf.as_any())?;
let Geom2dData::ArcOfEllipse {
ellipse,
first,
last,
} = data
else {
return Err(OCCError::new_err("the object is not an ArcOfEllipse2d"));
};
replace_geometry_data(
slf.as_any(),
Geom2dData::ArcOfEllipse {
ellipse: Ellipse::with_axis(
ellipse.center(),
ellipse.normal(),
ellipse.x_axis(),
ellipse.major_radius(),
value,
)
.map_err(curve_error)?,
first,
last,
},
)
}
#[getter]
fn Ellipse(slf: &Bound<'_, Self>, py: Python<'_>) -> PyResult<Py<PyEllipse2d>> {
match geometry_data(slf.as_any())? {
Geom2dData::ArcOfEllipse { ellipse, .. } => {
Py::new(py, PyEllipse2d::initializer(Geom2dData::Ellipse(ellipse)))
}
_ => Err(OCCError::new_err("the object is not an ArcOfEllipse2d")),
}
}
}
#[pyclass(name = "Hyperbola2d", module = "Part.Geom2d", extends = PyConic2d)]
pub struct PyHyperbola2d;
conic2d_initializer!(PyHyperbola2d);
#[pymethods]
impl PyHyperbola2d {
#[new]
#[pyo3(signature = (*args))]
fn new(args: &Bound<'_, PyTuple>) -> PyResult<PyClassInitializer<Self>> {
let data = match args.len() {
0 => hyperbola_data((0.0, 0.0), (1.0, 0.0), 2.0, 1.0)?,
1 => {
let data = geometry_data(&args.get_item(0)?)?;
if !matches!(data, Geom2dData::Hyperbola(_)) {
return Err(PyTypeError::new_err(
"Hyperbola2d copy expects a Hyperbola2d",
));
}
data
}
3 => {
let major: Result<f64, _> = args.get_item(1)?.extract();
let minor: Result<f64, _> = args.get_item(2)?.extract();
match (major, minor) {
(Ok(major), Ok(minor)) => {
let center = vector2d(&args.get_item(0)?)?;
hyperbola_data(center, (0.0, 1.0), major, minor)?
}
_ => hyperbola_through_points(
vector2d(&args.get_item(0)?)?,
vector2d(&args.get_item(1)?)?,
vector2d(&args.get_item(2)?)?,
)?,
}
}
_ => {
return Err(PyTypeError::new_err(
"Hyperbola2d constructor accepts:\n\
-- empty parameter list\n\
-- Hyperbola2d\n\
-- Center, MajorRadius, MinorRadius\n\
-- Point, Point, Point",
))
}
};
Ok(Self::initializer(data))
}
#[getter]
fn MajorRadius(slf: &Bound<'_, Self>) -> PyResult<f64> {
match geometry_data(slf.as_any())? {
Geom2dData::Hyperbola(hyperbola) => Ok(hyperbola.major_radius()),
_ => Err(OCCError::new_err("the object is not a Hyperbola2d")),
}
}
#[setter]
fn set_MajorRadius(slf: &Bound<'_, Self>, value: f64) -> PyResult<()> {
let data = geometry_data(slf.as_any())?;
let Geom2dData::Hyperbola(hyperbola) = data else {
return Err(OCCError::new_err("the object is not a Hyperbola2d"));
};
replace_geometry_data(
slf.as_any(),
hyperbola_data(
xy(hyperbola.center()),
xy(hyperbola.x_axis()),
value,
hyperbola.minor_radius(),
)?,
)
}
#[getter]
fn MinorRadius(slf: &Bound<'_, Self>) -> PyResult<f64> {
match geometry_data(slf.as_any())? {
Geom2dData::Hyperbola(hyperbola) => Ok(hyperbola.minor_radius()),
_ => Err(OCCError::new_err("the object is not a Hyperbola2d")),
}
}
#[setter]
fn set_MinorRadius(slf: &Bound<'_, Self>, value: f64) -> PyResult<()> {
let data = geometry_data(slf.as_any())?;
let Geom2dData::Hyperbola(hyperbola) = data else {
return Err(OCCError::new_err("the object is not a Hyperbola2d"));
};
replace_geometry_data(
slf.as_any(),
hyperbola_data(
xy(hyperbola.center()),
xy(hyperbola.x_axis()),
hyperbola.major_radius(),
value,
)?,
)
}
#[getter]
fn Focal(slf: &Bound<'_, Self>) -> PyResult<f64> {
match geometry_data(slf.as_any())? {
Geom2dData::Hyperbola(hyperbola) => Ok(hyperbola.focal()),
_ => Err(OCCError::new_err("the object is not a Hyperbola2d")),
}
}
#[getter]
fn Focus1(slf: &Bound<'_, Self>, py: Python<'_>) -> PyResult<PyObject> {
match geometry_data(slf.as_any())? {
Geom2dData::Hyperbola(hyperbola) => py_vector2d(py, xy(hyperbola.focus1())),
_ => Err(OCCError::new_err("the object is not a Hyperbola2d")),
}
}
#[getter]
fn Focus2(slf: &Bound<'_, Self>, py: Python<'_>) -> PyResult<PyObject> {
match geometry_data(slf.as_any())? {
Geom2dData::Hyperbola(hyperbola) => py_vector2d(py, xy(hyperbola.focus2())),
_ => Err(OCCError::new_err("the object is not a Hyperbola2d")),
}
}
}
#[pyclass(name = "ArcOfHyperbola2d", module = "Part.Geom2d", extends = PyArcOfConic2d)]
pub struct PyArcOfHyperbola2d;
arc_of_conic2d_initializer!(PyArcOfHyperbola2d);
#[pymethods]
impl PyArcOfHyperbola2d {
#[new]
#[pyo3(signature = (*args))]
fn new(args: &Bound<'_, PyTuple>) -> PyResult<PyClassInitializer<Self>> {
if args.len() < 3 || args.len() > 4 {
return Err(PyTypeError::new_err(
"ArcOfHyperbola2d constructor expects a hyperbola curve and a \
parameter range",
));
}
let Geom2dData::Hyperbola(hyperbola) = geometry_data(&args.get_item(0)?)? else {
return Err(PyTypeError::new_err(
"ArcOfHyperbola2d expects a Hyperbola2d",
));
};
let first: f64 = args.get_item(1)?.extract()?;
let last: f64 = args.get_item(2)?.extract()?;
let sense = if args.len() == 4 {
args.get_item(3)?.extract::<bool>()?
} else {
true
};
Ok(Self::initializer(arc_of_hyperbola_data(
hyperbola, first, last, sense,
)?))
}
#[getter]
fn MajorRadius(slf: &Bound<'_, Self>) -> PyResult<f64> {
match geometry_data(slf.as_any())? {
Geom2dData::ArcOfHyperbola(arc) => Ok(arc.hyperbola().major_radius()),
_ => Err(OCCError::new_err("the object is not an ArcOfHyperbola2d")),
}
}
#[setter]
fn set_MajorRadius(slf: &Bound<'_, Self>, value: f64) -> PyResult<()> {
let Geom2dData::ArcOfHyperbola(arc) = geometry_data(slf.as_any())? else {
return Err(OCCError::new_err("the object is not an ArcOfHyperbola2d"));
};
let hyperbola = Hyperbola::with_axis(
arc.hyperbola().center(),
arc.hyperbola().normal(),
arc.hyperbola().x_axis(),
value,
arc.hyperbola().minor_radius(),
)
.map_err(curve_error)?;
replace_geometry_data(
slf.as_any(),
Geom2dData::ArcOfHyperbola(
ArcOfHyperbola::new(hyperbola, arc.first(), arc.last()).map_err(curve_error)?,
),
)
}
#[getter]
fn MinorRadius(slf: &Bound<'_, Self>) -> PyResult<f64> {
match geometry_data(slf.as_any())? {
Geom2dData::ArcOfHyperbola(arc) => Ok(arc.hyperbola().minor_radius()),
_ => Err(OCCError::new_err("the object is not an ArcOfHyperbola2d")),
}
}
#[setter]
fn set_MinorRadius(slf: &Bound<'_, Self>, value: f64) -> PyResult<()> {
let Geom2dData::ArcOfHyperbola(arc) = geometry_data(slf.as_any())? else {
return Err(OCCError::new_err("the object is not an ArcOfHyperbola2d"));
};
let hyperbola = Hyperbola::with_axis(
arc.hyperbola().center(),
arc.hyperbola().normal(),
arc.hyperbola().x_axis(),
arc.hyperbola().major_radius(),
value,
)
.map_err(curve_error)?;
replace_geometry_data(
slf.as_any(),
Geom2dData::ArcOfHyperbola(
ArcOfHyperbola::new(hyperbola, arc.first(), arc.last()).map_err(curve_error)?,
),
)
}
#[getter]
fn Hyperbola(slf: &Bound<'_, Self>, py: Python<'_>) -> PyResult<Py<PyHyperbola2d>> {
match geometry_data(slf.as_any())? {
Geom2dData::ArcOfHyperbola(arc) => Py::new(
py,
PyHyperbola2d::initializer(Geom2dData::Hyperbola(arc.hyperbola())),
),
_ => Err(OCCError::new_err("the object is not an ArcOfHyperbola2d")),
}
}
}
#[pyclass(name = "Parabola2d", module = "Part.Geom2d", extends = PyConic2d)]
pub struct PyParabola2d;
conic2d_initializer!(PyParabola2d);
#[pymethods]
impl PyParabola2d {
#[new]
#[pyo3(signature = (*args))]
fn new(args: &Bound<'_, PyTuple>) -> PyResult<PyClassInitializer<Self>> {
let data = match args.len() {
0 => parabola_data((0.0, 0.0), (1.0, 0.0), 1.0)?,
1 => {
let data = geometry_data(&args.get_item(0)?)?;
if !matches!(data, Geom2dData::Parabola(_)) {
return Err(PyTypeError::new_err(
"Parabola2d copy expects a Parabola2d",
));
}
data
}
_ => {
return Err(PyTypeError::new_err(
"Parabola2d constructor accepts:\n\
-- empty parameter list\n\
-- Parabola2d",
))
}
};
Ok(Self::initializer(data))
}
#[getter]
fn Focal(slf: &Bound<'_, Self>) -> PyResult<f64> {
match geometry_data(slf.as_any())? {
Geom2dData::Parabola(parabola) => Ok(parabola.focal()),
_ => Err(OCCError::new_err("the object is not a Parabola2d")),
}
}
#[setter]
fn set_Focal(slf: &Bound<'_, Self>, value: f64) -> PyResult<()> {
let data = geometry_data(slf.as_any())?;
let Geom2dData::Parabola(parabola) = data else {
return Err(OCCError::new_err("the object is not a Parabola2d"));
};
replace_geometry_data(
slf.as_any(),
parabola_data(xy(parabola.vertex()), xy(parabola.x_axis()), value)?,
)
}
#[getter]
fn Focus(slf: &Bound<'_, Self>, py: Python<'_>) -> PyResult<PyObject> {
match geometry_data(slf.as_any())? {
Geom2dData::Parabola(parabola) => py_vector2d(py, xy(parabola.focus())),
_ => Err(OCCError::new_err("the object is not a Parabola2d")),
}
}
#[getter]
fn Parameter(slf: &Bound<'_, Self>) -> PyResult<f64> {
match geometry_data(slf.as_any())? {
Geom2dData::Parabola(parabola) => Ok(parabola.parameter()),
_ => Err(OCCError::new_err("the object is not a Parabola2d")),
}
}
}
#[pyclass(name = "ArcOfParabola2d", module = "Part.Geom2d", extends = PyArcOfConic2d)]
pub struct PyArcOfParabola2d;
arc_of_conic2d_initializer!(PyArcOfParabola2d);
#[pymethods]
impl PyArcOfParabola2d {
#[new]
#[pyo3(signature = (*args))]
fn new(args: &Bound<'_, PyTuple>) -> PyResult<PyClassInitializer<Self>> {
if args.len() < 3 || args.len() > 4 {
return Err(PyTypeError::new_err(
"ArcOfParabola2d constructor expects a parabola curve and a \
parameter range",
));
}
let Geom2dData::Parabola(parabola) = geometry_data(&args.get_item(0)?)? else {
return Err(PyTypeError::new_err(
"ArcOfParabola2d expects a Parabola2d",
));
};
let first: f64 = args.get_item(1)?.extract()?;
let last: f64 = args.get_item(2)?.extract()?;
let sense = if args.len() == 4 {
args.get_item(3)?.extract::<bool>()?
} else {
true
};
Ok(Self::initializer(arc_of_parabola_data(
parabola, first, last, sense,
)?))
}
#[getter]
fn Focal(slf: &Bound<'_, Self>) -> PyResult<f64> {
match geometry_data(slf.as_any())? {
Geom2dData::ArcOfParabola(arc) => Ok(arc.parabola().focal()),
_ => Err(OCCError::new_err("the object is not an ArcOfParabola2d")),
}
}
#[setter]
fn set_Focal(slf: &Bound<'_, Self>, value: f64) -> PyResult<()> {
let Geom2dData::ArcOfParabola(arc) = geometry_data(slf.as_any())? else {
return Err(OCCError::new_err("the object is not an ArcOfParabola2d"));
};
let parabola = Parabola::with_axis(
arc.parabola().vertex(),
arc.parabola().normal(),
arc.parabola().x_axis(),
value,
)
.map_err(curve_error)?;
replace_geometry_data(
slf.as_any(),
Geom2dData::ArcOfParabola(
ArcOfParabola::new(parabola, arc.first(), arc.last()).map_err(curve_error)?,
),
)
}
#[getter]
fn Parabola(slf: &Bound<'_, Self>, py: Python<'_>) -> PyResult<Py<PyParabola2d>> {
match geometry_data(slf.as_any())? {
Geom2dData::ArcOfParabola(arc) => Py::new(
py,
PyParabola2d::initializer(Geom2dData::Parabola(arc.parabola())),
),
_ => Err(OCCError::new_err("the object is not an ArcOfParabola2d")),
}
}
}
#[pyclass(name = "OffsetCurve2d", module = "Part.Geom2d", extends = PyCurve2d)]
pub struct PyOffsetCurve2d;
curve2d_initializer!(PyOffsetCurve2d);
#[pymethods]
impl PyOffsetCurve2d {
#[new]
fn new(basis: &Bound<'_, PyAny>, offset: f64) -> PyResult<PyClassInitializer<Self>> {
if !offset.is_finite() {
return Err(OCCError::new_err("the offset value must be finite"));
}
let basis = geometry_data(basis)?;
Ok(Self::initializer(Geom2dData::Offset {
basis: Box::new(basis),
offset,
}))
}
#[getter]
fn OffsetValue(slf: &Bound<'_, Self>) -> PyResult<f64> {
match geometry_data(slf.as_any())? {
Geom2dData::Offset { offset, .. } => Ok(offset),
_ => Err(OCCError::new_err("the object is not an OffsetCurve2d")),
}
}
#[setter]
fn set_OffsetValue(slf: &Bound<'_, Self>, value: f64) -> PyResult<()> {
if !value.is_finite() {
return Err(OCCError::new_err("the offset value must be finite"));
}
let data = geometry_data(slf.as_any())?;
let Geom2dData::Offset { basis, .. } = data else {
return Err(OCCError::new_err("the object is not an OffsetCurve2d"));
};
replace_geometry_data(
slf.as_any(),
Geom2dData::Offset {
basis,
offset: value,
},
)
}
#[getter]
fn BasisCurve(slf: &Bound<'_, Self>, py: Python<'_>) -> PyResult<PyObject> {
match geometry_data(slf.as_any())? {
Geom2dData::Offset { basis, .. } => geometry2d_to_py(py, *basis),
_ => Err(OCCError::new_err("the object is not an OffsetCurve2d")),
}
}
#[setter]
fn set_BasisCurve(slf: &Bound<'_, Self>, value: &Bound<'_, PyAny>) -> PyResult<()> {
let basis = geometry_data(value)?;
let data = geometry_data(slf.as_any())?;
let Geom2dData::Offset { offset, .. } = data else {
return Err(OCCError::new_err("the object is not an OffsetCurve2d"));
};
replace_geometry_data(
slf.as_any(),
Geom2dData::Offset {
basis: Box::new(basis),
offset,
},
)
}
}
#[pyclass(name = "BezierCurve2d", module = "Part.Geom2d", extends = PyCurve2d)]
pub struct PyBezierCurve2d;
curve2d_initializer!(PyBezierCurve2d);
fn bezier2d(slf: &Bound<'_, PyAny>) -> PyResult<BezierCurve> {
match geometry_data(slf)? {
Geom2dData::Bezier(curve) => Ok(curve),
_ => Err(OCCError::new_err("the object is not a BezierCurve2d")),
}
}
#[pymethods]
impl PyBezierCurve2d {
#[new]
#[pyo3(signature = (*args))]
fn new(args: &Bound<'_, PyTuple>) -> PyResult<PyClassInitializer<Self>> {
let data = match args.len() {
0 => Geom2dData::Bezier(
BezierCurve::with_range(
vec![xyz(0.0, 0.0), xyz(0.0, 1.0)],
vec![1.0, 1.0],
0.0,
1.0,
)
.map_err(curve_error)?,
),
1 => {
let data = geometry_data(&args.get_item(0)?)?;
if !matches!(data, Geom2dData::Bezier(_)) {
return Err(PyTypeError::new_err(
"BezierCurve2d copy expects a BezierCurve2d",
));
}
data
}
_ => {
return Err(PyTypeError::new_err(
"BezierCurve2d constructor accepts:\n\
-- empty parameter list\n\
-- BezierCurve2d",
))
}
};
Ok(Self::initializer(data))
}
#[getter]
fn Degree(slf: &Bound<'_, Self>) -> PyResult<i32> {
Ok(bezier2d(slf.as_any())?.degree())
}
#[getter]
fn MaxDegree(&self) -> i32 {
25
}
#[getter]
fn NbPoles(slf: &Bound<'_, Self>) -> PyResult<usize> {
Ok(bezier2d(slf.as_any())?.poles().len())
}
#[getter]
fn StartPoint(slf: &Bound<'_, Self>, py: Python<'_>) -> PyResult<PyObject> {
let curve = bezier2d(slf.as_any())?;
py_vector2d(py, xy(curve.start()))
}
#[getter]
fn EndPoint(slf: &Bound<'_, Self>, py: Python<'_>) -> PyResult<PyObject> {
let curve = bezier2d(slf.as_any())?;
py_vector2d(py, xy(curve.end()))
}
fn isRational(slf: &Bound<'_, Self>) -> PyResult<bool> {
Ok(bezier2d(slf.as_any())?.is_rational())
}
fn isPeriodic(&self) -> bool {
false
}
fn isClosed(slf: &Bound<'_, Self>) -> PyResult<bool> {
Ok(bezier2d(slf.as_any())?.is_closed())
}
fn getPole(slf: &Bound<'_, Self>, py: Python<'_>, index: i32) -> PyResult<PyObject> {
let curve = bezier2d(slf.as_any())?;
let position = index_at(curve.poles().len(), index)? as usize - 1;
py_vector2d(py, xy(curve.poles()[position]))
}
fn getPoles(slf: &Bound<'_, Self>, py: Python<'_>) -> PyResult<Vec<PyObject>> {
bezier2d(slf.as_any())?
.poles()
.iter()
.map(|pole| py_vector2d(py, xy(*pole)))
.collect()
}
#[pyo3(signature = (index, pole, weight = None))]
fn setPole(
slf: &Bound<'_, Self>,
index: i32,
pole: &Bound<'_, PyAny>,
weight: Option<f64>,
) -> PyResult<()> {
let point = vector2d(pole)?;
let mut curve = bezier2d(slf.as_any())?;
let position = index_at(curve.poles().len(), index)? as usize - 1;
curve
.set_pole(position, xyz(point.0, point.1), weight)
.map_err(curve_error)?;
replace_geometry_data(slf.as_any(), Geom2dData::Bezier(curve))
}
fn setPoles(slf: &Bound<'_, Self>, poles: &Bound<'_, PyAny>) -> PyResult<()> {
let mut parsed = Vec::new();
for point in poles.try_iter()? {
let point = vector2d(&point?)?;
parsed.push(xyz(point.0, point.1));
}
let mut curve = bezier2d(slf.as_any())?;
curve.set_poles(parsed).map_err(curve_error)?;
replace_geometry_data(slf.as_any(), Geom2dData::Bezier(curve))
}
fn getWeight(slf: &Bound<'_, Self>, index: i32) -> PyResult<f64> {
let curve = bezier2d(slf.as_any())?;
let position = index_at(curve.weights().len(), index)? as usize - 1;
Ok(curve.weights()[position])
}
fn getWeights(slf: &Bound<'_, Self>) -> PyResult<Vec<f64>> {
Ok(bezier2d(slf.as_any())?.weights().to_vec())
}
fn setWeight(slf: &Bound<'_, Self>, index: i32, weight: f64) -> PyResult<()> {
let mut curve = bezier2d(slf.as_any())?;
let position = index_at(curve.weights().len(), index)? as usize - 1;
curve.set_weight(position, weight).map_err(curve_error)?;
replace_geometry_data(slf.as_any(), Geom2dData::Bezier(curve))
}
fn segment(slf: &Bound<'_, Self>, u1: f64, u2: f64) -> PyResult<()> {
let curve = bezier2d(slf.as_any())?;
let trimmed = curve.trim(u1, u2).map_err(curve_error)?;
replace_geometry_data(slf.as_any(), Geom2dData::Bezier(trimmed))
}
#[pyo3(signature = (*_args))]
fn increase(&self, _args: &Bound<'_, PyTuple>) -> PyResult<()> {
Err(geom2d_not_implemented("BezierCurve2d.increase"))
}
#[pyo3(signature = (*_args))]
fn insertPoleAfter(&self, _args: &Bound<'_, PyTuple>) -> PyResult<()> {
Err(geom2d_not_implemented("BezierCurve2d.insertPoleAfter"))
}
#[pyo3(signature = (*_args))]
fn insertPoleBefore(&self, _args: &Bound<'_, PyTuple>) -> PyResult<()> {
Err(geom2d_not_implemented("BezierCurve2d.insertPoleBefore"))
}
#[pyo3(signature = (*_args))]
fn removePole(&self, _args: &Bound<'_, PyTuple>) -> PyResult<()> {
Err(geom2d_not_implemented("BezierCurve2d.removePole"))
}
#[pyo3(signature = (*_args))]
fn getResolution(&self, _args: &Bound<'_, PyTuple>) -> PyResult<f64> {
Err(geom2d_not_implemented("BezierCurve2d.getResolution"))
}
}
#[pyclass(name = "BSplineCurve2d", module = "Part.Geom2d", extends = PyCurve2d)]
pub struct PyBSplineCurve2d;
curve2d_initializer!(PyBSplineCurve2d);
impl PyBSplineCurve2d {
fn point_count(value: &Bound<'_, PyAny>) -> PyResult<usize> {
match geometry_data(value)? {
Geom2dData::BSpline(points) => Ok(points.len()),
_ => Err(OCCError::new_err("the object is not a BSplineCurve2d")),
}
}
fn record(slf: &Bound<'_, Self>, points: &Bound<'_, PyAny>) -> PyResult<()> {
let mut parsed = Vec::new();
for point in points.try_iter()? {
parsed.push(vector2d(&point?)?);
}
if parsed.len() < 2 {
return Err(OCCError::new_err(
"a 2D B-spline needs at least two points",
));
}
replace_geometry_data(slf.as_any(), Geom2dData::BSpline(parsed))
}
}
#[pymethods]
impl PyBSplineCurve2d {
#[new]
fn new() -> PyResult<PyClassInitializer<Self>> {
Ok(Self::initializer(Geom2dData::BSpline(Vec::new())))
}
#[pyo3(signature = (Points, PeriodicFlag = false, Tolerance = 1e-7))]
fn interpolate(
slf: &Bound<'_, Self>,
Points: &Bound<'_, PyAny>,
PeriodicFlag: bool,
Tolerance: f64,
) -> PyResult<()> {
let _ = (PeriodicFlag, Tolerance);
Self::record(slf, Points)
}
#[pyo3(signature = (Points, DegMax = 8, Continuity = "C2", Tolerance = 1e-3, DegMin = 3, ParamType = "ChordLength"))]
fn approximate(
slf: &Bound<'_, Self>,
Points: &Bound<'_, PyAny>,
DegMax: i32,
Continuity: &str,
Tolerance: f64,
DegMin: i32,
ParamType: &str,
) -> PyResult<()> {
let _ = (DegMax, Continuity, Tolerance, DegMin, ParamType);
Self::record(slf, Points)
}
#[pyo3(signature = (u, mult = 1, tol = 0.0))]
fn insertKnot(&self, u: f64, mult: i32, tol: f64) -> PyResult<()> {
let _ = (u, mult, tol);
Err(OCCError::new_err(
"Part.Geom2d.BSplineCurve2d.insertKnot is not implemented \
(this slice binds no 2D B-spline kernel)",
))
}
fn __repr__(slf: &Bound<'_, Self>) -> PyResult<String> {
Ok(format!(
"Part.Geom2d.BSplineCurve2d({} points)",
Self::point_count(slf.as_any())?
))
}
}
fn geometry2d_to_py(py: Python<'_>, data: Geom2dData) -> PyResult<PyObject> {
Ok(match data {
data @ Geom2dData::Line(_) => Py::new(py, PyLine2d::initializer(data))?.into_any(),
data @ Geom2dData::Segment { .. } => {
Py::new(py, PyLine2dSegment::initializer(data))?.into_any()
}
data @ Geom2dData::Circle(_) => Py::new(py, PyCircle2d::initializer(data))?.into_any(),
data @ Geom2dData::Arc(_) => {
Py::new(py, PyArcOfCircle2d::initializer(data))?.into_any()
}
data @ Geom2dData::Ellipse(_) => Py::new(py, PyEllipse2d::initializer(data))?.into_any(),
data @ Geom2dData::ArcOfEllipse { .. } => {
Py::new(py, PyArcOfEllipse2d::initializer(data))?.into_any()
}
data @ Geom2dData::Hyperbola(_) => {
Py::new(py, PyHyperbola2d::initializer(data))?.into_any()
}
data @ Geom2dData::ArcOfHyperbola(_) => {
Py::new(py, PyArcOfHyperbola2d::initializer(data))?.into_any()
}
data @ Geom2dData::Parabola(_) => {
Py::new(py, PyParabola2d::initializer(data))?.into_any()
}
data @ Geom2dData::ArcOfParabola(_) => {
Py::new(py, PyArcOfParabola2d::initializer(data))?.into_any()
}
data @ Geom2dData::Bezier(_) => {
Py::new(py, PyBezierCurve2d::initializer(data))?.into_any()
}
data @ Geom2dData::BSpline(_) => {
Py::new(py, PyBSplineCurve2d::initializer(data))?.into_any()
}
data @ Geom2dData::Offset { .. } => {
Py::new(py, PyOffsetCurve2d::initializer(data))?.into_any()
}
})
}
fn circle_center(p1: (f64, f64), p2: (f64, f64), p3: (f64, f64)) -> PyResult<(f64, f64)> {
let sub = |a: (f64, f64), b: (f64, f64)| (a.0 - b.0, a.1 - b.1);
let dot = |a: (f64, f64), b: (f64, f64)| a.0 * b.0 + a.1 * b.1;
let u = sub(p2, p1);
let v = sub(p3, p2);
let w = sub(p1, p3);
let (uu, vv, ww) = (dot(u, u), dot(v, v), dot(w, w));
let eps2 = 1e-14;
if uu < eps2 || vv < eps2 || ww < eps2 {
return Err(PyValueError::new_err("Two points are coincident"));
}
let uv = -dot(u, v);
let vw = -dot(v, w);
let uw = -dot(u, w);
let w0 = 2.0 * (uu * ww - uw * uw).abs().sqrt() * uw / (uu * ww);
let w1 = 2.0 * (uu * vv - uv * uv).abs().sqrt() * uv / (uu * vv);
let w2 = 2.0 * (vv * ww - vw * vw).abs().sqrt() * vw / (vv * ww);
let wx = w0 + w1 + w2;
if wx.abs() < 1e-7 {
return Err(PyValueError::new_err("Points are collinear"));
}
Ok((
(w0 * p1.0 + w1 * p2.0 + w2 * p3.0) / wx,
(w0 * p1.1 + w1 * p2.1 + w2 * p3.1) / wx,
))
}
fn require_surface_or_face(value: &Bound<'_, PyAny>) -> PyResult<()> {
if value.downcast::<PyFace>().is_ok() {
return Ok(());
}
if value.downcast::<PyCylinder>().is_ok()
|| value.downcast::<PySphere>().is_ok()
|| value.downcast::<PyPlane>().is_ok()
|| value.downcast::<PyRectangularTrimmedSurface>().is_ok()
|| value.downcast::<PySurface>().is_ok()
{
return Ok(());
}
Err(PyTypeError::new_err(
"toShape(surface) expects a Part surface \
(Part.Plane, Part.Cylinder, Part.Sphere or Part.RectangularTrimmedSurface) \
or a Part.Face",
))
}
fn empty_error(what: &str) -> PyErr {
PyValueError::new_err(format!("{what} of an empty shape"))
}
fn require_edge(shape: &PartShape, what: &str) -> PyResult<()> {
if shape.shape().is_none() {
return Err(empty_error(what));
}
Ok(())
}
fn edge_query_error(what: &str) -> PyErr {
OCCError::new_err(format!(
"Part.{what} is not available in this slice (the seam exposes no such query)"
))
}
fn edge_vertex(edge: &PyRef<'_, PyEdge>, first: bool) -> PyResult<Py<PyVertex>> {
let what = if first { "firstVertex" } else { "lastVertex" };
require_edge(edge.as_super(), what)?;
let shape = edge
.as_super()
.shape()
.ok_or_else(|| empty_error(what))?;
let vertices = backend().elements(&shape, ElementKind::Vertex);
let vertex = if first {
vertices.first()
} else {
vertices.last()
}
.ok_or_else(|| OCCError::new_err(format!("Part.{what}: the edge has no vertices")))?;
Py::new(edge.py(), PyVertex::from_shape(vertex.shape.clone())?)
}
#[derive(Clone)]
pub(crate) struct EdgeEntry {
pub(crate) shape: Shape,
pub(crate) curve: Option<EdgeCurve>,
}
#[pyclass(name = "Edge", module = "Part", extends = PartShape)]
pub struct PyEdge {
entry: Mutex<Option<EdgeEntry>>,
curve2d: Mutex<Option<Geom2dData>>,
bspline: Mutex<Option<SplineState>>,
}
impl PyEdge {
fn initializer(entry: EdgeEntry) -> PyClassInitializer<Self> {
PyClassInitializer::from(PartShape::from_shape(entry.shape.clone())).add_subclass(Self {
entry: Mutex::new(Some(entry)),
curve2d: Mutex::new(None),
bspline: Mutex::new(None),
})
}
fn null_initializer() -> PyClassInitializer<Self> {
PyClassInitializer::from(PartShape::empty()).add_subclass(Self {
entry: Mutex::new(None),
curve2d: Mutex::new(None),
bspline: Mutex::new(None),
})
}
pub(crate) fn from_entry(entry: EdgeEntry) -> PyClassInitializer<Self> {
Self::initializer(entry)
}
pub(crate) fn from_entry_with_curve2d(
entry: EdgeEntry,
curve2d: Geom2dData,
) -> PyClassInitializer<Self> {
PyClassInitializer::from(PartShape::from_shape(entry.shape.clone())).add_subclass(Self {
entry: Mutex::new(Some(entry)),
curve2d: Mutex::new(Some(curve2d)),
bspline: Mutex::new(None),
})
}
fn from_bspline(shape: Shape, state: SplineState) -> PyClassInitializer<Self> {
PyClassInitializer::from(PartShape::from_shape(shape.clone())).add_subclass(Self {
entry: Mutex::new(Some(EdgeEntry {
shape,
curve: None,
})),
curve2d: Mutex::new(None),
bspline: Mutex::new(Some(state)),
})
}
fn from_bspline_with_curve2d(
shape: Shape,
state: SplineState,
curve2d: Geom2dData,
) -> PyClassInitializer<Self> {
PyClassInitializer::from(PartShape::from_shape(shape.clone())).add_subclass(Self {
entry: Mutex::new(Some(EdgeEntry {
shape,
curve: None,
})),
curve2d: Mutex::new(Some(curve2d)),
bspline: Mutex::new(Some(state)),
})
}
pub(crate) fn entry(&self) -> Option<EdgeEntry> {
self.entry
.lock()
.unwrap_or_else(|e| e.into_inner())
.clone()
}
pub(crate) fn entry_from(value: &Bound<'_, PyAny>) -> PyResult<EdgeEntry> {
if let Ok(edge) = value.downcast::<PyEdge>() {
return edge.borrow().entry().ok_or_else(|| {
PyValueError::new_err("an empty Part.Edge has no geometry")
});
}
if let Ok(line) = value.downcast::<PyLineSegment>() {
let curve = EdgeCurve::LineSegment(line.borrow().curve);
return Ok(EdgeEntry {
shape: shape_from_curve(&curve)?,
curve: Some(curve),
});
}
if let Ok(circle) = value.downcast::<PyCircle>() {
let curve = EdgeCurve::Circle(circle.borrow().curve);
return Ok(EdgeEntry {
shape: shape_from_curve(&curve)?,
curve: Some(curve),
});
}
if let Ok(arc) = value.downcast::<PyArcOfCircle>() {
let curve = EdgeCurve::Arc(arc.borrow().curve);
return Ok(EdgeEntry {
shape: shape_from_curve(&curve)?,
curve: Some(curve),
});
}
if let Ok(shape) = value.downcast::<PartShape>() {
let inner = shape
.borrow()
.shape()
.ok_or_else(|| OCCError::new_err("a null shape cannot be an edge"))?;
return Ok(EdgeEntry {
shape: inner,
curve: None,
});
}
Err(PyTypeError::new_err(
"Part.Edge expects a Part.Edge, a curve, or a Part.Shape",
))
}
pub(crate) fn from_curve(curve: EdgeCurve) -> PyResult<PyClassInitializer<Self>> {
Ok(Self::initializer(EdgeEntry {
shape: shape_from_curve(&curve)?,
curve: Some(curve),
}))
}
}
#[pymethods]
impl PyEdge {
#[new]
#[pyo3(signature = (value = None))]
fn new(value: Option<&Bound<'_, PyAny>>) -> PyResult<PyClassInitializer<Self>> {
match value {
Some(value) => Ok(Self::initializer(Self::entry_from(value)?)),
None => Ok(Self::null_initializer()),
}
}
#[getter]
fn Curve(&self, py: Python<'_>) -> PyResult<Option<PyObject>> {
let Some(entry) = self.entry() else {
return Err(empty_error("Curve"));
};
match backend().curve_of(&entry.shape) {
Ok(curve) => Ok(Some(curve_to_py(py, curve)?)),
Err(_) => {
if let Some(curve) = entry.curve {
return Ok(Some(curve_to_py(py, curve)?));
}
let state = self
.bspline
.lock()
.unwrap_or_else(|e| e.into_inner())
.clone();
match state {
Some(state) => Ok(Some(Py::new(py, PyBSplineCurve::from_state(state))?.into_any())),
None => Ok(None),
}
}
}
}
fn toShape(slf: PyRef<'_, Self>) -> PartShape {
slf.as_super().copy_handle()
}
#[pyo3(signature = (index, orientation = 0))]
fn curveOnSurface(
&self,
py: Python<'_>,
index: i32,
orientation: i32,
) -> PyResult<Option<PyObject>> {
let _ = orientation;
if index != 0 {
return Ok(None);
}
let curve2d = self
.curve2d
.lock()
.unwrap_or_else(|e| e.into_inner())
.clone();
match curve2d {
Some(data) => Ok(Some(geometry2d_to_py(py, data)?)),
None => Ok(None),
}
}
fn isNull(slf: PyRef<'_, Self>) -> bool {
slf.as_super().shape().is_none()
}
#[getter]
fn Length(slf: PyRef<'_, Self>) -> PyResult<f64> {
let shape = slf
.as_super()
.shape()
.ok_or_else(|| empty_error("Length"))?;
Ok(backend()
.mass_properties(&shape)
.map(|mass| mass.length)
.unwrap_or(0.0))
}
#[getter]
fn Tolerance(slf: PyRef<'_, Self>) -> PyResult<f64> {
require_edge(slf.as_super(), "Tolerance")?;
Err(OCCError::new_err(
"Part.Edge.Tolerance is not available in this slice (the seam exposes \
no edge tolerance)",
))
}
#[setter]
fn set_Tolerance(slf: PyRef<'_, Self>, _value: f64) -> PyResult<()> {
require_edge(slf.as_super(), "Tolerance")?;
Err(OCCError::new_err(
"Part.Edge.Tolerance is not available in this slice (the seam exposes \
no edge tolerance)",
))
}
#[getter]
fn ParameterRange(slf: PyRef<'_, Self>) -> PyResult<PyObject> {
require_edge(slf.as_super(), "ParameterRange")?;
Err(OCCError::new_err(
"Part.Edge.ParameterRange is not available in this slice (the seam \
exposes no parameter range)",
))
}
#[getter]
fn FirstParameter(slf: PyRef<'_, Self>) -> PyResult<f64> {
require_edge(slf.as_super(), "FirstParameter")?;
Err(OCCError::new_err(
"Part.Edge.FirstParameter is not available in this slice (the seam \
exposes no parameter range)",
))
}
#[getter]
fn LastParameter(slf: PyRef<'_, Self>) -> PyResult<f64> {
require_edge(slf.as_super(), "LastParameter")?;
Err(OCCError::new_err(
"Part.Edge.LastParameter is not available in this slice (the seam \
exposes no parameter range)",
))
}
fn valueAt(slf: PyRef<'_, Self>, _parameter: f64) -> PyResult<PyObject> {
require_edge(slf.as_super(), "valueAt")?;
Err(edge_query_error("valueAt"))
}
fn tangentAt(slf: PyRef<'_, Self>, _parameter: f64) -> PyResult<PyObject> {
require_edge(slf.as_super(), "tangentAt")?;
Err(edge_query_error("tangentAt"))
}
fn normalAt(slf: PyRef<'_, Self>, _parameter: f64) -> PyResult<PyObject> {
require_edge(slf.as_super(), "normalAt")?;
Err(edge_query_error("normalAt"))
}
fn curvatureAt(slf: PyRef<'_, Self>, _parameter: f64) -> PyResult<f64> {
require_edge(slf.as_super(), "curvatureAt")?;
Err(edge_query_error("curvatureAt"))
}
fn centerOfCurvatureAt(slf: PyRef<'_, Self>, _parameter: f64) -> PyResult<PyObject> {
require_edge(slf.as_super(), "centerOfCurvatureAt")?;
Err(edge_query_error("centerOfCurvatureAt"))
}
fn derivative1At(slf: PyRef<'_, Self>, _parameter: f64) -> PyResult<PyObject> {
require_edge(slf.as_super(), "derivative1At")?;
Err(edge_query_error("derivative1At"))
}
fn derivative2At(slf: PyRef<'_, Self>, _parameter: f64) -> PyResult<PyObject> {
require_edge(slf.as_super(), "derivative2At")?;
Err(edge_query_error("derivative2At"))
}
fn derivative3At(slf: PyRef<'_, Self>, _parameter: f64) -> PyResult<PyObject> {
require_edge(slf.as_super(), "derivative3At")?;
Err(edge_query_error("derivative3At"))
}
fn firstVertex(slf: PyRef<'_, Self>) -> PyResult<Py<PyVertex>> {
edge_vertex(&slf, true)
}
fn lastVertex(slf: PyRef<'_, Self>) -> PyResult<Py<PyVertex>> {
edge_vertex(&slf, false)
}
fn getParameterByLength(slf: PyRef<'_, Self>, _length: f64) -> PyResult<f64> {
require_edge(slf.as_super(), "getParameterByLength")?;
Err(edge_query_error("getParameterByLength"))
}
fn parameterAt(
slf: PyRef<'_, Self>,
_vertex: &Bound<'_, PyAny>,
) -> PyResult<f64> {
require_edge(slf.as_super(), "parameterAt")?;
Err(edge_query_error("parameterAt"))
}
fn parameters(
slf: PyRef<'_, Self>,
_face: &Bound<'_, PyAny>,
) -> PyResult<Vec<f64>> {
require_edge(slf.as_super(), "parameters")?;
Err(edge_query_error("parameters"))
}
fn __repr__(&self) -> String {
"Part.Edge(...)".to_string()
}
}
#[pyclass(name = "Wire", module = "Part", extends = PartShape)]
pub struct PyWire {
entries: Mutex<Vec<EdgeEntry>>,
}
impl PyWire {
fn entries(&self) -> std::sync::MutexGuard<'_, Vec<EdgeEntry>> {
self.entries.lock().unwrap_or_else(|e| e.into_inner())
}
pub(crate) fn from_shape(shape: Shape) -> PyClassInitializer<Self> {
PyClassInitializer::from(PartShape::from_shape(shape)).add_subclass(Self {
entries: Mutex::new(Vec::new()),
})
}
fn initializer(entries: Vec<EdgeEntry>) -> PyResult<PyClassInitializer<Self>> {
let base = if entries.is_empty() {
PartShape::empty()
} else {
let shapes: Vec<Shape> = entries.iter().map(|entry| entry.shape.clone()).collect();
PartShape::from_shape(backend().make_wire(&shapes).map_err(kernel_error)?)
};
Ok(PyClassInitializer::from(base).add_subclass(Self {
entries: Mutex::new(entries),
}))
}
}
#[pymethods]
impl PyWire {
#[new]
fn new(edges: &Bound<'_, PyAny>) -> PyResult<PyClassInitializer<Self>> {
if let Ok(wire) = edges.downcast::<PyWire>() {
return Self::initializer(wire.borrow().entries().clone());
}
if let Ok(edge) = edges.downcast::<PyEdge>() {
return Self::initializer(vec![PyEdge::entry_from(edge.as_any())?]);
}
if let Ok(entry) = PyEdge::entry_from(edges) {
return Self::initializer(vec![entry]);
}
let mut entries = Vec::new();
for edge in edges.try_iter()? {
entries.push(PyEdge::entry_from(&edge?)?);
}
Self::initializer(entries)
}
fn add(mut slf: PyRefMut<'_, Self>, edge: &Bound<'_, PyAny>) -> PyResult<()> {
let entry = PyEdge::entry_from(edge)?;
let shapes = {
let mut entries = slf.entries.lock().unwrap_or_else(|e| e.into_inner());
entries.push(entry);
entries
.iter()
.map(|entry| entry.shape.clone())
.collect::<Vec<_>>()
};
let shape = backend().make_wire(&shapes).map_err(kernel_error)?;
*slf
.as_super()
.shape
.lock()
.unwrap_or_else(|e| e.into_inner()) = Some(shape);
Ok(())
}
#[getter]
fn Edges(slf: PyRef<'_, Self>, py: Python<'_>) -> PyResult<Vec<Py<PyEdge>>> {
let entries = slf.entries();
if !entries.is_empty() {
return entries
.iter()
.map(|entry| Py::new(py, PyEdge::from_entry(entry.clone())))
.collect();
}
slf.as_super()
.sub_shapes(ElementKind::Edge)?
.into_iter()
.map(|shape| {
Py::new(
py,
PyEdge::from_entry(EdgeEntry {
shape,
curve: None,
}),
)
})
.collect()
}
fn isClosed(&self) -> bool {
let entries = self.entries();
let (Some(first), Some(last)) = (entries.first(), entries.last()) else {
return false;
};
let points = |curve: EdgeCurve| curve.endpoints();
match (first.curve.and_then(points), last.curve.and_then(points)) {
(Some((start, _)), Some((_, end))) => {
start.distance(&end) <= ferrocad_curves::TOLERANCE
}
_ => false,
}
}
fn toShape(slf: PyRef<'_, Self>) -> PartShape {
slf.as_super().copy_handle()
}
fn __len__(&self) -> usize {
self.entries().len()
}
fn __getitem__(&self, py: Python<'_>, index: isize) -> PyResult<Py<PyEdge>> {
let entries = self.entries();
let len = entries.len() as isize;
let index = if index < 0 { index + len } else { index };
if index < 0 || index >= len {
return Err(pyo3::exceptions::PyIndexError::new_err(
"wire index out of range",
));
}
Py::new(py, PyEdge::from_entry(entries[index as usize].clone()))
}
fn __repr__(&self) -> String {
format!("Part.Wire({} edges)", self.entries().len())
}
}
fn shapes_are_same(a: &Shape, b: &Shape) -> bool {
if a.is_same(b) {
return true;
}
match (
a.downcast_ref::<ferrocad_occt::OcctShape>(),
b.downcast_ref::<ferrocad_occt::OcctShape>(),
) {
(Some(a), Some(b)) => match (a.borrow().as_ref(), b.borrow().as_ref()) {
(Some(a), Some(b)) => a.IsEqual(b),
_ => false,
},
_ => false,
}
}
#[pyclass(name = "Vertex", module = "Part", extends = PartShape)]
pub struct PyVertex {
point: Vector3,
shape: Mutex<Option<Shape>>,
}
impl PyVertex {
fn from_parts(shape: Option<Shape>, point: Vector3) -> PyClassInitializer<Self> {
let base = match &shape {
Some(shape) => PartShape::from_shape(shape.clone()),
None => PartShape::empty(),
};
PyClassInitializer::from(base).add_subclass(Self {
point,
shape: Mutex::new(shape),
})
}
fn initializer(point: Vector3) -> PyResult<PyClassInitializer<Self>> {
let shape = backend().make_vertex(point).map_err(kernel_error)?;
Ok(Self::from_parts(Some(shape), point))
}
pub(crate) fn from_shape(shape: Shape) -> PyResult<PyClassInitializer<Self>> {
let point = backend()
.vertex_point(&shape)
.unwrap_or_else(Vector3::zero);
Ok(Self::from_parts(Some(shape), point))
}
fn shape(&self) -> Option<Shape> {
self.shape.lock().unwrap_or_else(|e| e.into_inner()).clone()
}
fn is_null(&self) -> bool {
self.shape().is_none()
}
fn is_same(&self, other: &Self) -> bool {
match (self.shape(), other.shape()) {
(Some(a), Some(b)) => shapes_are_same(&a, &b),
(None, None) => true,
_ => false,
}
}
}
#[pymethods]
impl PyVertex {
#[new]
#[pyo3(signature = (x = None, y = None, z = None))]
fn new(
x: Option<&Bound<'_, PyAny>>,
y: Option<f64>,
z: Option<f64>,
) -> PyResult<PyClassInitializer<Self>> {
match (x, y, z) {
(Some(x), Some(y), Some(z)) => Self::initializer(Vector3::new(x.extract()?, y, z)),
(Some(x), None, None) => {
if let Ok(vertex) = x.extract::<PyRef<'_, Self>>() {
return Ok(Self::from_parts(vertex.shape(), vertex.point));
}
Self::initializer(vector(x)?)
}
(None, None, None) => Ok(Self::from_parts(None, Vector3::zero())),
_ => Err(PyTypeError::new_err(
"Part.Vertex takes a point or x, y, z",
)),
}
}
#[getter]
fn Point(&self, py: Python<'_>) -> PyResult<PyObject> {
py_vector(py, self.point)
}
#[getter]
fn X(&self) -> f64 {
self.point.x
}
#[getter]
fn Y(&self) -> f64 {
self.point.y
}
#[getter]
fn Z(&self) -> f64 {
self.point.z
}
fn toShape(&self, py: Python<'_>) -> PyResult<Py<PyVertex>> {
Py::new(py, Self::from_parts(self.shape(), self.point))
}
fn __repr__(&self) -> String {
if self.is_null() {
"<Part.Vertex()>".to_string()
} else {
format!(
"<Part.Vertex ({}, {}, {})>",
self.point.x, self.point.y, self.point.z
)
}
}
fn __len__(&self) -> usize {
3
}
fn __getitem__(&self, index: isize) -> PyResult<f64> {
let index = if index < 0 { index + 3 } else { index };
let value = match index {
0 => self.point.x,
1 => self.point.y,
2 => self.point.z,
_ => return Err(PyIndexError::new_err("index out of range")),
};
if self.is_null() {
return Err(PyRuntimeError::new_err("a null vertex has no coordinates"));
}
Ok(value)
}
fn __eq__(&self, py: Python<'_>, other: &Bound<'_, PyAny>) -> PyResult<PyObject> {
match other.extract::<PyRef<'_, Self>>() {
Ok(other) => self.is_same(&other).into_py_any(py),
Err(_) => Ok(py.NotImplemented()),
}
}
fn __ne__(&self, py: Python<'_>, other: &Bound<'_, PyAny>) -> PyResult<PyObject> {
match other.extract::<PyRef<'_, Self>>() {
Ok(other) => (!self.is_same(&other)).into_py_any(py),
Err(_) => Ok(py.NotImplemented()),
}
}
fn __hash__(&self) -> u64 {
use std::hash::{Hash, Hasher};
let mut hasher = std::collections::hash_map::DefaultHasher::new();
self.point.x.to_bits().hash(&mut hasher);
self.point.y.to_bits().hash(&mut hasher);
self.point.z.to_bits().hash(&mut hasher);
hasher.finish()
}
}
#[pyclass(name = "Face", module = "Part", extends = PartShape)]
pub struct PyFace;
impl PyFace {
fn initializer(shape: Shape) -> PyClassInitializer<Self> {
PyClassInitializer::from(PartShape::from_shape(shape)).add_subclass(Self)
}
fn empty_initializer() -> PyClassInitializer<Self> {
PyClassInitializer::from(PartShape::empty()).add_subclass(Self)
}
}
fn wire_shapes(value: &Bound<'_, PyAny>) -> PyResult<Vec<Shape>> {
if let Ok(compound) = value.downcast::<crate::containers::PyCompound>() {
let compound = compound.borrow();
return Ok(compound.children(compound.as_super()));
}
if let Ok(wire) = value.downcast::<PyWire>() {
let base = wire.borrow();
let shape = base
.as_super()
.shape()
.ok_or_else(|| OCCError::new_err("an empty wire has no shape"))?;
return Ok(vec![shape]);
}
if let Ok(edge) = value.downcast::<PyEdge>() {
let shape = edge
.borrow()
.entry()
.ok_or_else(|| PyValueError::new_err("an empty Part.Edge has no geometry"))?
.shape;
let wire = backend().make_wire(&[shape]).map_err(kernel_error)?;
return Ok(vec![wire]);
}
if let Ok(shape) = value.downcast::<PartShape>() {
let inner = shape
.borrow()
.shape()
.ok_or_else(|| OCCError::new_err("a null shape cannot bound a face"))?;
if let Some(outer) = backend().outer_wire(&inner) {
return Ok(vec![outer]);
}
return Ok(vec![inner]);
}
let mut shapes = Vec::new();
for item in value.try_iter()? {
shapes.extend(wire_shapes(&item?)?);
}
Ok(shapes)
}
#[pymethods]
impl PyFace {
#[new]
#[pyo3(signature = (value = None, face_maker = "Part::FaceMakerBullseye"))]
fn new(
value: Option<&Bound<'_, PyAny>>,
face_maker: &str,
) -> PyResult<PyClassInitializer<Self>> {
let Some(value) = value else {
return Ok(Self::empty_initializer());
};
let shapes = wire_shapes(value)?;
let shape = if face_maker == "Part::FaceMakerUnified" {
backend().make_faces(&shapes).map_err(face_error)?
} else {
backend().make_face(&shapes).map_err(kernel_error)?
};
Ok(Self::initializer(shape))
}
fn makeOffset(slf: PyRef<'_, Self>, distance: f64) -> PyResult<PartShape> {
let shape = slf
.as_super()
.shape()
.ok_or_else(|| empty_error("makeOffset"))?;
let offset = backend()
.make_offset(&shape, distance, 0)
.map_err(kernel_error)?;
Ok(PartShape::from_shape(offset))
}
fn valueAt(slf: PyRef<'_, Self>, _u: f64, _v: f64) -> PyResult<PyObject> {
require_edge(slf.as_super(), "valueAt")?;
Err(edge_query_error("valueAt"))
}
fn normalAt(slf: PyRef<'_, Self>, _u: f64, _v: f64) -> PyResult<PyObject> {
require_edge(slf.as_super(), "normalAt")?;
Err(edge_query_error("normalAt"))
}
fn tangentAt(slf: PyRef<'_, Self>, _u: f64, _v: f64) -> PyResult<PyObject> {
require_edge(slf.as_super(), "tangentAt")?;
Err(edge_query_error("tangentAt"))
}
fn curvatureAt(slf: PyRef<'_, Self>, _u: f64, _v: f64) -> PyResult<PyObject> {
require_edge(slf.as_super(), "curvatureAt")?;
Err(edge_query_error("curvatureAt"))
}
fn derivative1At(slf: PyRef<'_, Self>, _u: f64, _v: f64) -> PyResult<PyObject> {
require_edge(slf.as_super(), "derivative1At")?;
Err(edge_query_error("derivative1At"))
}
fn derivative2At(slf: PyRef<'_, Self>, _u: f64, _v: f64) -> PyResult<PyObject> {
require_edge(slf.as_super(), "derivative2At")?;
Err(edge_query_error("derivative2At"))
}
fn cutHoles(slf: PyRef<'_, Self>, _holes: &Bound<'_, PyAny>) -> PyResult<()> {
require_edge(slf.as_super(), "cutHoles")?;
Err(edge_query_error("cutHoles"))
}
fn getUVNodes(slf: PyRef<'_, Self>) -> PyResult<Vec<(f64, f64)>> {
require_edge(slf.as_super(), "getUVNodes")?;
Err(edge_query_error("getUVNodes"))
}
#[getter]
fn Tolerance(slf: PyRef<'_, Self>) -> PyResult<f64> {
require_edge(slf.as_super(), "Tolerance")?;
Err(edge_query_error("Tolerance"))
}
#[setter]
fn set_Tolerance(slf: PyRef<'_, Self>, _value: f64) -> PyResult<()> {
require_edge(slf.as_super(), "Tolerance")?;
Err(edge_query_error("Tolerance"))
}
#[getter]
fn ParameterRange(slf: PyRef<'_, Self>) -> PyResult<PyObject> {
require_edge(slf.as_super(), "ParameterRange")?;
Err(edge_query_error("ParameterRange"))
}
}
#[pyfunction]
fn makeLine(py: Python<'_>, p1: &Bound<'_, PyAny>, p2: &Bound<'_, PyAny>) -> PyResult<PyObject> {
let segment = LineSegment::new(vector(p1)?, vector(p2)?).map_err(curve_error)?;
Ok(Py::new(py, PyEdge::from_curve(EdgeCurve::LineSegment(segment))?)?.into_any())
}
#[pyfunction]
#[pyo3(signature = (radius, center = None, normal = None, angle1 = None, angle2 = None))]
fn makeCircle(
py: Python<'_>,
radius: f64,
center: Option<&Bound<'_, PyAny>>,
normal: Option<&Bound<'_, PyAny>>,
angle1: Option<f64>,
angle2: Option<f64>,
) -> PyResult<PyObject> {
let center = center.map(vector).transpose()?.unwrap_or_else(Vector3::zero);
let normal = normal
.map(vector)
.transpose()?
.unwrap_or_else(|| Vector3::new(0.0, 0.0, 1.0));
let circle = Circle::new(center, normal, radius).map_err(curve_error)?;
let first = angle1.unwrap_or(0.0);
let last = angle2.unwrap_or(360.0);
if (last - first).abs() <= ANGULAR_TOLERANCE
|| (last - first).abs() >= 360.0 - ANGULAR_TOLERANCE
{
return Ok(Py::new(py, PyEdge::from_curve(EdgeCurve::Circle(circle))?)?.into_any());
}
let arc = Arc::new(circle, first.to_radians(), last.to_radians()).map_err(curve_error)?;
Ok(Py::new(py, PyEdge::from_curve(EdgeCurve::Arc(arc))?)?.into_any())
}
#[pyfunction]
#[pyo3(signature = (points, closed = false))]
fn makePolygon(py: Python<'_>, points: &Bound<'_, PyAny>, closed: bool) -> PyResult<Py<PyWire>> {
let mut parsed = Vec::new();
for point in points.try_iter()? {
parsed.push(vector(&point?)?);
}
let edges = polygon(&parsed, closed).map_err(curve_error)?;
let mut entries = Vec::with_capacity(edges.len());
for curve in edges {
entries.push(EdgeEntry {
shape: shape_from_curve(&curve)?,
curve: Some(curve),
});
}
Py::new(py, PyWire::initializer(entries)?)
}
#[pyfunction]
#[pyo3(signature = (wires, face_maker = "Part::FaceMakerBullseye", noElementMap = false))]
fn makeFace(
py: Python<'_>,
wires: &Bound<'_, PyAny>,
face_maker: &str,
noElementMap: bool,
) -> PyResult<Py<PyFace>> {
let _ = noElementMap;
let shapes = wire_shapes(wires)?;
let shape = if face_maker == "Part::FaceMakerUnified" {
backend().make_faces(&shapes).map_err(face_error)?
} else {
backend().make_face(&shapes).map_err(kernel_error)?
};
Py::new(py, PyFace::initializer(shape))
}
const HELIX_CONFUSION: f64 = 1e-7;
#[pyfunction]
#[pyo3(signature = (pitch, height, radius, angle = -1.0, pleft = false, pvertHeight = false))]
fn makeHelix(
py: Python<'_>,
pitch: f64,
height: f64,
radius: f64,
angle: f64,
pleft: bool,
pvertHeight: bool,
) -> PyResult<Py<PyEdge>> {
if !pitch.is_finite() || pitch.abs() < HELIX_CONFUSION {
return Err(OCCError::new_err("Pitch of helix too small"));
}
if !height.is_finite() || height.abs() < HELIX_CONFUSION {
return Err(OCCError::new_err("Height of helix too small"));
}
if (height > 0.0 && pitch < 0.0) || (height < 0.0 && pitch > 0.0) {
return Err(OCCError::new_err(
"Pitch and height of helix not compatible",
));
}
let conical = angle >= HELIX_CONFUSION;
if !conical {
if radius < HELIX_CONFUSION {
return Err(OCCError::new_err("Radius of helix too small"));
}
} else if angle.to_radians() < HELIX_CONFUSION {
return Err(OCCError::new_err("Angle of helix too small"));
}
let (pitch_argument, height_argument) = if conical && !pvertHeight {
let cosine = angle.to_radians().cos();
(pitch * cosine, height * cosine)
} else {
(pitch, height)
};
let shape = surface_ops::make_helix_edge(
pitch_argument,
height_argument,
radius,
if conical { angle } else { 0.0 },
pleft,
)
.map_err(kernel_error)?;
Py::new(
py,
PyEdge::from_entry(EdgeEntry {
shape,
curve: None,
}),
)
}
fn helix_turn_height(pitch: f64, conical: bool, angle: f64) -> f64 {
if conical {
pitch * angle.to_radians().cos()
} else {
pitch
}
}
fn helix_split_height(edge: &Shape, parameter: f64, z_end: f64) -> Result<f64, String> {
let wire = backend()
.split_edge(edge, &[parameter])
.map_err(|error| error.to_string())?;
let margin = 1e-6 * z_end.abs().max(1.0);
let mut heights = Vec::new();
for element in backend().elements(&wire, ElementKind::Edge) {
for vertex in backend().elements(&element.shape, ElementKind::Vertex) {
if let Some(point) = backend().vertex_point(&vertex.shape) {
if point.z > margin && point.z < z_end - margin {
heights.push(point.z);
}
}
}
}
heights.sort_by(|a, b| a.total_cmp(b));
heights.dedup_by(|a, b| (*a - *b).abs() <= margin * 0.1);
heights
.first()
.copied()
.ok_or_else(|| "the helix turn boundary could not be measured".to_string())
}
fn helix_split_limit(edge: &Shape) -> Result<f64, String> {
let mut inside = 0.0;
let mut outside = 1.0;
while backend().split_edge(edge, &[outside]).is_ok() {
inside = outside;
outside *= 2.0;
if outside > 1.0e30 {
return Err("the helix edge has no finite parameter limit".to_string());
}
}
for _ in 0..60 {
let middle = 0.5 * (inside + outside);
if backend().split_edge(edge, &[middle]).is_ok() {
inside = middle;
} else {
outside = middle;
}
}
Ok(inside)
}
fn helix_parameter_at_height(
edge: &Shape,
target: f64,
z_end: f64,
samples: &mut Vec<(f64, f64)>,
) -> Result<f64, String> {
let lower = samples
.iter()
.rev()
.find(|(_, height)| *height <= target)
.copied()
.ok_or_else(|| "no lower helix sample".to_string())?;
let upper = samples
.iter()
.find(|(_, height)| *height >= target)
.copied()
.ok_or_else(|| "no upper helix sample".to_string())?;
let mut low = lower;
let mut high = upper;
let tolerance = 1e-9 * z_end.abs().max(1.0);
let mut best = low.0;
for _ in 0..60 {
let span = high.1 - low.1;
if span.abs() <= tolerance {
break;
}
let mut parameter = low.0 + (target - low.1) * (high.0 - low.0) / span;
if high.0 - low.0 <= 1e-12 {
break;
}
parameter = parameter.clamp(low.0 + 1e-12, high.0 - 1e-12);
let height = helix_split_height(edge, parameter, z_end)?;
samples.push((parameter, height));
samples.sort_by(|a, b| a.1.total_cmp(&b.1));
if (height - target).abs() <= tolerance {
return Ok(parameter);
}
if height < target {
low = (parameter, height);
} else {
high = (parameter, height);
}
best = parameter;
}
Ok(best)
}
#[pyfunction]
#[pyo3(signature = (pitch, height, radius, angle = -1.0, pleft = false))]
fn makeLongHelix(
py: Python<'_>,
pitch: f64,
height: f64,
radius: f64,
angle: f64,
pleft: bool,
) -> PyResult<Py<PyWire>> {
if !pitch.is_finite() || pitch < HELIX_CONFUSION {
return Err(OCCError::new_err("Pitch of helix too small"));
}
if !height.is_finite() || height < HELIX_CONFUSION {
return Err(OCCError::new_err("Height of helix too small"));
}
let conical = angle >= HELIX_CONFUSION;
if !conical && radius < HELIX_CONFUSION {
return Err(OCCError::new_err("Radius of helix too small"));
}
let shape = surface_ops::make_helix_edge(
if conical {
pitch * angle.to_radians().cos()
} else {
pitch
},
if conical {
height * angle.to_radians().cos()
} else {
height
},
radius,
if conical { angle } else { 0.0 },
pleft,
)
.map_err(kernel_error)?;
let turns = height / pitch;
let whole_turns = turns.floor();
let part_turn = turns - whole_turns;
let z_end = if conical {
height * angle.to_radians().cos()
} else {
height
};
let turn_height = helix_turn_height(pitch, conical, angle);
let mut targets = Vec::new();
let mut boundary = 1.0;
while boundary < whole_turns - HELIX_CONFUSION {
targets.push(boundary * turn_height);
boundary += 1.0;
}
if part_turn > HELIX_CONFUSION && whole_turns >= 1.0 {
targets.push(whole_turns * turn_height);
}
let wire = if targets.is_empty() || whole_turns > 1.0e4 {
backend().make_wire(&[shape]).map_err(kernel_error)?
} else {
match split_helix_turns(&shape, &targets, z_end) {
Ok(parameters) => backend()
.split_edge(&shape, ¶meters)
.map_err(kernel_error)?,
Err(_) => backend().make_wire(&[shape]).map_err(kernel_error)?,
}
};
Py::new(py, PyWire::from_shape(wire))
}
fn split_helix_turns(
shape: &Shape,
targets: &[f64],
z_end: f64,
) -> Result<Vec<f64>, String> {
let limit = helix_split_limit(shape)?;
if !(limit > 0.0) {
return Err("the helix edge has no split range".to_string());
}
let mut samples = vec![(0.0, 0.0), (limit, z_end)];
let mut parameters = Vec::with_capacity(targets.len());
for target in targets {
parameters.push(helix_parameter_at_height(shape, *target, z_end, &mut samples)?);
}
Ok(parameters)
}
fn edge_endpoints(shape: &Shape) -> Option<(Vector3, Vector3)> {
let mut points = Vec::new();
for element in backend().elements(shape, ElementKind::Vertex) {
if let Some(point) = backend().vertex_point(&element.shape) {
points.push(point);
}
}
match points.as_slice() {
[first, second, ..] => Some((*first, *second)),
[only] => Some((*only, *only)),
_ => None,
}
}
struct PointedEdge {
first: Vector3,
second: Vector3,
shape: Shape,
}
fn sort_edge_chain(tolerance: f64, edges: Vec<Shape>) -> Result<(Vec<Shape>, Vec<Shape>), String> {
let squared_tolerance = (tolerance * tolerance).max(0.0);
let mut remaining: Vec<PointedEdge> = Vec::with_capacity(edges.len());
for shape in edges {
let (first, second) = edge_endpoints(&shape)
.ok_or_else(|| "an edge without vertices cannot be sorted".to_string())?;
remaining.push(PointedEdge {
first,
second,
shape,
});
}
if remaining.is_empty() {
return Ok((Vec::new(), Vec::new()));
}
let seed = remaining.remove(0);
let mut first = seed.first;
let mut last = seed.second;
let mut sorted = std::collections::VecDeque::new();
sorted.push_back(seed.shape);
while !remaining.is_empty() {
let mut found = None;
for (index, candidate) in remaining.iter().enumerate() {
let square_distance = |a: Vector3, b: Vector3| {
let delta = a.sub(&b);
delta.dot(&delta)
};
if square_distance(candidate.first, last) <= squared_tolerance {
last = candidate.second;
sorted.push_back(candidate.shape.clone());
found = Some(index);
break;
} else if square_distance(candidate.second, first) <= squared_tolerance {
first = candidate.first;
sorted.push_front(candidate.shape.clone());
found = Some(index);
break;
} else if square_distance(candidate.second, last) <= squared_tolerance {
last = candidate.first;
match backend().reversed(&candidate.shape) {
Ok(reversed) => sorted.push_back(reversed),
Err(_) => sorted.push_back(candidate.shape.clone()),
}
found = Some(index);
break;
} else if square_distance(candidate.first, first) <= squared_tolerance {
first = candidate.second;
match backend().reversed(&candidate.shape) {
Ok(reversed) => sorted.push_front(reversed),
Err(_) => sorted.push_front(candidate.shape.clone()),
}
found = Some(index);
break;
}
}
match found {
Some(index) => {
remaining.remove(index);
}
None => break,
}
if first.distance(&last) <= tolerance {
break;
}
}
Ok((
sorted.into_iter().collect(),
remaining.into_iter().map(|pointed| pointed.shape).collect(),
))
}
fn edge_argument_list(value: &Bound<'_, PyAny>) -> PyResult<Vec<Shape>> {
let mut edges = Vec::new();
for item in value.try_iter()? {
let item = item?;
let shape = item
.downcast::<PartShape>()
.map_err(|_| PyTypeError::new_err("item is not a shape"))?;
let shape = shape.borrow();
if shape.shape_type() != "Edge" {
return Err(PyTypeError::new_err("shape is not an edge"));
}
edges.push(
shape
.shape()
.ok_or_else(|| OCCError::new_err("a null shape is not an edge"))?,
);
}
Ok(edges)
}
fn edges_to_python(py: Python<'_>, edges: Vec<Shape>) -> PyResult<Vec<Py<PyEdge>>> {
edges
.into_iter()
.map(|shape| {
Py::new(
py,
PyEdge::from_entry(EdgeEntry {
shape,
curve: None,
}),
)
})
.collect()
}
#[pyfunction(name = "__sortEdges__")]
fn sortEdgesSingle(py: Python<'_>, obj: &Bound<'_, PyAny>) -> PyResult<Vec<Py<PyEdge>>> {
let edges = edge_argument_list(obj)?;
let (sorted, _unused) = sort_edge_chain(1e-7, edges).map_err(OCCError::new_err)?;
edges_to_python(py, sorted)
}
#[pyfunction]
#[pyo3(signature = (obj, tol3d = None))]
fn sortEdges(py: Python<'_>, obj: &Bound<'_, PyAny>, tol3d: Option<f64>) -> PyResult<Vec<Vec<Py<PyEdge>>>> {
let tolerance = tol3d.unwrap_or(1e-7);
let mut remaining = edge_argument_list(obj)?;
let mut clusters = Vec::new();
while !remaining.is_empty() {
let (sorted, unused) = sort_edge_chain(tolerance, remaining).map_err(OCCError::new_err)?;
remaining = unused;
clusters.push(edges_to_python(py, sorted)?);
}
Ok(clusters)
}
fn face_error(error: ferrocad_geom::GeomError) -> PyErr {
let invalid = matches!(
error.downcast_ref::<ferrocad_occt::OcctError>(),
Some(ferrocad_occt::OcctError::InvalidInput { .. })
);
if invalid {
PyValueError::new_err(error.to_string())
} else {
kernel_error(error)
}
}
pub(crate) fn register(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_class::<PyLineSegment>()?;
m.add_class::<PyLine>()?;
m.add_class::<PyCircle>()?;
m.add_class::<PyArcOfCircle>()?;
m.add_class::<PyEllipse>()?;
m.add_class::<PyArcOfEllipse>()?;
m.add_class::<PyHyperbola>()?;
m.add_class::<PyArcOfHyperbola>()?;
m.add_class::<PyParabola>()?;
m.add_class::<PyArcOfParabola>()?;
m.add_class::<PyBezierCurve>()?;
m.add_class::<PyPlane>()?;
m.add_class::<PyCylinder>()?;
m.add_class::<PySphere>()?;
m.add_class::<PyRectangularTrimmedSurface>()?;
m.add_class::<PyEdge>()?;
m.add_class::<PyWire>()?;
m.add_class::<PyVertex>()?;
m.add_class::<PyFace>()?;
m.add_class::<PyBSplineCurve>()?;
m.add_function(wrap_pyfunction!(makeLine, m)?)?;
m.add_function(wrap_pyfunction!(makeCircle, m)?)?;
m.add_function(wrap_pyfunction!(makePolygon, m)?)?;
m.add_function(wrap_pyfunction!(makeFace, m)?)?;
m.add_function(wrap_pyfunction!(makeHelix, m)?)?;
m.add_function(wrap_pyfunction!(makeLongHelix, m)?)?;
m.add_function(wrap_pyfunction!(sortEdgesSingle, m)?)?;
m.add_function(wrap_pyfunction!(sortEdges, m)?)?;
m.add("OCCError", m.py().get_type::<OCCError>())?;
let geom2d = PyModule::new(m.py(), "Part.Geom2d")?;
geom2d.add_class::<PyGeometry2d>()?;
geom2d.add_class::<PyCurve2d>()?;
geom2d.add_class::<PyConic2d>()?;
geom2d.add_class::<PyArcOfConic2d>()?;
geom2d.add_class::<PyLine2d>()?;
geom2d.add_class::<PyLine2dSegment>()?;
geom2d.add_class::<PyCircle2d>()?;
geom2d.add_class::<PyArcOfCircle2d>()?;
geom2d.add_class::<PyEllipse2d>()?;
geom2d.add_class::<PyArcOfEllipse2d>()?;
geom2d.add_class::<PyHyperbola2d>()?;
geom2d.add_class::<PyArcOfHyperbola2d>()?;
geom2d.add_class::<PyParabola2d>()?;
geom2d.add_class::<PyArcOfParabola2d>()?;
geom2d.add_class::<PyBezierCurve2d>()?;
geom2d.add_class::<PyBSplineCurve2d>()?;
geom2d.add_class::<PyOffsetCurve2d>()?;
m.add("Geom2d", &geom2d)?;
let arc = m.getattr("ArcOfCircle")?;
m.add("Arc", arc)?;
Ok(())
}