use crate::math::portable::Portable;
use cadmpeg_core::text::NonBlankString;
#[cfg(feature = "schema")]
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
crate::ids::id_type!(
DrawingId, compose
);
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "snake_case")]
#[serde(deny_unknown_fields)]
pub enum DrawingKind {
Page,
Template,
View,
Projection,
Section,
Detail,
Dimension,
Annotation,
Balloon,
Symbol,
Image,
Leader,
Other,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct Drawing {
pub id: DrawingId,
pub object: String,
pub kind: DrawingKind,
pub runtime_type: String,
pub order: u32,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_visible"
)]
pub visible: Option<bool>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
#[serde(deserialize_with = "cadmpeg_core::distinct_keys::btree_map")]
pub relationships: BTreeMap<NonBlankString, Vec<crate::references::ReferenceSelection>>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_template"
)]
pub template: Option<DrawingId>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[serde(deserialize_with = "deserialize_position")]
pub position: Option<crate::units::FiniteVector<2>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[serde(deserialize_with = "deserialize_scale")]
pub scale: Option<crate::scalar::PositiveReal>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[serde(deserialize_with = "deserialize_direction")]
pub direction: Option<crate::units::NonzeroVector<3>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[serde(deserialize_with = "deserialize_rotation_degrees")]
pub rotation_degrees: Option<crate::scalar::FiniteReal>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_sheet"
)]
pub sheet: Option<DrawingSheet>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub curves: Vec<DrawingCurve>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_template_geometry"
)]
pub template_geometry: Option<TemplateGeometry>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_x_direction"
)]
pub x_direction: Option<crate::units::NonzeroVector<3>>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
#[serde(deserialize_with = "cadmpeg_core::distinct_keys::btree_map")]
pub parameters: BTreeMap<NonBlankString, String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub assets: Vec<String>,
pub native_ref: String,
}
impl Drawing {
#[must_use]
pub fn new(
id: DrawingId,
object: String,
kind: DrawingKind,
runtime_type: String,
order: u32,
native_ref: String,
) -> Self {
Self {
id,
object,
kind,
runtime_type,
order,
native_ref,
visible: None,
relationships: BTreeMap::new(),
template: None,
position: None,
scale: None,
direction: None,
rotation_degrees: None,
template_geometry: None,
x_direction: None,
sheet: None,
curves: Vec::new(),
parameters: BTreeMap::new(),
assets: Vec::new(),
}
}
}
cadmpeg_core::named_optional_field!(
deserialize_position,
crate::units::FiniteVector<2>,
"position"
);
cadmpeg_core::named_optional_field!(deserialize_scale, crate::scalar::PositiveReal, "scale");
cadmpeg_core::named_optional_field!(
deserialize_direction,
crate::units::NonzeroVector<3>,
"direction"
);
cadmpeg_core::named_optional_field!(
deserialize_rotation_degrees,
crate::scalar::FiniteReal,
"rotation_degrees"
);
#[cfg(test)]
mod tests {
use super::{Drawing, DrawingId};
#[test]
fn sheet_and_curves_wire_admission() {
let wire = serde_json::json!({"id": "synthetic:test:drawing#view", "object": "source", "kind": "view", "runtime_type": "View", "order": 0, "native_ref": "native",
"sheet": {"width_mm": 210.0, "height_mm": 297.0, "fields": [{"name": "Title", "value": "Test"}]},
"curves": [
{"geometry": {"kind": "line", "start": [0.0, 1.0], "end": [2.0, 3.0]}, "visibility": "hidden", "source": "projected_model_edge"},
{"geometry": {"kind": "circle", "center": [0.0, 0.0], "radius": 2.0}, "visibility": "visible", "source": "cosmetic"},
{"geometry": {"kind": "arc", "center": [0.0, 0.0], "radius": 2.0, "start": [2.0, 0.0], "middle": [0.0, 2.0], "end": [-2.0, 0.0]}, "visibility": "visible", "source": "center_line"}
]});
let drawing: Drawing = serde_json::from_value(wire.clone()).unwrap();
assert!(drawing.sheet.as_ref().unwrap().is_valid());
assert!(drawing.curves.iter().all(super::DrawingCurve::is_valid));
assert_eq!(serde_json::to_value(drawing).unwrap(), wire);
let mut rejected = wire.clone();
rejected["sheet"]["width_mm"] = serde_json::json!(0);
assert!(serde_json::from_value::<Drawing>(rejected)
.unwrap_err()
.to_string()
.contains("sheet"));
let mut rejected = wire.clone();
rejected["curves"][1]["geometry"]["radius"] = serde_json::json!(-1);
assert!(serde_json::from_value::<Drawing>(rejected).is_err());
let mut rejected = wire.clone();
rejected["curves"][0]["geometry"]["start"] = serde_json::json!([0, null]);
assert!(serde_json::from_value::<Drawing>(rejected).is_err());
let mut rejected = wire;
rejected["curves"][0]
.as_object_mut()
.unwrap()
.remove("visibility");
assert!(serde_json::from_value::<Drawing>(rejected)
.unwrap_err()
.to_string()
.contains("visibility"));
}
#[test]
fn template_reference_preserves_string_wire_and_rejects_invalid_ids() {
let value = serde_json::json!({"id": "synthetic:test:drawing#page", "object": "source", "kind": "page", "runtime_type": "Page", "order": 0, "template": "synthetic:test:drawing#template", "native_ref": "native"});
let drawing: Drawing = serde_json::from_value(value.clone()).expect("valid drawing");
assert_eq!(
drawing.template.as_ref().map(DrawingId::as_str),
Some("synthetic:test:drawing#template")
);
assert_eq!(
serde_json::to_value(drawing).expect("serialize drawing"),
value
);
for invalid in ["", "bad id"] {
let mut invalid_value = value.clone();
invalid_value["template"] = serde_json::json!(invalid);
assert!(serde_json::from_value::<Drawing>(invalid_value).is_err());
}
}
#[test]
fn numeric_admission_names_fields_and_preserves_valid_values() {
let wire = serde_json::json!({"id": "synthetic:test:drawing#view", "object": "source", "kind": "view", "runtime_type": "View", "order": 0, "native_ref": "native", "position": [-2.0, 0.0], "scale": 0.5, "direction": [0.0, 0.0, 2.0], "rotation_degrees": -90.0});
let drawing: Drawing = serde_json::from_value(wire.clone()).expect("valid drawing");
assert_eq!(serde_json::to_value(drawing).expect("serialize"), wire);
for (field, invalid) in [
("scale", serde_json::json!(0)),
("direction", serde_json::json!([0, 0, 0])),
("position", serde_json::json!([0, null])),
] {
let mut rejected = wire.clone();
rejected[field] = invalid;
let error =
serde_json::from_value::<Drawing>(rejected).expect_err("invalid numeric field");
assert!(error.to_string().contains(field));
}
}
#[test]
fn a_free_form_map_refuses_a_blank_key() {
let wire = serde_json::json!({
"id": "synthetic:test:drawing#view",
"object": "source",
"kind": "view",
"runtime_type": "View",
"order": 0,
"native_ref": "native",
"parameters": {" ": "value"},
});
let error = serde_json::from_value::<Drawing>(wire).expect_err("blank parameter role");
assert!(error.to_string().contains("must not be blank"), "{error}");
let relationships = serde_json::json!({
"id": "synthetic:test:drawing#view",
"object": "source",
"kind": "view",
"runtime_type": "View",
"order": 0,
"native_ref": "native",
"relationships": {"": []},
});
let error =
serde_json::from_value::<Drawing>(relationships).expect_err("blank relationship role");
assert!(error.to_string().contains("must not be blank"), "{error}");
let named = serde_json::json!({
"id": "synthetic:test:drawing#view",
"object": "source",
"kind": "view",
"runtime_type": "View",
"order": 0,
"native_ref": "native",
"parameters": {" scale ": "value"},
});
let drawing: Drawing =
serde_json::from_value(named).expect("a named role is kept verbatim");
assert!(drawing.parameters.contains_key(" scale "));
}
}
cadmpeg_core::named_optional_field!(deserialize_visible, bool, "visible");
cadmpeg_core::named_optional_field!(deserialize_template, DrawingId, "template");
mod identity_rewrite;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct DrawingSheet {
pub width_mm: crate::scalar::PositiveReal,
pub height_mm: crate::scalar::PositiveReal,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub fields: Vec<TitleBlockField>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct TitleBlockField {
pub name: NonBlankString,
pub value: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "snake_case", deny_unknown_fields)]
pub enum CurveVisibility {
Visible,
Hidden,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "snake_case", deny_unknown_fields)]
pub enum CurveSource {
ProjectedModelEdge,
Cosmetic,
CenterLine,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct DrawingCurve {
pub geometry: DrawingCurveGeometry,
pub visibility: CurveVisibility,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_displayed"
)]
pub displayed: Option<bool>,
pub source: CurveSource,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
pub enum DrawingCurveGeometry {
Line {
start: crate::units::FiniteVector<2>,
end: crate::units::FiniteVector<2>,
},
Circle {
center: crate::units::FiniteVector<2>,
radius: crate::scalar::PositiveReal,
},
Arc {
center: crate::units::FiniteVector<2>,
radius: crate::scalar::PositiveReal,
start: crate::units::FiniteVector<2>,
middle: crate::units::FiniteVector<2>,
end: crate::units::FiniteVector<2>,
},
}
impl DrawingSheet {
#[must_use]
pub fn is_valid(&self) -> bool {
let mut names = std::collections::BTreeSet::new();
self.width_mm.get().is_finite()
&& self.width_mm.get() > 0.0
&& self.height_mm.get().is_finite()
&& self.height_mm.get() > 0.0
&& self
.fields
.iter()
.all(|field| names.insert(field.name.as_str()))
}
}
const CURVE_RADIAL_RELATIVE_TOLERANCE: f64 = 1e-9;
impl DrawingCurve {
#[must_use]
pub fn is_valid(&self) -> bool {
self.geometry.is_valid()
}
}
impl DrawingCurveGeometry {
#[must_use]
pub fn is_valid(&self) -> bool {
let finite =
|point: crate::units::FiniteVector<2>| point.get().iter().all(|v| v.is_finite());
let positive =
|radius: crate::scalar::PositiveReal| radius.get().is_finite() && radius.get() > 0.0;
match *self {
DrawingCurveGeometry::Line { start, end } => finite(start) && finite(end),
DrawingCurveGeometry::Circle { center, radius } => finite(center) && positive(radius),
DrawingCurveGeometry::Arc {
center,
radius,
start,
middle,
end,
} => {
finite(center)
&& positive(radius)
&& finite(start)
&& finite(middle)
&& finite(end)
&& start != middle
&& middle != end
&& start != end
&& [start, middle, end].iter().all(|point| {
let [x, y] = point.get();
let [cx, cy] = center.get();
((x - cx).portable_hypot(y - cy) - radius.get()).abs()
<= CURVE_RADIAL_RELATIVE_TOLERANCE * radius.get()
})
}
}
}
}
cadmpeg_core::named_optional_field!(deserialize_sheet, DrawingSheet, "sheet");
cadmpeg_core::named_optional_field!(deserialize_displayed, bool, "displayed");
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct TemplateGeometry {
pub curves: Vec<DrawingCurveGeometry>,
pub texts: Vec<TemplateText>,
}
impl TemplateGeometry {
pub fn is_valid(&self) -> bool {
self.curves.iter().all(DrawingCurveGeometry::is_valid)
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct TemplateText {
pub text: String,
pub position: crate::units::FiniteVector<2>,
pub height: crate::scalar::PositiveLength,
}
cadmpeg_core::named_optional_field!(
deserialize_template_geometry,
TemplateGeometry,
"template_geometry"
);
cadmpeg_core::named_optional_field!(
deserialize_x_direction,
crate::units::NonzeroVector<3>,
"x_direction"
);
impl Drawing {
#[must_use]
pub fn physical_content_is_valid(&self) -> bool {
(self.template_geometry.is_none() || self.kind == DrawingKind::Template)
&& self
.template_geometry
.as_ref()
.is_none_or(TemplateGeometry::is_valid)
&& (self.x_direction.is_none()
|| matches!(
self.kind,
DrawingKind::View
| DrawingKind::Projection
| DrawingKind::Section
| DrawingKind::Detail
))
}
}