use crate::features::FinitePoint3;
use crate::math::Point3;
use crate::scalar::{FiniteReal, NonNegativeReal, PositiveReal};
#[cfg(feature = "schema")]
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum GeometryLayoutError {
#[non_exhaustive]
Layout(String),
EditRefused(String),
}
impl std::fmt::Display for GeometryLayoutError {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Layout(message) | Self::EditRefused(message) => formatter.write_str(message),
}
}
}
impl std::error::Error for GeometryLayoutError {}
fn geometry_layout_error(message: impl Into<String>) -> GeometryLayoutError {
GeometryLayoutError::Layout(message.into())
}
fn admit_chordal_deflection(
chordal_deflection: f64,
) -> Result<NonNegativeReal, GeometryLayoutError> {
NonNegativeReal::new(chordal_deflection)
.ok_or_else(|| geometry_layout_error("chordal_deflection must be finite and non-negative"))
}
fn layout_error(message: &'static str) -> GeometryLayoutError {
GeometryLayoutError::Layout(message.into())
}
fn admit_finite_vertices(vertices: Vec<Point3>) -> Result<Vec<FinitePoint3>, GeometryLayoutError> {
vertices
.into_iter()
.map(|point| match FinitePoint3::new(point) {
Some(point) => Ok(point),
None => Err(layout_error("vertices must be finite")),
})
.collect::<Result<Vec<_>, _>>()
}
#[derive(Debug, Clone, PartialEq, Serialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
pub struct PolygonalSurface {
#[cfg_attr(feature = "schema", schemars(with = "Vec<Point3>"))]
vertices: Vec<FinitePoint3>,
triangles: Vec<[u32; 3]>,
#[cfg_attr(feature = "schema", schemars(with = "f64"))]
chordal_deflection: NonNegativeReal,
}
impl PolygonalSurface {
pub fn vertices(&self) -> &[FinitePoint3] {
&self.vertices
}
pub fn triangles(&self) -> &[[u32; 3]] {
&self.triangles
}
pub fn new(
vertices: Vec<Point3>,
triangles: Vec<[u32; 3]>,
chordal_deflection: f64,
) -> Result<Self, GeometryLayoutError> {
Self::build(vertices, triangles, admit_finite_vertices, || {
NonNegativeReal::new(chordal_deflection)
})
}
pub fn from_scaled_deflection(
vertices: Vec<Point3>,
triangles: Vec<[u32; 3]>,
chordal_deflection: NonNegativeReal,
scale: PositiveReal,
) -> Result<Self, GeometryLayoutError> {
Self::build(vertices, triangles, admit_finite_vertices, || {
chordal_deflection.scaled(scale)
})
}
pub fn from_admitted_scaled_deflection(
vertices: Vec<FinitePoint3>,
triangles: Vec<[u32; 3]>,
chordal_deflection: NonNegativeReal,
scale: PositiveReal,
) -> Result<Self, GeometryLayoutError> {
Self::build(vertices, triangles, Ok, || chordal_deflection.scaled(scale))
}
fn build<P>(
vertices: Vec<P>,
triangles: Vec<[u32; 3]>,
convert: impl FnOnce(Vec<P>) -> Result<Vec<FinitePoint3>, GeometryLayoutError>,
deflection: impl FnOnce() -> Option<NonNegativeReal>,
) -> Result<Self, GeometryLayoutError> {
Self::check_layout(vertices.len(), &triangles)?;
let vertices = convert(vertices)?;
let Some(chordal_deflection) = deflection() else {
return Err(layout_error(
"chordal_deflection must be finite and non-negative",
));
};
Ok(Self {
vertices,
triangles,
chordal_deflection,
})
}
fn check_layout(
vertex_count: usize,
triangles: &[[u32; 3]],
) -> Result<(), GeometryLayoutError> {
if vertex_count < 3 {
return Err(layout_error(
"polygonal surface must contain at least three vertices",
));
}
if triangles.is_empty() {
return Err(layout_error(
"polygonal surface must contain at least one triangle",
));
}
for index in triangles.iter().flatten() {
if usize::try_from(*index).map_or(true, |index| index >= vertex_count) {
return Err(layout_error(
"polygonal surface contains an out-of-range triangle index",
));
}
}
Ok(())
}
pub fn edit_vertices(
&mut self,
edit: impl FnOnce(&mut [Point3]) -> Result<(), GeometryLayoutError>,
) -> Result<(), GeometryLayoutError> {
let mut candidate: Vec<Point3> = self.vertices.iter().map(|point| point.get()).collect();
edit(&mut candidate)?;
self.vertices = admit_finite_vertices(candidate)?;
Ok(())
}
#[must_use]
pub const fn chordal_deflection(&self) -> NonNegativeReal {
self.chordal_deflection
}
pub fn set_chordal_deflection(
&mut self,
chordal_deflection: f64,
) -> Result<(), GeometryLayoutError> {
self.chordal_deflection = admit_chordal_deflection(chordal_deflection)?;
Ok(())
}
}
impl<'de> Deserialize<'de> for PolygonalSurface {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct Wire {
vertices: Vec<Point3>,
triangles: Vec<[u32; 3]>,
chordal_deflection: f64,
}
let wire = Wire::deserialize(deserializer)?;
Self::build(wire.vertices, wire.triangles, admit_finite_vertices, || {
NonNegativeReal::new(wire.chordal_deflection)
})
.map_err(serde::de::Error::custom)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct PolylineVertex<R = f64, P = Point3> {
pub parameter: R,
pub point: P,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(tag = "kind", rename_all = "snake_case")]
#[serde(deny_unknown_fields)]
pub enum PolylineSamples<R = f64, P = Point3> {
Unparameterized {
points: crate::features::NonEmptyMembers<P>,
},
Parameterized {
vertices: crate::features::NonEmptyMembers<PolylineVertex<R, P>>,
},
}
#[derive(Debug, Clone, PartialEq, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[cfg_attr(feature = "schema", schemars(with = "PolylineCurveWire"))]
#[serde(try_from = "PolylineCurveWire")]
pub struct PolylineCurve {
samples: PolylineSamples<FiniteReal, FinitePoint3>,
chordal_deflection: NonNegativeReal,
}
#[derive(Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(deny_unknown_fields)]
struct PolylineCurveWire {
samples: PolylineSamples,
chordal_deflection: f64,
}
#[derive(Serialize)]
struct PolylineCurveWriteWire<'a> {
samples: &'a PolylineSamples<FiniteReal, FinitePoint3>,
chordal_deflection: NonNegativeReal,
}
impl Serialize for PolylineCurve {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
PolylineCurveWriteWire {
samples: &self.samples,
chordal_deflection: self.chordal_deflection,
}
.serialize(serializer)
}
}
impl TryFrom<PolylineCurveWire> for PolylineCurve {
type Error = GeometryLayoutError;
fn try_from(wire: PolylineCurveWire) -> Result<Self, Self::Error> {
Self::build(wire.samples, || {
NonNegativeReal::new(wire.chordal_deflection)
})
}
}
impl<R, P> PolylineSamples<R, P> {
#[must_use]
pub fn count(&self) -> usize {
match self {
Self::Unparameterized { points } => points.len(),
Self::Parameterized { vertices } => vertices.len(),
}
}
}
impl<R: Copy, P: Copy> PolylineSamples<R, P> {
pub fn points(&self) -> impl Iterator<Item = P> + '_ {
let (points, vertices) = match self {
Self::Unparameterized { points } => (Some(points), None),
Self::Parameterized { vertices } => (None, Some(vertices)),
};
points
.into_iter()
.flatten()
.copied()
.chain(vertices.into_iter().flatten().map(|vertex| vertex.point))
}
pub fn parameters(&self) -> Option<impl Iterator<Item = R> + '_> {
match self {
Self::Unparameterized { .. } => None,
Self::Parameterized { vertices } => {
Some(vertices.iter().map(|vertex| vertex.parameter))
}
}
}
}
impl PolylineSamples {
pub fn edit_points(
&mut self,
mut edit: impl FnMut(&mut Point3) -> Result<(), GeometryLayoutError>,
) -> Result<(), GeometryLayoutError> {
let mut candidate = self.clone();
match &mut candidate {
Self::Unparameterized { points } => {
for point in points.iter_mut() {
edit(point)?;
}
}
Self::Parameterized { vertices } => {
for vertex in vertices.iter_mut() {
edit(&mut vertex.point)?;
}
}
}
*self = candidate;
Ok(())
}
}
impl PolylineSamples<FiniteReal, FinitePoint3> {
fn has_strictly_monotonic_parameters(&self) -> bool {
let Self::Parameterized { vertices } = self else {
return true;
};
let mut increasing = true;
for pair in vertices.windows(2) {
if pair[0].parameter >= pair[1].parameter {
increasing = false;
break;
}
}
if increasing {
return true;
}
for pair in vertices.windows(2) {
if pair[0].parameter <= pair[1].parameter {
return false;
}
}
true
}
pub fn edit_admitted_points<E>(
&mut self,
mut edit: impl FnMut(FinitePoint3) -> Result<FinitePoint3, E>,
) -> Result<(), E> {
match self {
Self::Unparameterized { points } => edit_sample_rows(points, |point| {
*point = edit(*point)?;
Ok(())
}),
Self::Parameterized { vertices } => edit_sample_rows(vertices, |vertex| {
vertex.point = edit(vertex.point)?;
Ok(())
}),
}
}
#[must_use]
pub fn to_raw(&self) -> PolylineSamples {
match self {
Self::Unparameterized { points } => PolylineSamples::Unparameterized {
points: points.clone().map(FinitePoint3::get),
},
Self::Parameterized { vertices } => PolylineSamples::Parameterized {
vertices: vertices.clone().map(|vertex| PolylineVertex {
parameter: vertex.parameter.get(),
point: vertex.point.get(),
}),
},
}
}
}
fn edit_sample_rows<T: Copy, E>(
rows: &mut [T],
mut edit: impl FnMut(&mut T) -> Result<(), E>,
) -> Result<(), E> {
let mut candidate = rows.to_vec();
for row in &mut candidate {
edit(row)?;
}
rows.copy_from_slice(&candidate);
Ok(())
}
impl PolylineCurve {
pub fn from_checked_samples(
samples: PolylineSamples<FiniteReal, FinitePoint3>,
chordal_deflection: f64,
) -> Result<Self, GeometryLayoutError> {
Self::build_checked_samples(samples, || NonNegativeReal::new(chordal_deflection))
}
fn build_checked_samples(
samples: PolylineSamples<FiniteReal, FinitePoint3>,
deflection: impl FnOnce() -> Option<NonNegativeReal>,
) -> Result<Self, GeometryLayoutError> {
Self::require_sample_count(samples.count())?;
let chordal_deflection = Self::admit_deflection(deflection())?;
if !samples.has_strictly_monotonic_parameters() {
return Err(layout_error(
"parameters must be finite and strictly monotonic",
));
}
Ok(Self {
samples,
chordal_deflection,
})
}
pub fn new(
samples: PolylineSamples,
chordal_deflection: f64,
) -> Result<Self, GeometryLayoutError> {
Self::build(samples, || NonNegativeReal::new(chordal_deflection))
}
pub fn from_scaled_deflection(
samples: PolylineSamples<FiniteReal, FinitePoint3>,
chordal_deflection: NonNegativeReal,
scale: PositiveReal,
) -> Result<Self, GeometryLayoutError> {
Self::build_checked_samples(samples, || chordal_deflection.scaled(scale))
}
fn build(
samples: PolylineSamples,
deflection: impl FnOnce() -> Option<NonNegativeReal>,
) -> Result<Self, GeometryLayoutError> {
Self::admit_sample_points(&samples)?;
let chordal_deflection = Self::admit_deflection(deflection())?;
let samples = Self::admit_sample_parameters(samples)?;
Ok(Self {
samples,
chordal_deflection,
})
}
fn require_sample_count(count: usize) -> Result<(), GeometryLayoutError> {
if count < 2 {
return Err(layout_error("polyline must contain at least two points"));
}
Ok(())
}
fn admit_deflection(
value: Option<NonNegativeReal>,
) -> Result<NonNegativeReal, GeometryLayoutError> {
match value {
Some(value) => Ok(value),
None => Err(layout_error(
"chordal_deflection must be finite and non-negative",
)),
}
}
fn admit_sample_points(samples: &PolylineSamples) -> Result<(), GeometryLayoutError> {
Self::require_sample_count(samples.count())?;
for index in 0..samples.count() {
let point = match samples {
PolylineSamples::Unparameterized { points } => points[index],
PolylineSamples::Parameterized { vertices } => vertices[index].point,
};
if !point.is_finite() {
return Err(layout_error("points must be finite"));
}
}
Ok(())
}
fn admit_sample_parameters(
samples: PolylineSamples,
) -> Result<PolylineSamples<FiniteReal, FinitePoint3>, GeometryLayoutError> {
fn members<T>(
values: Vec<T>,
) -> Result<crate::features::NonEmptyMembers<T>, GeometryLayoutError> {
match values.try_into() {
Ok(values) => Ok(values),
Err(_) => Err(layout_error("polyline must contain at least two points")),
}
}
let point = |point| match FinitePoint3::new(point) {
Some(point) => Ok(point),
None => Err(layout_error("points must be finite")),
};
let samples = match samples {
PolylineSamples::Unparameterized { points } => PolylineSamples::Unparameterized {
points: members(
points
.into_iter()
.map(point)
.collect::<Result<Vec<_>, _>>()?,
)?,
},
PolylineSamples::Parameterized { vertices } => PolylineSamples::Parameterized {
vertices: members(
vertices
.into_iter()
.map(|vertex| {
let Some(parameter) = FiniteReal::new(vertex.parameter) else {
return Err(layout_error(
"parameters must be finite and strictly monotonic",
));
};
Ok(PolylineVertex {
parameter,
point: point(vertex.point)?,
})
})
.collect::<Result<Vec<_>, _>>()?,
)?,
},
};
if !samples.has_strictly_monotonic_parameters() {
return Err(layout_error(
"parameters must be finite and strictly monotonic",
));
}
Ok(samples)
}
pub fn points(&self) -> impl Iterator<Item = FinitePoint3> + '_ {
self.samples.points()
}
#[must_use]
pub fn point_count(&self) -> usize {
self.samples.count()
}
#[must_use]
pub fn point_at(&self, index: usize) -> Option<FinitePoint3> {
match &self.samples {
PolylineSamples::Unparameterized { points } => points.get(index).copied(),
PolylineSamples::Parameterized { vertices } => vertices.get(index).map(|row| row.point),
}
}
#[must_use]
pub fn parameter_at(&self, index: usize) -> Option<FiniteReal> {
match &self.samples {
PolylineSamples::Unparameterized { points } => points
.get(index)
.and_then(|_| FiniteReal::from_index(index)),
PolylineSamples::Parameterized { vertices } => {
vertices.get(index).map(|row| row.parameter)
}
}
}
pub fn parameters(&self) -> Option<impl Iterator<Item = FiniteReal> + '_> {
self.samples.parameters()
}
pub fn edit_samples(
&mut self,
edit: impl FnOnce(&mut PolylineSamples) -> Result<(), GeometryLayoutError>,
) -> Result<(), GeometryLayoutError> {
let mut candidate = self.samples.to_raw();
edit(&mut candidate)?;
Self::admit_sample_points(&candidate)?;
self.samples = Self::admit_sample_parameters(candidate)?;
Ok(())
}
#[must_use]
pub const fn chordal_deflection(&self) -> NonNegativeReal {
self.chordal_deflection
}
pub fn set_chordal_deflection(
&mut self,
chordal_deflection: f64,
) -> Result<(), GeometryLayoutError> {
self.chordal_deflection = admit_chordal_deflection(chordal_deflection)?;
Ok(())
}
}
fn scale_admitted_points<'a>(
points: impl Iterator<Item = &'a mut FinitePoint3>,
scale: PositiveReal,
message: &'static str,
) -> Result<(), GeometryLayoutError> {
for point in points {
*point = point
.scaled(scale)
.ok_or_else(|| GeometryLayoutError::Layout(message.to_owned()))?;
}
Ok(())
}
fn scale_admitted_deflection(
deflection: &mut NonNegativeReal,
scale: PositiveReal,
) -> Result<(), GeometryLayoutError> {
*deflection = deflection.scaled(scale).ok_or_else(|| {
GeometryLayoutError::Layout("chordal_deflection must be finite and non-negative".to_owned())
})?;
Ok(())
}
impl PolygonalSurface {
pub(crate) fn scale_points(&mut self, scale: PositiveReal) -> Result<(), GeometryLayoutError> {
scale_admitted_points(self.vertices.iter_mut(), scale, "vertices must be finite")?;
scale_admitted_deflection(&mut self.chordal_deflection, scale)
}
}
impl PolylineCurve {
pub(crate) fn scale_points(&mut self, scale: PositiveReal) -> Result<(), GeometryLayoutError> {
match &mut self.samples {
PolylineSamples::Unparameterized { points } => {
scale_admitted_points(points.iter_mut(), scale, "points must be finite")
}
PolylineSamples::Parameterized { vertices } => scale_admitted_points(
vertices.iter_mut().map(|vertex| &mut vertex.point),
scale,
"points must be finite",
),
}?;
scale_admitted_deflection(&mut self.chordal_deflection, scale)
}
}
#[cfg(test)]
mod tests;
mod identity_rewrite;