use crate::features::{BodySelectionError, FinitePoint3, NonEmptyMembers};
use crate::ids::{
BodyId, CoedgeId, CurveId, EdgeId, FaceId, IdentityKey, IdentityNamespace, LoopId, PcurveId,
PointId, RegionId, ShellId, SurfaceId, VertexId,
};
use crate::math::Point3;
use crate::scalar::UnitBinary32;
use crate::transform::Transform;
use cadmpeg_core::decode::{u64_from_index, DecodeContext};
#[cfg(feature = "schema")]
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(try_from = "ColorWire")]
pub struct Color {
r: f32,
g: f32,
b: f32,
a: f32,
}
#[derive(Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(deny_unknown_fields)]
struct ColorWire {
r: f32,
g: f32,
b: f32,
a: f32,
}
impl TryFrom<ColorWire> for Color {
type Error = String;
fn try_from(wire: ColorWire) -> Result<Self, Self::Error> {
for (field, value) in [("r", wire.r), ("g", wire.g), ("b", wire.b), ("a", wire.a)] {
if !(0.0..=1.0).contains(&value) {
return Err(format!("color {field} must be finite and in [0, 1]"));
}
}
Ok(Self {
r: wire.r,
g: wire.g,
b: wire.b,
a: wire.a,
})
}
}
impl Color {
pub fn new(r: f32, g: f32, b: f32, a: f32) -> Option<Self> {
Self::try_from(ColorWire { r, g, b, a }).ok()
}
#[must_use]
pub fn from_unit_binary32(components: [UnitBinary32; 4]) -> Self {
let [r, g, b, a] = components.map(UnitBinary32::get);
Self { r, g, b, a }
}
pub fn from_rgba8(r: u8, g: u8, b: u8, a: u8) -> Self {
Self {
r: f32::from(r) / 255.0,
g: f32::from(g) / 255.0,
b: f32::from(b) / 255.0,
a: f32::from(a) / 255.0,
}
}
pub const fn r(self) -> f32 {
self.r
}
pub const fn g(self) -> f32 {
self.g
}
pub const fn b(self) -> f32 {
self.b
}
pub const fn a(self) -> f32 {
self.a
}
#[must_use]
pub fn invert_alpha(self) -> Self {
Self {
a: 1.0 - self.a,
..self
}
}
pub fn with_alpha(self, a: f32) -> Option<Self> {
Self::new(self.r, self.g, self.b, a)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "snake_case")]
#[serde(deny_unknown_fields)]
pub enum Sense {
Forward,
Reversed,
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "snake_case")]
#[serde(deny_unknown_fields)]
pub enum BodyKind {
#[default]
Solid,
Sheet,
Wire,
General,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct Body {
pub id: BodyId,
#[serde(default)]
pub kind: BodyKind,
pub regions: Vec<RegionId>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_transform"
)]
pub transform: Option<Transform>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_name"
)]
pub name: Option<String>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_color"
)]
pub color: Option<Color>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_visible"
)]
pub visible: Option<bool>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct Region {
pub id: RegionId,
pub body: BodyId,
pub shells: Vec<ShellId>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(tag = "kind", rename_all = "snake_case")]
#[serde(deny_unknown_fields)]
pub enum ShellMember {
Face {
id: FaceId,
},
WireEdge {
id: EdgeId,
},
FreeVertex {
id: VertexId,
},
}
#[derive(Debug, Clone, PartialEq, Deserialize)]
#[serde(from = "NonEmptyMembers<ShellMember>", into = "Vec<ShellMember>")]
struct ShellMembers {
faces: Vec<FaceId>,
wire_edges: Vec<EdgeId>,
free_vertices: Vec<VertexId>,
}
impl From<NonEmptyMembers<ShellMember>> for ShellMembers {
fn from(members: NonEmptyMembers<ShellMember>) -> Self {
let mut sorted = Self {
faces: Vec::new(),
wire_edges: Vec::new(),
free_vertices: Vec::new(),
};
for member in members {
match member {
ShellMember::Face { id } => sorted.faces.push(id),
ShellMember::WireEdge { id } => sorted.wire_edges.push(id),
ShellMember::FreeVertex { id } => sorted.free_vertices.push(id),
}
}
sorted
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct Shell {
pub id: ShellId,
pub region: RegionId,
#[cfg_attr(feature = "schema", schemars(with = "Vec<ShellMember>"))]
members: ShellMembers,
}
impl Shell {
pub fn new(
id: ShellId,
region: RegionId,
faces: Vec<FaceId>,
wire_edges: Vec<EdgeId>,
free_vertices: Vec<VertexId>,
) -> Result<Self, BodySelectionError> {
if faces.is_empty() && wire_edges.is_empty() && free_vertices.is_empty() {
return Err(BodySelectionError::Empty);
}
let members = ShellMembers {
faces,
wire_edges,
free_vertices,
};
Ok(Self {
id,
region,
members,
})
}
pub fn with_face(id: ShellId, region: RegionId, face: FaceId) -> Self {
Self {
id,
region,
members: ShellMembers {
faces: vec![face],
wire_edges: Vec::new(),
free_vertices: Vec::new(),
},
}
}
pub fn with_wire_edge(id: ShellId, region: RegionId, edge: EdgeId) -> Self {
Self {
id,
region,
members: ShellMembers {
faces: Vec::new(),
wire_edges: vec![edge],
free_vertices: Vec::new(),
},
}
}
pub fn with_free_vertex(id: ShellId, region: RegionId, vertex: VertexId) -> Self {
Self {
id,
region,
members: ShellMembers {
faces: Vec::new(),
wire_edges: Vec::new(),
free_vertices: vec![vertex],
},
}
}
pub fn with_faces(
id: ShellId,
region: RegionId,
faces: Vec<FaceId>,
) -> Result<Self, BodySelectionError> {
if faces.is_empty() {
return Err(BodySelectionError::Empty);
}
Ok(Self {
id,
region,
members: ShellMembers {
faces,
wire_edges: Vec::new(),
free_vertices: Vec::new(),
},
})
}
pub fn with_free_vertices(
id: ShellId,
region: RegionId,
free_vertices: Vec<VertexId>,
) -> Result<Self, BodySelectionError> {
if free_vertices.is_empty() {
return Err(BodySelectionError::Empty);
}
Ok(Self {
id,
region,
members: ShellMembers {
faces: Vec::new(),
wire_edges: Vec::new(),
free_vertices,
},
})
}
pub fn faces(&self) -> &[FaceId] {
&self.members.faces
}
pub fn wire_edges(&self) -> &[EdgeId] {
&self.members.wire_edges
}
pub fn free_vertices(&self) -> &[VertexId] {
&self.members.free_vertices
}
pub fn replace_faces(&mut self, faces: Vec<FaceId>) -> Result<(), BodySelectionError> {
if faces.is_empty()
&& self.members.wire_edges.is_empty()
&& self.members.free_vertices.is_empty()
{
return Err(BodySelectionError::Empty);
}
self.members.faces = faces;
Ok(())
}
pub fn edit_topology<R>(
&mut self,
edit: impl FnOnce(&mut Vec<FaceId>, &mut Vec<EdgeId>, &mut Vec<VertexId>) -> R,
) -> Result<R, BodySelectionError> {
let mut faces = self.members.faces.clone();
let mut wire_edges = self.members.wire_edges.clone();
let mut free_vertices = self.members.free_vertices.clone();
let result = edit(&mut faces, &mut wire_edges, &mut free_vertices);
*self = Self::new(
self.id.clone(),
self.region.clone(),
faces,
wire_edges,
free_vertices,
)?;
Ok(result)
}
pub fn add_face(&mut self, face: FaceId) {
self.members.faces.push(face);
}
pub fn add_wire_edge(&mut self, edge: EdgeId) {
self.members.wire_edges.push(edge);
}
pub fn add_free_vertex(&mut self, vertex: VertexId) {
self.members.free_vertices.push(vertex);
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct Face {
pub id: FaceId,
pub shell: ShellId,
pub surface: SurfaceId,
pub sense: Sense,
pub loops: FaceLoops,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_name"
)]
pub name: Option<String>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_color"
)]
pub color: Option<Color>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[serde(deserialize_with = "deserialize_tolerance")]
pub tolerance: Option<crate::scalar::PositiveReal>,
}
impl Face {
#[must_use]
pub fn loop_role(&self, id: &LoopId) -> LoopBoundaryRole {
self.loops.role(id)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(tag = "classification", rename_all = "snake_case")]
#[serde(deny_unknown_fields)]
pub enum FaceLoops {
Unspecified {
loops: Vec<LoopId>,
},
Classified {
outer: LoopId,
inner: Vec<LoopId>,
},
}
impl FaceLoops {
#[must_use]
pub const fn unspecified(ids: Vec<LoopId>) -> Self {
Self::Unspecified { loops: ids }
}
#[must_use]
pub const fn classified(outer: LoopId, inner: Vec<LoopId>) -> Self {
Self::Classified { outer, inner }
}
pub fn iter(&self) -> Box<dyn Iterator<Item = &LoopId> + '_> {
match self {
Self::Unspecified { loops } => Box::new(loops.iter()),
Self::Classified { outer, inner } => {
Box::new(std::iter::once(outer).chain(inner.iter()))
}
}
}
#[must_use]
pub fn contains(&self, id: &LoopId) -> bool {
self.iter().any(|member| member == id)
}
#[must_use]
pub fn to_vec(&self) -> Vec<LoopId> {
self.iter().cloned().collect()
}
#[must_use]
pub fn len(&self) -> usize {
match self {
Self::Unspecified { loops } => loops.len(),
Self::Classified { inner, .. } => 1 + inner.len(),
}
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.len() == 0
}
#[must_use]
pub fn role(&self, id: &LoopId) -> LoopBoundaryRole {
match self {
Self::Unspecified { .. } => LoopBoundaryRole::Unspecified,
Self::Classified { outer, inner } => {
if outer == id {
LoopBoundaryRole::Outer
} else if inner.iter().any(|member| member == id) {
LoopBoundaryRole::Inner
} else {
LoopBoundaryRole::Unspecified
}
}
}
}
}
impl PartialEq<Vec<LoopId>> for FaceLoops {
fn eq(&self, other: &Vec<LoopId>) -> bool {
self.iter().eq(other.iter())
}
}
impl PartialEq<FaceLoops> for Vec<LoopId> {
fn eq(&self, other: &FaceLoops) -> bool {
other.iter().eq(self.iter())
}
}
impl<'a> IntoIterator for &'a FaceLoops {
type Item = &'a LoopId;
type IntoIter = Box<dyn Iterator<Item = &'a LoopId> + 'a>;
fn into_iter(self) -> Self::IntoIter {
Box::new(self.iter())
}
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "snake_case")]
#[serde(deny_unknown_fields)]
pub enum LoopBoundaryRole {
#[default]
Unspecified,
Outer,
Inner,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct Loop {
pub id: LoopId,
pub face: FaceId,
pub boundary: LoopBoundary,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct PcurveUse {
pub pcurve: PcurveId,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_isoparametric"
)]
pub isoparametric: Option<bool>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_parameter_range"
)]
pub parameter_range: Option<crate::geometry::DirectedParameterRange>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
pub enum LoopBoundary {
Vertex {
vertex: VertexId,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pcurves: Vec<PcurveUse>,
},
Ring(LoopRing),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LoopRingError(&'static str);
impl std::fmt::Display for LoopRingError {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.write_str(self.0)
}
}
impl std::error::Error for LoopRingError {}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum LoopRingAdmissionError {
#[error("{0}")]
Invalid(LoopRingError),
#[error("decode resource limit: {0:?}")]
Resource(cadmpeg_core::decode::ResourceLimit),
}
impl From<cadmpeg_core::decode::ResourceLimit> for LoopRingAdmissionError {
fn from(limit: cadmpeg_core::decode::ResourceLimit) -> Self {
Self::Resource(limit)
}
}
impl From<LoopRingError> for LoopRingAdmissionError {
fn from(error: LoopRingError) -> Self {
Self::Invalid(error)
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(try_from = "LoopRingWire")]
pub struct LoopRing {
coedges: Vec<CoedgeId>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
vertex_uses: Vec<AnchoredVertexUse>,
}
#[derive(Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(deny_unknown_fields)]
struct LoopRingWire {
coedges: Vec<CoedgeId>,
#[serde(default)]
vertex_uses: Vec<AnchoredVertexUse>,
}
impl TryFrom<LoopRingWire> for LoopRing {
type Error = LoopRingAdmissionError;
fn try_from(wire: LoopRingWire) -> Result<Self, Self::Error> {
if wire.coedges.is_empty() {
return Err(LoopRingError("loop ring must contain a coedge").into());
}
let mut members = std::collections::HashSet::new();
members
.try_reserve(wire.coedges.len())
.map_err(|_| LoopRingError("loop ring member allocation failed"))?;
for coedge in &wire.coedges {
if !members.insert(coedge) {
return Err(LoopRingError("loop ring coedges must be distinct").into());
}
}
for vertex_use in &wire.vertex_uses {
if !members.contains(&vertex_use.after) {
return Err(LoopRingError(
"loop ring vertex-use after must name a coedge in the ring",
)
.into());
}
}
Ok(Self {
coedges: wire.coedges,
vertex_uses: wire.vertex_uses,
})
}
}
impl LoopRing {
pub fn single(coedge: CoedgeId) -> Self {
Self {
coedges: vec![coedge],
vertex_uses: Vec::new(),
}
}
pub fn from_vertices(
namespace: &IdentityNamespace,
key_prefix: &IdentityKey,
vertices: NonEmptyMembers<VertexId>,
) -> Self {
let mut coedges = Vec::new();
let mut vertex_uses = Vec::new();
for (ordinal, vertex) in vertices.into_iter().enumerate() {
let coedge = CoedgeId::compose(namespace, key_prefix.clone().colon(ordinal));
let vertex_use = AnchoredVertexUse {
vertex,
after: coedge.clone(),
pcurves: Vec::new(),
};
coedges.push(coedge);
vertex_uses.push(vertex_use);
}
Self {
coedges,
vertex_uses,
}
}
pub fn try_push(&mut self, coedge: CoedgeId) -> Result<(), LoopRingError> {
if self.coedges.contains(&coedge) {
return Err(LoopRingError("loop ring coedges must be distinct"));
}
self.coedges.push(coedge);
Ok(())
}
pub fn new(
ctx: &DecodeContext<'_>,
coedges: Vec<CoedgeId>,
vertex_uses: Vec<AnchoredVertexUse>,
) -> Result<Result<Self, LoopRingError>, cadmpeg_core::decode::ResourceLimit> {
if coedges.is_empty() {
return Ok(Err(LoopRingError("loop ring must contain a coedge")));
}
let (storage, mut members) = {
let mut values = Vec::new();
let reservation =
ctx.reserve_temporary_vec(&mut values, coedges.len(), "loop ring members")?;
(reservation, values)
};
for coedge in &coedges {
ctx.charge_work_limit(1, "loop ring members")?;
let Err(position) = Self::coedge_position(ctx, &members, coedge, "loop ring members")?
else {
return Ok(Err(LoopRingError("loop ring coedges must be distinct")));
};
ctx.charge_work_limit(
u64_from_index(members.len() - position),
"loop ring members",
)?;
ctx.charge_work_limit(1, "loop ring members")?;
members.insert(position, coedge);
}
for vertex_use in &vertex_uses {
ctx.charge_work_limit(1, "loop ring anchors")?;
if Self::coedge_position(ctx, &members, &vertex_use.after, "loop ring anchors")?
.is_err()
{
return Ok(Err(LoopRingError(
"loop ring vertex-use after must name a coedge in the ring",
)));
}
}
drop(members);
drop(storage);
Ok(Ok(Self {
coedges,
vertex_uses,
}))
}
fn coedge_position(
ctx: &DecodeContext<'_>,
members: &[&CoedgeId],
coedge: &CoedgeId,
operation: &'static str,
) -> Result<Result<usize, usize>, cadmpeg_core::decode::ResourceLimit> {
let mut low = 0;
let mut high = members.len();
while low < high {
ctx.charge_work_limit(1, operation)?;
let middle = low + (high - low) / 2;
let candidate = members[middle].as_str();
let query = coedge.as_str();
ctx.charge_work_limit(u64_from_index(candidate.len().min(query.len())), operation)?;
match candidate.cmp(query) {
std::cmp::Ordering::Less => low = middle + 1,
std::cmp::Ordering::Greater => high = middle,
std::cmp::Ordering::Equal => return Ok(Ok(middle)),
}
}
Ok(Err(low))
}
#[must_use]
pub fn coedges(&self) -> &[CoedgeId] {
&self.coedges
}
#[must_use]
pub fn vertex_uses(&self) -> &[AnchoredVertexUse] {
&self.vertex_uses
}
}
impl Loop {
pub fn replace_ring(
&mut self,
ctx: &DecodeContext<'_>,
coedges: Vec<CoedgeId>,
vertex_uses: Vec<AnchoredVertexUse>,
) -> Result<(), LoopRingAdmissionError> {
if !matches!(&self.boundary, LoopBoundary::Ring(_)) {
return Err(LoopRingError("cannot replace the ring of a vertex-only loop").into());
}
self.boundary = LoopBoundary::Ring(
LoopRing::new(ctx, coedges, vertex_uses)?.map_err(LoopRingAdmissionError::from)?,
);
Ok(())
}
#[must_use]
pub fn coedges(&self) -> &[CoedgeId] {
match &self.boundary {
LoopBoundary::Vertex { .. } => &[],
LoopBoundary::Ring(ring) => ring.coedges(),
}
}
#[must_use]
pub fn anchored_vertex_uses(&self) -> &[AnchoredVertexUse] {
match &self.boundary {
LoopBoundary::Vertex { .. } => &[],
LoopBoundary::Ring(ring) => ring.vertex_uses(),
}
}
#[must_use]
pub fn singular_vertex(&self) -> Option<(&VertexId, &[PcurveUse])> {
match &self.boundary {
LoopBoundary::Vertex { vertex, pcurves } => Some((vertex, pcurves)),
LoopBoundary::Ring(_) => None,
}
}
pub fn vertices(&self) -> impl Iterator<Item = &VertexId> {
let (singular, anchored) = match &self.boundary {
LoopBoundary::Vertex { vertex, .. } => (Some(vertex), &[][..]),
LoopBoundary::Ring(ring) => (None, ring.vertex_uses()),
};
singular
.into_iter()
.chain(anchored.iter().map(|use_| &use_.vertex))
}
#[must_use]
pub fn next_coedge(&self, id: &CoedgeId) -> Option<&CoedgeId> {
let ring = self.coedges();
let index = ring.iter().position(|coedge| coedge == id)?;
ring.get((index + 1) % ring.len())
}
#[must_use]
pub fn previous_coedge(&self, id: &CoedgeId) -> Option<&CoedgeId> {
let ring = self.coedges();
let index = ring.iter().position(|coedge| coedge == id)?;
ring.get((index + ring.len() - 1) % ring.len())
}
#[must_use]
pub fn ring_neighbors_of(&self, coedge: &Coedge) -> Option<(CoedgeId, CoedgeId)> {
if self.id != coedge.owner_loop {
return None;
}
Some((
self.next_coedge(&coedge.id)?.clone(),
self.previous_coedge(&coedge.id)?.clone(),
))
}
pub fn vertex_pcurves(&self) -> impl Iterator<Item = &PcurveUse> {
let (singular, anchored) = match &self.boundary {
LoopBoundary::Vertex { pcurves, .. } => (Some(pcurves.as_slice()), &[][..]),
LoopBoundary::Ring(ring) => (None, ring.vertex_uses()),
};
singular
.into_iter()
.flatten()
.chain(anchored.iter().flat_map(|use_| use_.pcurves.iter()))
}
pub fn vertex_occurrences(
&self,
) -> impl Iterator<Item = (&VertexId, Option<&CoedgeId>, &[PcurveUse])> {
let (singular, anchored) = match &self.boundary {
LoopBoundary::Vertex { vertex, pcurves } => {
(Some((vertex, pcurves.as_slice())), &[][..])
}
LoopBoundary::Ring(ring) => (None, ring.vertex_uses()),
};
singular
.into_iter()
.map(|(vertex, pcurves)| (vertex, None, pcurves))
.chain(
anchored
.iter()
.map(|use_| (&use_.vertex, Some(&use_.after), use_.pcurves.as_slice())),
)
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct AnchoredVertexUse {
pub vertex: VertexId,
pub after: CoedgeId,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub pcurves: Vec<PcurveUse>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct Coedge {
pub id: CoedgeId,
pub owner_loop: LoopId,
pub edge: EdgeId,
pub radial_next: CoedgeId,
pub sense: Sense,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub pcurves: Vec<PcurveUse>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_use_curve"
)]
pub use_curve: Option<CoedgeUseCurve>,
}
#[must_use]
pub fn coedge_ring_neighbors(loops: &[Loop], coedge: &Coedge) -> Option<(CoedgeId, CoedgeId)> {
loops
.iter()
.find_map(|loop_| loop_.ring_neighbors_of(coedge))
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct CoedgeUseCurve {
pub curve: CurveId,
pub parameter_range: ParameterInterval,
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(try_from = "[f64; 2]", into = "[f64; 2]")]
pub struct ParameterInterval([f64; 2]);
impl ParameterInterval {
pub(crate) const UNIT: Self = Self([0.0, 1.0]);
pub(crate) fn ordered(
lower: crate::scalar::FiniteReal,
upper: crate::scalar::FiniteReal,
) -> Option<Self> {
(lower <= upper).then_some(Self([lower.get(), upper.get()]))
}
pub fn new(endpoints: [f64; 2]) -> Result<Self, &'static str> {
let endpoints = crate::units::FiniteVector::new(endpoints)
.ok_or("parameter_range must be finite and ordered")?;
Self::from_finite_endpoints(endpoints)
}
pub fn from_finite_endpoints(
endpoints: crate::units::FiniteVector<2>,
) -> Result<Self, &'static str> {
let endpoints = endpoints.get();
(endpoints[0] <= endpoints[1])
.then_some(Self(endpoints))
.ok_or("parameter_range must be finite and ordered")
}
#[must_use]
pub fn scaled(self, scale: crate::scalar::PositiveReal) -> Option<Self> {
let endpoints = self.0.map(|value| value * scale.get());
endpoints
.iter()
.all(|value| value.is_finite())
.then_some(Self(endpoints))
}
pub const fn endpoints(self) -> [f64; 2] {
self.0
}
}
impl crate::geometry::nurbs::NurbsCurve {
#[must_use]
pub fn full_knot_endpoints(&self) -> ParameterInterval {
let knots = self.knots();
ParameterInterval([knots[0], knots[knots.len() - 1]])
}
}
impl crate::geometry::nurbs::KnotVector {
#[must_use]
pub(crate) fn span(&self, first: usize, last: usize) -> Option<ParameterInterval> {
(first <= last).then_some(())?;
Some(ParameterInterval([*self.get(first)?, *self.get(last)?]))
}
pub(crate) fn active_spans<'ctx>(
&self,
ctx: &'ctx DecodeContext<'_>,
first: usize,
last: usize,
) -> Result<
Option<crate::geometry::nurbs::scoped::ScopedRows<'ctx, IncreasingParameterInterval>>,
cadmpeg_core::decode::ResourceLimit,
> {
let Some(knots) = self.get(first..=last) else {
return Ok(None);
};
let mut storage = ctx.reserve_scoped_limit(0, "IR active knot spans")?;
let mut spans = Vec::new();
for pair in knots.windows(2) {
ctx.charge_work_limit(1, "IR active knot span visit")?;
if pair[0] < pair[1] {
ctx.reserve_scoped_vec_limit(&mut storage, &mut spans, 1, "IR active knot spans")?;
ctx.charge_work_limit(1, "IR active knot span copy")?;
spans.push(IncreasingParameterInterval([pair[0], pair[1]]));
}
}
Ok(Some(crate::geometry::nurbs::scoped::ScopedRows::new(
spans, storage,
)))
}
}
impl TryFrom<[f64; 2]> for ParameterInterval {
type Error = &'static str;
fn try_from(value: [f64; 2]) -> Result<Self, Self::Error> {
Self::new(value)
}
}
impl From<ParameterInterval> for [f64; 2] {
fn from(value: ParameterInterval) -> Self {
value.0
}
}
impl From<IncreasingParameterInterval> for ParameterInterval {
fn from(value: IncreasingParameterInterval) -> Self {
Self(value.endpoints())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(try_from = "[f64; 2]", into = "[f64; 2]")]
pub struct IncreasingParameterInterval([f64; 2]);
impl IncreasingParameterInterval {
pub fn from_finite_endpoints(endpoints: crate::units::FiniteVector<2>) -> Option<Self> {
(endpoints[0] < endpoints[1]).then_some(Self(endpoints.get()))
}
pub fn between(
lower: crate::scalar::FiniteReal,
upper: crate::scalar::FiniteReal,
) -> Option<Self> {
(lower < upper).then_some(Self([lower.get(), upper.get()]))
}
pub fn new(endpoints: [f64; 2]) -> Option<Self> {
(endpoints.iter().all(|value| value.is_finite()) && endpoints[0] < endpoints[1])
.then_some(Self(endpoints))
}
pub const fn endpoints(self) -> [f64; 2] {
self.0
}
pub const fn lower(self) -> f64 {
self.0[0]
}
pub const fn upper(self) -> f64 {
self.0[1]
}
#[must_use]
pub(crate) fn split_at_midpoint(self) -> Option<(Self, Self)> {
let [lower, upper] = self.0;
let middle = lower.midpoint(upper);
(lower < middle && middle < upper).then_some((Self([lower, middle]), Self([middle, upper])))
}
}
impl TryFrom<[f64; 2]> for IncreasingParameterInterval {
type Error = &'static str;
fn try_from(value: [f64; 2]) -> Result<Self, Self::Error> {
Self::new(value).ok_or("parameter interval must be finite and strictly increasing")
}
}
impl From<IncreasingParameterInterval> for [f64; 2] {
fn from(value: IncreasingParameterInterval) -> Self {
value.0
}
}
#[derive(Debug, Clone, PartialEq, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[cfg_attr(feature = "schema", schemars(with = "EdgeCarrierWire"))]
#[serde(try_from = "EdgeCarrierWire", into = "EdgeCarrierWire")]
pub enum EdgeCarrier {
Free,
Endpoints([crate::scalar::FiniteReal; 2]),
Curve(CurveId),
Bounded(CurveId, ParameterInterval),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(deny_unknown_fields)]
struct EdgeCarrierWire {
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_curve"
)]
curve: Option<CurveId>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_param_range"
)]
param_range: Option<[f64; 2]>,
}
impl EdgeCarrier {
pub fn unbounded(curve: Option<CurveId>) -> Self {
match curve {
Some(curve) => Self::Curve(curve),
None => Self::Free,
}
}
pub fn from_finite_parts(
curve: Option<CurveId>,
param_range: Option<[crate::scalar::FiniteReal; 2]>,
) -> Result<Self, &'static str> {
match (curve, param_range) {
(None, None) => Ok(Self::Free),
(Some(curve), None) => Ok(Self::Curve(curve)),
(Some(curve), Some(range)) => ParameterInterval::from_finite_endpoints(range.into())
.map(|interval| Self::Bounded(curve, interval))
.map_err(|_| "edge param_range must be finite and ordered"),
(None, Some(range)) => Ok(Self::Endpoints(range)),
}
}
pub fn new(
curve: Option<CurveId>,
param_range: Option<[f64; 2]>,
) -> Result<Self, &'static str> {
match (curve, param_range) {
(None, None) => Ok(Self::Free),
(Some(curve), None) => Ok(Self::Curve(curve)),
(Some(curve), Some(range)) => ParameterInterval::new(range)
.map(|interval| Self::Bounded(curve, interval))
.map_err(|_| "edge param_range must be finite and ordered"),
(None, Some([start, end])) => {
let start = crate::scalar::FiniteReal::new(start);
let end = crate::scalar::FiniteReal::new(end);
match (start, end) {
(Some(start), Some(end)) => Ok(Self::Endpoints([start, end])),
_ => Err("edge param_range endpoints must be finite"),
}
}
}
}
pub const fn curve(&self) -> Option<&CurveId> {
match self {
Self::Free | Self::Endpoints(_) => None,
Self::Curve(curve) | Self::Bounded(curve, _) => Some(curve),
}
}
pub fn param_range(&self) -> Option<crate::units::FiniteVector<2>> {
match self {
Self::Free | Self::Curve(_) => None,
Self::Endpoints(endpoints) => Some((*endpoints).into()),
Self::Bounded(_, interval) => Some((*interval).into()),
}
}
fn map_curve(&mut self, map: impl FnOnce(&CurveId) -> CurveId) {
match self {
Self::Free | Self::Endpoints(_) => {}
Self::Curve(curve) | Self::Bounded(curve, _) => *curve = map(curve),
}
}
}
impl TryFrom<EdgeCarrierWire> for EdgeCarrier {
type Error = &'static str;
fn try_from(wire: EdgeCarrierWire) -> Result<Self, Self::Error> {
Self::new(wire.curve, wire.param_range)
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct Edge {
pub id: EdgeId,
pub carrier: EdgeCarrier,
pub start: VertexId,
pub end: VertexId,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[serde(deserialize_with = "deserialize_tolerance")]
pub tolerance: Option<crate::scalar::PositiveReal>,
}
impl Edge {
pub fn map_curve(&mut self, map: impl FnOnce(&CurveId) -> CurveId) {
self.carrier.map_curve(map);
}
pub fn set_curve(&mut self, curve: Option<CurveId>) -> Result<(), &'static str> {
self.carrier = EdgeCarrier::new(
curve,
self.param_range().map(crate::units::FiniteVector::get),
)?;
Ok(())
}
pub fn set_param_range(&mut self, range: Option<ParameterInterval>) {
self.carrier = match (self.curve().cloned(), range) {
(curve, None) => EdgeCarrier::unbounded(curve),
(Some(curve), Some(range)) => EdgeCarrier::Bounded(curve, range),
(None, Some(range)) => EdgeCarrier::Endpoints(range.finite_endpoints()),
};
}
pub const fn curve(&self) -> Option<&CurveId> {
self.carrier.curve()
}
pub fn param_range(&self) -> Option<crate::units::FiniteVector<2>> {
self.carrier.param_range()
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct Vertex {
pub id: VertexId,
pub point: PointId,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[serde(deserialize_with = "deserialize_tolerance")]
pub tolerance: Option<crate::scalar::PositiveReal>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(try_from = "PointWire")]
pub struct Point {
pub id: PointId,
position: FinitePoint3,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_source_object"
)]
pub source_object: Option<crate::provenance::SourceObjectAssociation>,
}
#[derive(Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(deny_unknown_fields)]
struct PointWire {
id: PointId,
position: Point3,
#[serde(default, deserialize_with = "deserialize_source_object")]
source_object: Option<crate::provenance::SourceObjectAssociation>,
}
impl Point {
pub const NON_FINITE_POSITION: &'static str = "point position coordinates must be finite";
#[must_use]
pub fn new(
id: PointId,
position: FinitePoint3,
source_object: Option<crate::provenance::SourceObjectAssociation>,
) -> Self {
Self {
id,
position,
source_object,
}
}
#[must_use]
pub const fn position(&self) -> FinitePoint3 {
self.position
}
pub fn set_position(&mut self, position: FinitePoint3) {
self.position = position;
}
}
impl TryFrom<PointWire> for Point {
type Error = &'static str;
fn try_from(wire: PointWire) -> Result<Self, Self::Error> {
let position = FinitePoint3::new(wire.position).ok_or(Self::NON_FINITE_POSITION)?;
Ok(Self::new(wire.id, position, wire.source_object))
}
}
cadmpeg_core::named_optional_field!(
deserialize_tolerance,
crate::scalar::PositiveReal,
"tolerance"
);
#[cfg(test)]
mod tests {
#[test]
fn color_from_unit_binary32_preserves_admitted_components() {
use super::Color;
use crate::scalar::UnitBinary32;
let components = [-0.0_f32, 0.25, 1.0, 0.5]
.map(|value| UnitBinary32::new(value).expect("each component is in the unit interval"));
let color = Color::from_unit_binary32(components);
assert_eq!(
[color.r(), color.g(), color.b(), color.a()].map(f32::to_bits),
[-0.0_f32, 0.25, 1.0, 0.5].map(f32::to_bits)
);
}
#[test]
fn checked_endpoints_build_ordered_and_increasing_intervals() {
use super::{IncreasingParameterInterval, ParameterInterval};
use crate::scalar::FiniteReal;
let [low, high] = [-1.0, 3.0].map(|value| FiniteReal::new(value).unwrap());
assert_eq!(
ParameterInterval::ordered(low, high).map(ParameterInterval::endpoints),
Some([-1.0, 3.0])
);
assert_eq!(
ParameterInterval::ordered(low, low).map(ParameterInterval::endpoints),
Some([-1.0, -1.0])
);
assert!(ParameterInterval::ordered(high, low).is_none());
assert_eq!(ParameterInterval::UNIT.endpoints(), [0.0, 1.0]);
assert_eq!(
IncreasingParameterInterval::between(low, high)
.map(IncreasingParameterInterval::endpoints),
Some([-1.0, 3.0])
);
assert!(IncreasingParameterInterval::between(low, low).is_none());
let (lower, upper) = IncreasingParameterInterval::between(low, high)
.and_then(IncreasingParameterInterval::split_at_midpoint)
.expect("the midpoint lies strictly inside");
assert_eq!(lower.endpoints(), [-1.0, 1.0]);
assert_eq!(upper.endpoints(), [1.0, 3.0]);
let next = f64::from_bits(1.0_f64.to_bits() + 1);
let narrow = IncreasingParameterInterval::new([1.0, next]).unwrap();
assert!(narrow.split_at_midpoint().is_none());
}
#[test]
fn finite_vector_endpoints_keep_bits_when_ordered_as_an_interval() {
use super::IncreasingParameterInterval;
use crate::units::FiniteVector;
let endpoints = FiniteVector::new([-0.0, f64::MAX]).expect("finite endpoints");
let interval = IncreasingParameterInterval::from_finite_endpoints(endpoints)
.expect("strictly increasing endpoints");
assert_eq!(interval.lower().to_bits(), (-0.0_f64).to_bits());
assert_eq!(interval.upper().to_bits(), f64::MAX.to_bits());
assert!(IncreasingParameterInterval::from_finite_endpoints(
FiniteVector::new([1.0, 1.0]).expect("finite endpoints")
)
.is_none());
assert!(IncreasingParameterInterval::from_finite_endpoints(
FiniteVector::new([2.0, 1.0]).expect("finite endpoints")
)
.is_none());
}
#[test]
fn active_knot_spans_preserve_refusals_and_hold_scoped_storage() {
use cadmpeg_core::decode::{DecodeArena, DecodeContext, DecodePolicy, ResourceDimension};
use cadmpeg_core::CodecError;
let knots = crate::geometry::nurbs::KnotVector::new(
&cadmpeg_test_support::service_decode_context(),
vec![0.0, 0.0, 1.0, 1.0, 2.5, 4.0],
)
.expect("knot admission")
.expect("nondecreasing");
for cap in 0..8 {
let mut policy = DecodePolicy::service();
policy.limits.max_work_units = cap;
let arena = DecodeArena::new();
let (ctx, _) = DecodeContext::from_root_bytes(&[], &arena, &policy).expect("root");
let limit = knots
.active_spans(&ctx, 0, 5)
.expect_err("all span work needs admission");
assert_eq!(limit.dimension, ResourceDimension::WorkUnits);
assert_eq!(limit.limit, cap);
assert!(
matches!(ctx.finish_session(), Err(CodecError::ResourceLimit(sticky)) if sticky == limit)
);
}
for dimension in [
ResourceDimension::MaterializedBytes,
ResourceDimension::CollectionItems,
] {
let mut policy = DecodePolicy::service();
match dimension {
ResourceDimension::MaterializedBytes => policy.limits.max_materialized_bytes = 0,
ResourceDimension::CollectionItems => policy.limits.max_collection_items = 0,
_ => unreachable!("tested storage dimensions"),
}
let arena = DecodeArena::new();
let (ctx, _) = DecodeContext::from_root_bytes(&[], &arena, &policy).expect("root");
let limit = knots
.active_spans(&ctx, 0, 5)
.expect_err("span storage admission");
assert_eq!(limit.dimension, dimension);
assert!(
matches!(ctx.finish_session(), Err(CodecError::ResourceLimit(sticky)) if sticky == limit)
);
}
let mut policy = DecodePolicy::service();
policy.limits.max_work_units = 8;
policy.limits.max_retained_bytes = 0;
policy.limits.max_materialized_bytes = 64;
policy.limits.max_collection_items = 3;
policy.limits.max_recursion_depth = 0;
let arena = DecodeArena::new();
let (ctx, _) = DecodeContext::from_root_bytes(&[], &arena, &policy).expect("root");
let output = knots
.active_spans(&ctx, 0, 5)
.expect("exact work")
.expect("valid range");
assert_eq!(
output
.iter()
.copied()
.map(super::IncreasingParameterInterval::endpoints)
.collect::<Vec<_>>(),
[[0.0, 1.0], [1.0, 2.5], [2.5, 4.0]]
);
drop(output);
let reuse = ctx
.reserve_scoped_limit(64, "test active spans released")
.expect("all bytes reusable");
drop(reuse);
ctx.finish_session()
.expect("scoped output with exact slots");
let mut policy = DecodePolicy::service();
policy.limits.max_work_units = 1;
policy.limits.max_retained_bytes = 0;
policy.limits.max_materialized_bytes = 0;
policy.limits.max_collection_items = 0;
let arena = DecodeArena::new();
let (ctx, _) = DecodeContext::from_root_bytes(&[], &arena, &policy).expect("root");
assert!(knots
.active_spans(&ctx, 0, 6)
.expect("invalid range is constant-time")
.is_none());
let empty = knots
.active_spans(&ctx, 2, 3)
.expect("one repeated interval")
.expect("valid range");
assert!(empty.is_empty());
drop(empty);
ctx.finish_session()
.expect("empty output allocates no storage");
}
#[test]
fn knot_spans_read_finite_intervals_between_knots() {
let knots = crate::geometry::nurbs::KnotVector::new(
&cadmpeg_test_support::service_decode_context(),
vec![0.0, 0.0, 1.0, 1.0, 2.5, 4.0],
)
.expect("fixture knot admission")
.expect("non-decreasing knots");
assert_eq!(
knots.span(1, 4).map(super::ParameterInterval::endpoints),
Some([0.0, 2.5])
);
assert!(knots.span(4, 1).is_none());
assert!(knots.span(1, 6).is_none());
assert_eq!(
knots
.active_spans(&cadmpeg_test_support::service_decode_context(), 0, 5)
.unwrap()
.map(|spans| spans
.iter()
.copied()
.map(super::IncreasingParameterInterval::endpoints)
.collect::<Vec<_>>()),
Some(vec![[0.0, 1.0], [1.0, 2.5], [2.5, 4.0]])
);
assert_eq!(
knots
.active_spans(&cadmpeg_test_support::service_decode_context(), 2, 3)
.unwrap()
.map(|spans| spans.to_vec()),
Some(Vec::new())
);
assert!(knots
.active_spans(&cadmpeg_test_support::service_decode_context(), 0, 6)
.unwrap()
.is_none());
assert_eq!(
knots.finite_knot(4).map(crate::scalar::FiniteReal::get),
Some(2.5)
);
assert!(knots.finite_knot(6).is_none());
}
#[test]
fn increasing_intervals_admit_only_finite_strictly_increasing_endpoints() {
use super::IncreasingParameterInterval;
let interval = IncreasingParameterInterval::new([-1.0, 2.5]).expect("increasing");
assert_eq!(interval.endpoints(), [-1.0, 2.5]);
assert_eq!([interval.lower(), interval.upper()], [-1.0, 2.5]);
let wire = serde_json::to_value(interval).unwrap();
assert_eq!(wire, serde_json::json!([-1.0, 2.5]));
assert_eq!(
serde_json::from_value::<IncreasingParameterInterval>(wire).unwrap(),
interval
);
for refused in [
[1.0, 1.0],
[2.0, 1.0],
[f64::NAN, 1.0],
[0.0, f64::INFINITY],
[f64::NEG_INFINITY, 0.0],
] {
assert!(IncreasingParameterInterval::new(refused).is_none());
assert_eq!(
IncreasingParameterInterval::try_from(refused),
Err("parameter interval must be finite and strictly increasing")
);
}
assert!(
serde_json::from_value::<IncreasingParameterInterval>(serde_json::json!([1.0, 1.0]))
.is_err()
);
}
#[test]
fn face_loops_are_one_tagged_object_that_round_trips() {
use super::{FaceLoops, LoopBoundaryRole};
use crate::ids::LoopId;
assert!(serde_json::from_value::<FaceLoops>(serde_json::json!([
{"id": "t:b:loop#1", "role": "outer"},
{"id": "t:b:loop#2", "role": "unspecified"}
]))
.is_err());
for wire in [
serde_json::json!({
"classification": "unspecified",
"loops": ["t:b:loop#1", "t:b:loop#2"]
}),
serde_json::json!({
"classification": "classified",
"outer": "t:b:loop#1",
"inner": ["t:b:loop#2"]
}),
] {
let loops: FaceLoops = serde_json::from_value(wire.clone()).unwrap();
assert_eq!(serde_json::to_value(&loops).unwrap(), wire);
}
let error = serde_json::from_value::<FaceLoops>(
serde_json::json!({"classification": "classified", "inner": ["t:b:loop#2"]}),
)
.unwrap_err()
.to_string();
assert!(error.contains("outer"), "{error}");
let outer = LoopId::mint("t:b:loop#1").unwrap();
let inner = LoopId::mint("t:b:loop#2").unwrap();
let classified = FaceLoops::classified(outer.clone(), vec![inner.clone()]);
assert_eq!(classified.role(&outer), LoopBoundaryRole::Outer);
assert_eq!(classified.role(&inner), LoopBoundaryRole::Inner);
let unspecified = FaceLoops::unspecified(vec![outer.clone(), inner.clone()]);
assert_eq!(unspecified.role(&outer), LoopBoundaryRole::Unspecified);
}
#[test]
fn parameter_interval_admission_preserves_direction_contracts() {
use super::{EdgeCarrier, ParameterInterval};
use crate::geometry::DirectedParameterRange;
assert!(ParameterInterval::new([2.0, 1.0]).is_err());
assert!(ParameterInterval::new([f64::INFINITY, 1.0]).is_err());
assert_eq!(
ParameterInterval::new([1.0, 1.0]).unwrap().endpoints(),
[1.0, 1.0]
);
assert_eq!(
DirectedParameterRange::new([2.0, 1.0]).unwrap().endpoints(),
[2.0, 1.0]
);
assert!(DirectedParameterRange::new([1.0, 1.0]).is_err());
assert!(serde_json::from_str::<ParameterInterval>("[2,1]").is_err());
assert!(serde_json::from_str::<DirectedParameterRange>("[1,1]").is_err());
let wire = serde_json::json!({"param_range": [2.0, 1.0]});
let carrier: EdgeCarrier = serde_json::from_value(wire.clone()).unwrap();
assert_eq!(serde_json::to_value(carrier).unwrap(), wire);
let attributed = serde_json::json!({"curve":"test:model:curve#1", "param_range":[2.0,1.0]});
assert!(serde_json::from_value::<EdgeCarrier>(attributed).is_err());
}
#[test]
fn an_admitted_interval_bounds_a_carrier_or_sets_free_endpoints() {
use super::{EdgeCarrier, IncreasingParameterInterval, ParameterInterval};
use crate::scalar::FiniteReal;
let increasing = IncreasingParameterInterval::new([-1.5, 2.0]).expect("increasing");
let interval = ParameterInterval::from(increasing);
assert_eq!(interval.endpoints(), [-1.5, 2.0]);
assert_eq!(
interval.finite_endpoints().map(FiniteReal::get),
interval.endpoints()
);
let mut edge = crate::examples::unit_cube()
.expect("valid unit cube fixture")
.model
.edges
.remove(0);
let curve = edge.curve().cloned().expect("the cube edges have carriers");
edge.set_param_range(Some(interval));
assert_eq!(edge.carrier, EdgeCarrier::Bounded(curve.clone(), interval));
assert_eq!(
Ok(edge.carrier.clone()),
EdgeCarrier::new(Some(curve), Some([-1.5, 2.0]))
);
edge.set_param_range(None);
assert_eq!(edge.param_range(), None);
assert!(edge.curve().is_some());
edge.set_curve(None)
.expect("a carrier without endpoints can drop its curve");
let equal = ParameterInterval::new([3.0, 3.0]).expect("equal endpoints are ordered");
edge.set_param_range(Some(equal));
assert_eq!(
edge.carrier,
EdgeCarrier::Endpoints(equal.finite_endpoints())
);
assert_eq!(
Ok(edge.carrier.clone()),
EdgeCarrier::new(None, Some([3.0, 3.0]))
);
}
#[test]
fn an_edge_hands_out_its_admitted_endpoints_as_a_finite_vector() {
use super::{EdgeCarrier, ParameterInterval};
use crate::scalar::FiniteReal;
let mut edge = crate::examples::unit_cube()
.expect("valid unit cube fixture")
.model
.edges
.remove(0);
let interval = ParameterInterval::new([-1.5, 2.0]).expect("ordered");
edge.set_param_range(Some(interval));
let bounded = edge
.param_range()
.expect("a bounded carrier states its range");
assert_eq!(bounded.get(), [-1.5, 2.0]);
assert_eq!(*bounded, interval.endpoints());
assert_eq!(bounded.into_iter().collect::<Vec<_>>(), [-1.5, 2.0]);
edge.set_curve(None)
.expect("a carrier with ordered endpoints can drop its curve");
let reversed = [
FiniteReal::new(4.0).unwrap(),
FiniteReal::new(-4.0).unwrap(),
];
edge.carrier = EdgeCarrier::Endpoints(reversed);
let free = edge
.param_range()
.expect("free endpoints state their range");
assert_eq!(free.get(), [4.0, -4.0]);
assert_eq!(free.iter().copied().collect::<Vec<_>>(), [4.0, -4.0]);
assert_eq!(
serde_json::to_value(&edge.carrier).unwrap(),
serde_json::json!({"param_range": [4.0, -4.0]})
);
}
#[test]
fn edge_parameter_edits_commit_only_valid_pairs() {
let mut edge = crate::examples::unit_cube()
.expect("valid unit cube fixture")
.model
.edges
.remove(0);
let original = edge.clone();
assert!(super::EdgeCarrier::new(edge.curve().cloned(), Some([2.0, 1.0])).is_err());
assert_eq!(edge, original);
edge.set_curve(None).unwrap();
edge.carrier = super::EdgeCarrier::new(edge.curve().cloned(), Some([2.0, 1.0])).unwrap();
let carrierless = edge.clone();
assert!(edge.set_curve(original.curve().cloned()).is_err());
assert_eq!(edge, carrierless);
}
#[test]
fn shell_admission_requires_one_topology_member_and_preserves_wire_fields() {
let id = super::ShellId::mint("test:model:shell#1").unwrap();
let region = super::RegionId::mint("test:model:region#1").unwrap();
let face = super::FaceId::mint("test:model:face#1").unwrap();
let edge = super::EdgeId::mint("test:model:edge#1").unwrap();
let vertex = super::VertexId::mint("test:model:vertex#1").unwrap();
for mask in 0..8 {
let faces = if mask & 1 != 0 {
vec![face.clone(), face.clone()]
} else {
Vec::new()
};
let edges = if mask & 2 != 0 {
vec![edge.clone()]
} else {
Vec::new()
};
let vertices = if mask & 4 != 0 {
vec![vertex.clone()]
} else {
Vec::new()
};
let admitted = super::Shell::new(
id.clone(),
region.clone(),
faces.clone(),
edges.clone(),
vertices.clone(),
);
assert_eq!(admitted.is_ok(), mask != 0);
let mut members = Vec::new();
for face in &faces {
members.push(serde_json::json!({"kind": "face", "id": face}));
}
for edge in &edges {
members.push(serde_json::json!({"kind": "wire_edge", "id": edge}));
}
for vertex in &vertices {
members.push(serde_json::json!({"kind": "free_vertex", "id": vertex}));
}
let wire = serde_json::json!({"id":id,"region":region,"members":members});
let decoded = serde_json::from_value::<super::Shell>(wire.clone());
assert_eq!(decoded.is_ok(), mask != 0);
if let Ok(shell) = decoded {
assert_eq!(serde_json::to_value(shell).unwrap(), wire);
}
}
}
#[test]
fn shell_admission_reuses_owned_member_storage() {
let faces = vec![super::FaceId::mint("test:model:face#1").unwrap()];
let storage = faces.as_ptr();
let shell = super::Shell::new(
super::ShellId::mint("test:model:shell#1").unwrap(),
super::RegionId::mint("test:model:region#1").unwrap(),
faces,
Vec::new(),
Vec::new(),
)
.unwrap();
assert_eq!(shell.faces().as_ptr(), storage);
}
#[test]
fn shell_topology_edits_admit_the_whole_replacement_and_keep_old_values_on_failure() {
let mut shell = super::Shell::with_face(
super::ShellId::mint("test:model:shell#1").unwrap(),
super::RegionId::mint("test:model:region#1").unwrap(),
super::FaceId::mint("test:model:face#1").unwrap(),
);
let original = shell.clone();
assert!(shell
.edit_topology(|faces, edges, vertices| {
faces.clear();
edges.clear();
vertices.clear();
})
.is_err());
assert_eq!(shell, original);
let vertex = super::VertexId::mint("test:model:vertex#1").unwrap();
shell
.edit_topology(|faces, _, vertices| {
faces.clear();
vertices.push(vertex.clone());
})
.unwrap();
assert!(shell.faces().is_empty());
assert_eq!(shell.free_vertices(), &[vertex]);
}
#[test]
fn shell_face_list_construction_and_replacement_keep_members() {
let shell_id = super::ShellId::mint("test:model:shell#faces").unwrap();
let region_id = super::RegionId::mint("test:model:region#faces").unwrap();
let first = super::FaceId::mint("test:model:face#first").unwrap();
let second = super::FaceId::mint("test:model:face#second").unwrap();
assert!(super::Shell::with_faces(shell_id.clone(), region_id.clone(), Vec::new()).is_err());
let mut shell =
super::Shell::with_faces(shell_id, region_id, vec![first.clone(), second.clone()])
.unwrap();
assert_eq!(shell.faces(), &[first.clone(), second.clone()]);
assert!(shell.replace_faces(Vec::new()).is_err());
assert_eq!(shell.faces(), &[first, second.clone()]);
shell.replace_faces(vec![second.clone()]).unwrap();
assert_eq!(shell.faces(), &[second]);
}
#[test]
fn color_rejects_invalid_components_on_construction_and_wire() {
for invalid in [-1.0, 1.1, f32::NAN, f32::INFINITY, f32::NEG_INFINITY] {
for index in 0..4 {
let mut values = [0.5; 4];
values[index] = invalid;
assert!(super::Color::new(values[0], values[1], values[2], values[3]).is_none());
}
}
for field in ["r", "g", "b", "a"] {
let mut value = serde_json::json!({"r": 0.0, "g": 0.0, "b": 0.0, "a": 1.0});
value[field] = serde_json::json!(-1.0);
let error =
serde_json::from_value::<super::Color>(value).expect_err("invalid component");
assert!(error.to_string().contains(field));
}
let color = super::Color::new(0.0, 0.25, 0.5, 1.0).expect("valid color");
assert_eq!(
serde_json::to_value(color).expect("serialize"),
serde_json::json!({"r": 0.0, "g": 0.25, "b": 0.5, "a": 1.0})
);
assert!(color.with_alpha(-1.0).is_none());
assert_eq!(color.a(), 1.0);
}
use super::{
coedge_ring_neighbors, AnchoredVertexUse, Coedge, CoedgeUseCurve, Face, FaceLoops, Loop,
LoopBoundary, LoopBoundaryRole, LoopId, LoopRing,
};
use crate::features::NonEmptyMembers;
#[test]
fn incremental_loop_ring_keeps_order_and_rejects_duplicates() {
let first = super::CoedgeId::mint("test:model:coedge#0").unwrap();
let second = super::CoedgeId::mint("test:model:coedge#1").unwrap();
let mut ring = LoopRing::single(first.clone());
ring.try_push(second.clone()).unwrap();
assert_eq!(ring.coedges(), &[first.clone(), second]);
let before = ring.clone();
assert!(ring.try_push(first).is_err());
assert_eq!(ring, before);
}
#[test]
fn admitted_loop_ring_refuses_distinct_node_limit_and_keeps_order() {
let first = super::CoedgeId::mint("test:model:coedge#0").unwrap();
let second = super::CoedgeId::mint("test:model:coedge#1").unwrap();
let arena = cadmpeg_core::decode::DecodeArena::new();
let mut policy = cadmpeg_core::decode::DecodePolicy::service();
policy.limits.max_collection_items = 1;
let (ctx, _) =
cadmpeg_core::decode::DecodeContext::from_root_bytes(&[], &arena, &policy).unwrap();
let error = super::LoopRing::new(&ctx, vec![first.clone(), second.clone()], Vec::new())
.unwrap_err();
assert!(matches!(error, resource
if resource.dimension == cadmpeg_core::decode::ResourceDimension::CollectionItems
&& resource.operation == "loop ring members"));
let arena = cadmpeg_core::decode::DecodeArena::new();
let policy = cadmpeg_core::decode::DecodePolicy::service();
let (ctx, _) =
cadmpeg_core::decode::DecodeContext::from_root_bytes(&[], &arena, &policy).unwrap();
let ring = super::LoopRing::new(&ctx, vec![first.clone(), second.clone()], Vec::new())
.unwrap()
.unwrap();
assert_eq!(ring.coedges(), &[first, second]);
}
#[test]
fn admitted_loop_ring_refuses_validation_index_before_allocation() {
use cadmpeg_core::decode::{DecodeArena, DecodeContext, DecodePolicy, ResourceDimension};
let coedge = super::CoedgeId::mint("test:model:coedge#0").unwrap();
let arena = DecodeArena::new();
let mut policy = DecodePolicy::service();
policy.limits.max_collection_items = 0;
let (ctx, _) = DecodeContext::from_root_bytes(&[], &arena, &policy).unwrap();
let refused = LoopRing::new(&ctx, vec![coedge.clone()], Vec::new());
assert!(matches!(refused,
Err(limit)
if limit.dimension == ResourceDimension::CollectionItems
&& limit.operation == "loop ring members"
&& limit.additional == 1
));
let arena = DecodeArena::new();
let (ctx, _) =
DecodeContext::from_root_bytes(&[], &arena, &DecodePolicy::service()).unwrap();
let ring = LoopRing::new(&ctx, vec![coedge.clone()], Vec::new())
.unwrap()
.unwrap();
assert_eq!(ring.coedges(), &[coedge]);
}
#[test]
fn loop_ring_constructor_keeps_original_first_later_and_anchor_refusals() {
use cadmpeg_core::decode::{DecodeArena, DecodeContext, DecodePolicy, ResourceDimension};
let first = super::CoedgeId::mint("test:model:coedge#0").unwrap();
let second = super::CoedgeId::mint("test:model:coedge#1").unwrap();
let member_work = u64::try_from(first.as_str().len()).unwrap() + 1;
for (dimension, cap, operation) in [
(ResourceDimension::MaterializedBytes, 0, "loop ring members"),
(ResourceDimension::CollectionItems, 0, "loop ring members"),
(ResourceDimension::CollectionItems, 1, "loop ring members"),
(ResourceDimension::WorkUnits, 0, "loop ring members"),
(
ResourceDimension::WorkUnits,
member_work,
"loop ring members",
),
(
ResourceDimension::WorkUnits,
member_work * 2,
"loop ring anchors",
),
] {
let arena = DecodeArena::new();
let mut policy = DecodePolicy::service();
match dimension {
ResourceDimension::MaterializedBytes => policy.limits.max_materialized_bytes = cap,
ResourceDimension::CollectionItems => policy.limits.max_collection_items = cap,
ResourceDimension::WorkUnits => policy.limits.max_work_units = cap,
_ => panic!("test dimension"),
}
let (ctx, _) = DecodeContext::from_root_bytes(&[], &arena, &policy).unwrap();
let limit = LoopRing::new(
&ctx,
vec![first.clone(), second.clone()],
vec![AnchoredVertexUse {
vertex: super::VertexId::mint("test:model:vertex#0").unwrap(),
after: first.clone(),
pcurves: vec![],
}],
)
.unwrap_err();
assert_eq!(limit.dimension, dimension);
assert_eq!(limit.operation, operation);
assert!(
matches!(ctx.finish_session(), Err(cadmpeg_core::CodecError::ResourceLimit(original)) if original == limit)
);
}
}
#[test]
fn loop_ring_replacement_keeps_original_refusal_and_is_atomic() {
use cadmpeg_core::decode::{DecodeArena, DecodeContext, DecodePolicy, ResourceDimension};
let first = super::CoedgeId::mint("test:model:coedge#0").unwrap();
let ring = LoopRing::new(
&cadmpeg_test_support::service_decode_context(),
vec![first.clone()],
vec![],
)
.unwrap()
.unwrap();
let mut loop_ = Loop {
id: "test:model:loop#0".try_into().unwrap(),
face: "test:model:face#0".try_into().unwrap(),
boundary: LoopBoundary::Ring(ring),
};
let before = loop_.clone();
let arena = DecodeArena::new();
let mut policy = DecodePolicy::service();
policy.limits.max_work_units = 0;
let (ctx, _) = DecodeContext::from_root_bytes(&[], &arena, &policy).unwrap();
let Err(super::LoopRingAdmissionError::Resource(limit)) =
loop_.replace_ring(&ctx, vec![first], vec![])
else {
panic!("original resource refusal");
};
assert_eq!(limit.dimension, ResourceDimension::WorkUnits);
assert_eq!(limit.operation, "loop ring members");
assert_eq!(loop_, before);
assert!(
matches!(ctx.finish_session(), Err(cadmpeg_core::CodecError::ResourceLimit(original)) if original == limit)
);
}
#[test]
fn loop_ring_constructor_releases_its_borrowed_index_without_retention() {
use cadmpeg_core::decode::{DecodeArena, DecodeContext, DecodePolicy};
let arena = DecodeArena::new();
let mut policy = DecodePolicy::service();
policy.limits.max_retained_bytes = 0;
policy.limits.max_materialized_bytes = 256;
let (ctx, _) = DecodeContext::from_root_bytes(&[], &arena, &policy).unwrap();
let first = super::CoedgeId::mint("test:model:coedge#0").unwrap();
let second = super::CoedgeId::mint("test:model:coedge#1").unwrap();
let vertex_uses = vec![AnchoredVertexUse {
vertex: "test:model:vertex#0".try_into().unwrap(),
after: second.clone(),
pcurves: vec![],
}];
let ring = LoopRing::new(
&ctx,
vec![first.clone(), second.clone()],
vertex_uses.clone(),
)
.unwrap()
.unwrap();
assert_eq!(ring.coedges(), &[first, second]);
assert_eq!(ring.vertex_uses(), vertex_uses);
drop(ctx.reserve_scoped(256, "ring index released").unwrap());
ctx.finish_session().unwrap();
}
#[test]
fn loop_ring_membership_checks_complete_text_and_keeps_source_order() {
let ctx = cadmpeg_test_support::service_decode_context();
let first = super::CoedgeId::mint("test:model:coedge#prefix-long").unwrap();
let second = super::CoedgeId::mint("test:model:coedge#prefix").unwrap();
let third = super::CoedgeId::mint("test:model:coedge#alpha").unwrap();
let vertex_uses = vec![AnchoredVertexUse {
vertex: "test:model:vertex#0".try_into().unwrap(),
after: second.clone(),
pcurves: vec![],
}];
let ring = LoopRing::new(
&ctx,
vec![first.clone(), second.clone(), third.clone()],
vertex_uses.clone(),
)
.unwrap()
.unwrap();
assert_eq!(
ring.coedges(),
&[first.clone(), second.clone(), third.clone()]
);
assert_eq!(ring.vertex_uses(), vertex_uses);
assert_eq!(
LoopRing::new(&ctx, vec![first, second.clone(), third, second], vec![])
.unwrap()
.unwrap_err()
.to_string(),
"loop ring coedges must be distinct"
);
let foreign = super::CoedgeId::mint("test:model:coedge#prefix-longer").unwrap();
let invalid = LoopRing::new(
&ctx,
ring.coedges().to_vec(),
vec![AnchoredVertexUse {
vertex: "test:model:vertex#0".try_into().unwrap(),
after: foreign,
pcurves: vec![],
}],
)
.unwrap()
.unwrap_err();
assert_eq!(
invalid.to_string(),
"loop ring vertex-use after must name a coedge in the ring"
);
ctx.finish_session().unwrap();
}
#[test]
fn loop_ring_lookup_preserves_first_and_later_comparison_refusals() {
use cadmpeg_core::decode::{DecodeArena, DecodeContext, DecodePolicy, ResourceDimension};
let first = super::CoedgeId::mint("test:model:coedge#0").unwrap();
let second = super::CoedgeId::mint("test:model:coedge#1").unwrap();
let members = [&first, &second];
for cap in [0, 1, u64::try_from(first.as_str().len()).unwrap() + 1] {
let arena = DecodeArena::new();
let mut policy = DecodePolicy::service();
policy.limits.max_work_units = cap;
let (ctx, _) = DecodeContext::from_root_bytes(&[], &arena, &policy).unwrap();
let limit =
LoopRing::coedge_position(&ctx, &members, &first, "loop ring comparison test")
.unwrap_err();
assert_eq!(limit.dimension, ResourceDimension::WorkUnits);
assert_eq!(limit.operation, "loop ring comparison test");
assert!(
matches!(ctx.finish_session(), Err(cadmpeg_core::CodecError::ResourceLimit(original)) if original == limit)
);
}
let ctx = cadmpeg_test_support::service_decode_context();
assert_eq!(
LoopRing::coedge_position(&ctx, &members, &first, "loop ring comparison test").unwrap(),
Ok(0)
);
let absent = super::CoedgeId::mint("test:model:coedge#2").unwrap();
assert_eq!(
LoopRing::coedge_position(&ctx, &members, &absent, "loop ring comparison test")
.unwrap(),
Err(2)
);
ctx.finish_session().unwrap();
}
#[test]
fn generated_loop_ring_anchors_each_vertex_to_its_generated_coedge() {
let coedge_namespace = crate::identity_namespace!("test", "model", "coedge");
let vertex_namespace = crate::identity_namespace!("test", "model", "vertex");
let mut vertices =
NonEmptyMembers::one(super::VertexId::compose(&vertex_namespace, 3_usize));
vertices.push(super::VertexId::compose(&vertex_namespace, 8_usize));
let ring = LoopRing::from_vertices(
&coedge_namespace,
&crate::ids::IdentityKey::from(4_usize).colon(2_usize),
vertices,
);
assert_eq!(
ring.coedges()
.iter()
.map(super::CoedgeId::as_str)
.collect::<Vec<_>>(),
["test:model:coedge#4:2:0", "test:model:coedge#4:2:1"]
);
assert!(ring
.vertex_uses()
.iter()
.zip(ring.coedges())
.all(|(vertex_use, coedge)| vertex_use.after == *coedge));
}
#[test]
fn generated_loop_ring_refuses_member_limit() {
use cadmpeg_core::decode::{DecodeArena, DecodeContext, DecodePolicy};
let run = |collection_limit| {
let arena = DecodeArena::new();
let mut policy = DecodePolicy::service();
policy.limits.max_collection_items = collection_limit;
let (ctx, _) =
DecodeContext::from_root_bytes(&[], &arena, &policy).expect("empty root");
let coedge = super::CoedgeId::mint("catia:standard:coedge#0:0:0").expect("identity");
let vertex = super::VertexId::mint("catia:standard:v#1").expect("identity");
LoopRing::new(
&ctx,
vec![coedge.clone()],
vec![super::AnchoredVertexUse {
vertex,
after: coedge,
pcurves: Vec::new(),
}],
)
};
assert!(matches!(run(0), Err(limit)
if limit.operation == "loop ring members"));
let ring = run(u64::MAX)
.expect("service profile admits generated ring")
.expect("valid generated ring");
assert_eq!(ring.coedges()[0].as_str(), "catia:standard:coedge#0:0:0");
}
#[test]
fn loop_ring_rejects_empty_or_duplicate_coedges_and_foreign_anchors() {
assert!(LoopRing::new(
&cadmpeg_test_support::service_decode_context(),
Vec::new(),
Vec::new()
)
.expect("fixture ring admission")
.is_err());
let coedge: super::CoedgeId = "test:model:coedge#0".try_into().unwrap();
let foreign: super::CoedgeId = "test:model:coedge#foreign".try_into().unwrap();
let vertex: super::VertexId = "test:model:vertex#0".try_into().unwrap();
assert!(LoopRing::new(
&cadmpeg_test_support::service_decode_context(),
vec![coedge.clone(), coedge.clone()],
Vec::new()
)
.expect("fixture ring admission")
.is_err());
assert!(LoopRing::new(
&cadmpeg_test_support::service_decode_context(),
vec![coedge],
vec![AnchoredVertexUse {
vertex,
after: foreign,
pcurves: Vec::new(),
}],
)
.expect("fixture ring admission")
.is_err());
}
#[test]
fn replacing_a_ring_rejects_invalid_contents_without_changing_the_loop() {
let coedge: super::CoedgeId = "test:model:coedge#0".try_into().unwrap();
let mut loop_ = Loop {
id: "test:model:loop#0".try_into().unwrap(),
face: "test:model:face#0".try_into().unwrap(),
boundary: LoopBoundary::Ring(
LoopRing::new(
&cadmpeg_test_support::service_decode_context(),
vec![coedge.clone()],
Vec::new(),
)
.expect("fixture ring admission")
.unwrap(),
),
};
let before = loop_.clone();
assert!(loop_
.replace_ring(
&cadmpeg_test_support::service_decode_context(),
Vec::new(),
Vec::new()
)
.is_err());
assert_eq!(loop_, before);
assert!(loop_
.replace_ring(
&cadmpeg_test_support::service_decode_context(),
vec![coedge],
vec![AnchoredVertexUse {
vertex: "test:model:vertex#0".try_into().unwrap(),
after: "test:model:coedge#foreign".try_into().unwrap(),
pcurves: Vec::new(),
}],
)
.is_err());
assert_eq!(loop_, before);
}
fn coedge_json() -> serde_json::Value {
serde_json::json!({
"id": "test:model:coedge#0",
"owner_loop": "test:model:loop#0",
"edge": "test:model:edge#0",
"radial_next": "test:model:coedge#0",
"sense": "forward",
"use_curve": {
"curve": "test:model:curve#0",
"parameter_range": [0.25, 0.75]
}
})
}
#[test]
fn the_coedge_use_curve_is_one_nested_key() {
let coedge: Coedge = serde_json::from_value(coedge_json()).unwrap();
assert_eq!(
coedge.use_curve,
Some(CoedgeUseCurve {
curve: "test:model:curve#0".try_into().expect("valid identity"),
parameter_range: crate::topology::ParameterInterval::new([0.25, 0.75]).unwrap(),
})
);
let loop_ = Loop {
id: coedge.owner_loop.clone(),
face: "test:model:face#0".try_into().expect("valid identity"),
boundary: LoopBoundary::Ring(
super::LoopRing::new(
&cadmpeg_test_support::service_decode_context(),
vec![coedge.id.clone()],
Vec::new(),
)
.expect("fixture ring admission")
.expect("valid loop ring"),
),
};
assert_eq!(
coedge_ring_neighbors(std::slice::from_ref(&loop_), &coedge),
Some((coedge.id.clone(), coedge.id.clone()))
);
let encoded = serde_json::to_value(&coedge).unwrap();
assert_eq!(
encoded["use_curve"],
serde_json::json!({
"curve": "test:model:curve#0",
"parameter_range": [0.25, 0.75]
})
);
assert_eq!(
serde_json::from_value::<Coedge>(encoded).unwrap().use_curve,
coedge.use_curve
);
}
#[test]
fn a_coedge_states_no_ring_neighbor_and_its_loop_order_survives_a_round_trip() {
let model = crate::examples::unit_cube()
.expect("valid unit cube fixture")
.model;
let coedge = &model.coedges[0];
let wire = serde_json::to_value(coedge).unwrap();
assert!(wire.get("next").is_none(), "{wire}");
assert!(wire.get("previous").is_none(), "{wire}");
let mut with_next = wire.clone();
with_next["next"] = serde_json::json!(coedge.id.as_str());
let error = serde_json::from_value::<Coedge>(with_next)
.expect_err("the ring order is the loop's, not the coedge's")
.to_string();
assert!(error.contains("next"), "{error}");
assert_eq!(&serde_json::from_value::<Coedge>(wire).unwrap(), coedge);
let before = coedge_ring_neighbors(&model.loops, coedge).expect("a ring neighbor pair");
let round_tripped: crate::document::Model =
serde_json::from_value(serde_json::to_value(&model).unwrap()).unwrap();
let after = coedge_ring_neighbors(&round_tripped.loops, &round_tripped.coedges[0])
.expect("a ring neighbor pair");
assert_eq!(before, after);
}
#[test]
fn a_half_stated_coedge_use_curve_has_no_encoding() {
let mut without_range = coedge_json();
without_range["use_curve"]
.as_object_mut()
.expect("a use-curve object")
.remove("parameter_range");
assert!(serde_json::from_value::<Coedge>(without_range).is_err());
let mut without_curve = coedge_json();
without_curve["use_curve"]
.as_object_mut()
.expect("a use-curve object")
.remove("curve");
assert!(serde_json::from_value::<Coedge>(without_curve).is_err());
let mut bogus = coedge_json();
bogus["use_curve"]["zz_bogus"] = serde_json::json!(1);
let error = serde_json::from_value::<Coedge>(bogus)
.unwrap_err()
.to_string();
assert!(error.contains("zz_bogus"), "{error}");
let mut absent = coedge_json();
absent
.as_object_mut()
.expect("a coedge object")
.remove("use_curve");
assert_eq!(
serde_json::from_value::<Coedge>(absent).unwrap().use_curve,
None
);
}
#[test]
fn the_vertex_loop_boundary_is_one_nested_tagged_object() {
let json = serde_json::json!({
"id": "test:model:loop#0",
"face": "test:model:face#0",
"boundary": {
"kind": "vertex",
"vertex": "test:model:vertex#0"
}
});
let loop_: Loop = serde_json::from_value(json.clone()).unwrap();
assert!(matches!(
loop_.boundary,
LoopBoundary::Vertex { ref vertex, ref pcurves }
if vertex.as_str() == "test:model:vertex#0" && pcurves.is_empty()
));
assert_eq!(serde_json::to_value(loop_).unwrap(), json);
}
#[test]
fn the_ring_loop_boundary_is_one_nested_tagged_object() {
let json = serde_json::json!({
"id": "test:model:loop#0",
"face": "test:model:face#0",
"boundary": {
"kind": "ring",
"coedges": ["test:model:coedge#0"],
"vertex_uses": [{
"vertex": "test:model:vertex#0",
"after": "test:model:coedge#0"
}]
}
});
let loop_: Loop = serde_json::from_value(json.clone()).unwrap();
assert!(matches!(loop_.boundary, LoopBoundary::Ring(_)));
assert_eq!(serde_json::to_value(loop_).unwrap(), json);
}
#[test]
fn a_loop_boundary_carries_no_key_of_the_other_form() {
let anchored_vertex = serde_json::json!({
"id": "test:model:loop#0",
"face": "test:model:face#0",
"boundary": {
"kind": "vertex",
"vertex": "test:model:vertex#0",
"after": "test:model:coedge#0"
}
});
let error = serde_json::from_value::<Loop>(anchored_vertex)
.unwrap_err()
.to_string();
assert!(error.contains("after"), "{error}");
let ring_without_anchor = serde_json::json!({
"id": "test:model:loop#0",
"face": "test:model:face#0",
"boundary": {
"kind": "ring",
"coedges": ["test:model:coedge#0"],
"vertex_uses": [{ "vertex": "test:model:vertex#0" }]
}
});
assert!(serde_json::from_value::<Loop>(ring_without_anchor).is_err());
let bogus = serde_json::json!({
"id": "test:model:loop#0",
"face": "test:model:face#0",
"boundary": {
"kind": "ring",
"coedges": ["test:model:coedge#0"],
"zz_bogus": 1
}
});
let error = serde_json::from_value::<Loop>(bogus)
.unwrap_err()
.to_string();
assert!(error.contains("zz_bogus"), "{error}");
}
#[test]
fn a_classified_face_states_its_outer_loop_once() {
let mut model = crate::examples::unit_cube()
.expect("valid unit cube fixture")
.model;
let face = &mut model.faces[0];
let mut members = face.loops.iter().cloned();
let outer = members.next().expect("a cube face states a loop");
let inner = members.collect();
face.loops = FaceLoops::classified(outer.clone(), inner);
let wire = serde_json::to_value(&*face).unwrap();
assert_eq!(
wire["loops"]["classification"],
serde_json::json!("classified")
);
assert_eq!(wire["loops"]["outer"], serde_json::json!(outer.as_str()));
assert!(wire["loops"].get("loops").is_none());
let restored: Face = serde_json::from_value(wire).unwrap();
assert_eq!(restored.loop_role(&outer), LoopBoundaryRole::Outer);
let FaceLoops::Classified {
outer: restored_outer,
inner,
} = &restored.loops
else {
panic!("classified loops")
};
assert_eq!(restored_outer, &outer);
for inner in inner {
assert_eq!(restored.loop_role(inner), LoopBoundaryRole::Inner);
}
}
#[test]
fn an_unclassified_face_states_its_loops_in_source_order() {
let model = crate::examples::unit_cube()
.expect("valid unit cube fixture")
.model;
let face = &model.faces[0];
assert!(matches!(face.loops, FaceLoops::Unspecified { .. }));
let wire = serde_json::to_value(face).unwrap();
assert_eq!(
wire["loops"]["classification"],
serde_json::json!("unspecified")
);
assert!(wire["loops"].get("outer").is_none());
assert_eq!(
wire["loops"]["loops"]
.as_array()
.expect("a loops array")
.len(),
face.loops.len()
);
let restored: Face = serde_json::from_value(wire).unwrap();
assert!(matches!(restored.loops, FaceLoops::Unspecified { .. }));
}
#[test]
fn a_face_states_either_a_classification_or_a_loop_order() {
for wire in [
serde_json::json!({"classification": "classified", "outer": "test:model:loop#0", "inner": []}),
serde_json::json!({"classification": "unspecified", "loops": ["test:model:loop#0"]}),
] {
let loops: FaceLoops = serde_json::from_value(wire.clone()).unwrap();
assert_eq!(serde_json::to_value(&loops).unwrap(), wire);
}
assert!(serde_json::from_value::<FaceLoops>(serde_json::json!({
"classification": "classified",
"outer": "test:model:loop#0",
"loops": ["test:model:loop#0"]
}))
.is_err());
let none: FaceLoops =
serde_json::from_value(serde_json::json!({"classification": "unspecified",
"loops": []}))
.unwrap();
assert!(none.is_empty());
}
#[test]
fn an_unclassified_face_states_no_outer_loop() {
let unclassified = FaceLoops::unspecified(vec![
LoopId::mint("test:model:loop#0").expect("identity"),
LoopId::mint("test:model:loop#1").expect("identity"),
]);
assert!(matches!(unclassified, FaceLoops::Unspecified { .. }));
assert_eq!(unclassified.len(), 2);
assert_eq!(
unclassified.role(&LoopId::mint("test:model:loop#0").expect("identity")),
LoopBoundaryRole::Unspecified
);
}
#[test]
fn a_face_with_two_outer_loops_has_no_encoding() {
let wire = serde_json::json!({
"id": "test:model:face#0",
"shell": "test:model:shell#0",
"surface": "test:model:surface#0",
"sense": "forward",
"loops": {
"classification": "classified",
"outer": ["test:model:loop#0", "test:model:loop#1"],
"inner": []
}
});
assert!(serde_json::from_value::<Face>(wire).is_err());
}
#[test]
fn a_face_loop_list_rejects_an_unknown_key_by_name() {
let wire = serde_json::json!({
"id": "test:model:face#0",
"shell": "test:model:shell#0",
"surface": "test:model:surface#0",
"sense": "forward",
"loops": {
"classification": "classified",
"outer": "test:model:loop#0",
"inner": [],
"zz_bogus": 1
}
});
let error = serde_json::from_value::<Face>(wire)
.unwrap_err()
.to_string();
assert!(error.contains("zz_bogus"), "{error}");
}
#[test]
fn a_loop_record_states_no_role_of_its_own() {
let wire = serde_json::json!({
"id": "test:model:loop#0",
"face": "test:model:face#0",
"boundary_role": "outer",
"boundary": { "kind": "vertex", "vertex": "test:model:vertex#0" }
});
let error = serde_json::from_value::<Loop>(wire)
.unwrap_err()
.to_string();
assert!(error.contains("boundary_role"), "{error}");
}
#[test]
fn a_point_position_must_be_finite() {
use super::{Point, PointWire};
use crate::features::FinitePoint3;
use crate::ids::PointId;
use crate::math::Point3;
let id = PointId::mint("t:model:point#0").expect("identity grammar");
for coordinates in [
Point3::new(f64::NAN, 0.0, 0.0),
Point3::new(0.0, f64::INFINITY, 0.0),
Point3::new(0.0, 0.0, f64::NEG_INFINITY),
] {
assert_eq!(
Point::try_from(PointWire {
id: id.clone(),
position: coordinates,
source_object: None,
}),
Err(Point::NON_FINITE_POSITION)
);
}
assert_eq!(
Point::NON_FINITE_POSITION,
"point position coordinates must be finite"
);
let point = Point::new(
id,
FinitePoint3::new(Point3::new(1.0, 2.0, 3.0)).expect("a finite position is a point"),
None,
);
assert_eq!(point.position().get(), Point3::new(1.0, 2.0, 3.0));
}
#[test]
fn a_read_point_position_must_be_finite() {
use super::{Point, PointWire};
use crate::ids::PointId;
use crate::math::Point3;
assert_eq!(
Point::try_from(PointWire {
id: PointId::mint("t:model:point#0").expect("identity grammar"),
position: Point3::new(f64::INFINITY, 0.0, 0.0),
source_object: None,
}),
Err("point position coordinates must be finite")
);
let point = serde_json::from_str::<Point>(
r#"{"id":"t:model:point#0","position":{"x":1.0,"y":2.0,"z":3.0}}"#,
)
.expect("a finite position reads");
assert_eq!(point.position().get(), Point3::new(1.0, 2.0, 3.0));
assert_eq!(
serde_json::to_string(&point).expect("a point writes"),
r#"{"id":"t:model:point#0","position":{"x":1.0,"y":2.0,"z":3.0}}"#
);
assert!(serde_json::from_str::<Point>(
r#"{"id":"t:model:point#0","position":{"x":1.0,"y":2.0,"z":3.0},"extra":1}"#
)
.is_err());
}
#[test]
fn a_point_hands_back_and_takes_its_admitted_position() {
use super::Point;
use crate::features::FinitePoint3;
use crate::ids::PointId;
use crate::math::Point3;
let raw = Point3::new(-0.0, f64::MAX, 5.0e-324);
let mut point = Point::new(
PointId::mint("t:model:point#0").expect("identity grammar"),
FinitePoint3::new(raw).expect("a finite position is a point"),
None,
);
let admitted = point.position();
assert_eq!(
admitted,
FinitePoint3::new(raw).expect("finite coordinates")
);
assert_eq!(
[admitted.x, admitted.y, admitted.z].map(f64::to_bits),
[raw.x, raw.y, raw.z].map(f64::to_bits)
);
let moved = FinitePoint3::new(Point3::new(1.0, -2.0, 3.5)).expect("finite coordinates");
point.set_position(moved);
assert_eq!(point.position(), moved);
assert_eq!(point.position().get(), Point3::new(1.0, -2.0, 3.5));
}
#[test]
fn a_scaled_interval_keeps_its_order_and_refuses_only_overflow() {
use super::ParameterInterval;
use crate::scalar::PositiveReal;
let scale = |value: f64| PositiveReal::new(value).expect("a positive scale");
let low = 1.9_f64;
let high = f64::from_bits(low.to_bits() + 1);
let interval = ParameterInterval::new([low, high]).expect("an ordered interval");
assert_eq!(
interval
.scaled(scale(25.4))
.map(ParameterInterval::endpoints),
Some([48.26, 48.26])
);
let wide = ParameterInterval::new([-f64::MAX, 1.0]).expect("an ordered interval");
assert!(wide.scaled(scale(2.0)).is_none());
let high_end = ParameterInterval::new([0.0, f64::MAX]).expect("an ordered interval");
assert!(high_end.scaled(scale(2.0)).is_none());
}
}
cadmpeg_core::named_optional_field!(deserialize_transform, Transform, "transform");
cadmpeg_core::named_optional_field!(deserialize_name, String, "name");
cadmpeg_core::named_optional_field!(deserialize_color, Color, "color");
cadmpeg_core::named_optional_field!(deserialize_visible, bool, "visible");
cadmpeg_core::named_optional_field!(deserialize_isoparametric, bool, "isoparametric");
cadmpeg_core::named_optional_field!(
deserialize_parameter_range,
crate::geometry::DirectedParameterRange,
"parameter_range"
);
cadmpeg_core::named_optional_field!(deserialize_use_curve, CoedgeUseCurve, "use_curve");
cadmpeg_core::named_optional_field!(deserialize_curve, CurveId, "curve");
cadmpeg_core::named_optional_field!(deserialize_param_range, [f64; 2], "param_range");
cadmpeg_core::named_optional_field!(
deserialize_source_object,
crate::provenance::SourceObjectAssociation,
"source_object"
);
mod identity_rewrite;
mod serialization;