use cadmpeg_core::text::NonBlankString;
#[cfg(feature = "schema")]
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
crate::ids::id_type!(
PresentationId, compose
);
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct CameraState {
#[serde(default, skip_serializing_if = "Option::is_none")]
#[serde(deserialize_with = "deserialize_position")]
pub position: Option<crate::units::FiniteVector<3>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[serde(deserialize_with = "deserialize_orientation")]
pub orientation: Option<crate::units::NonzeroVector<4>>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
#[serde(deserialize_with = "cadmpeg_core::distinct_keys::btree_map")]
pub properties: BTreeMap<NonBlankString, String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(tag = "type", content = "value")]
#[serde(deny_unknown_fields)]
pub enum PresentationStateKind {
Camera(CameraState),
Native(String),
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct PresentationState {
pub kind: PresentationStateKind,
pub order: u32,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
#[serde(deserialize_with = "cadmpeg_core::distinct_keys::btree_map")]
pub attributes: BTreeMap<NonBlankString, String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub assets: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(
try_from = "PresentationDocumentWire",
into = "PresentationDocumentWire"
)]
pub struct PresentationDocument {
pub id: PresentationId,
pub schema_version: Option<u32>,
pub active_view: Option<String>,
states: Vec<PresentationState>,
pub native_ref: Option<String>,
}
#[derive(Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(deny_unknown_fields)]
struct PresentationDocumentWire {
id: PresentationId,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_schema_version"
)]
schema_version: Option<u32>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_active_view"
)]
active_view: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
states: Vec<PresentationState>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_native_ref"
)]
native_ref: Option<String>,
}
impl PresentationDocument {
#[must_use]
pub fn new(id: PresentationId) -> Self {
Self {
id,
schema_version: None,
active_view: None,
states: Vec::new(),
native_ref: None,
}
}
#[must_use]
pub fn states(&self) -> &[PresentationState] {
&self.states
}
pub fn set_states(&mut self, states: Vec<PresentationState>) -> Result<(), String> {
if states.windows(2).all(|pair| pair[0].order < pair[1].order) {
self.states = states;
return Ok(());
}
let mut orders = std::collections::HashSet::new();
if states.iter().any(|state| !orders.insert(state.order)) {
return Err("states must have distinct order values".into());
}
self.states = states;
Ok(())
}
}
impl From<PresentationDocument> for PresentationDocumentWire {
fn from(document: PresentationDocument) -> Self {
Self {
id: document.id,
schema_version: document.schema_version,
active_view: document.active_view,
states: document.states,
native_ref: document.native_ref,
}
}
}
impl TryFrom<PresentationDocumentWire> for PresentationDocument {
type Error = String;
fn try_from(wire: PresentationDocumentWire) -> Result<Self, Self::Error> {
let mut document = Self {
schema_version: wire.schema_version,
active_view: wire.active_view,
native_ref: wire.native_ref,
..Self::new(wire.id)
};
document.set_states(wire.states)?;
Ok(document)
}
}
#[cfg(feature = "schema")]
impl JsonSchema for PresentationDocument {
fn schema_name() -> std::borrow::Cow<'static, str> {
"PresentationDocument".into()
}
fn schema_id() -> std::borrow::Cow<'static, str> {
concat!(module_path!(), "::PresentationDocument").into()
}
fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
PresentationDocumentWire::json_schema(generator)
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct ViewPresentation {
pub id: PresentationId,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_object"
)]
pub object: Option<String>,
pub order: u32,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_expanded"
)]
pub expanded: Option<bool>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_visible"
)]
pub visible: Option<bool>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_display_mode"
)]
pub display_mode: Option<String>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_selection_style"
)]
pub selection_style: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[serde(deserialize_with = "deserialize_line_width")]
pub line_width: Option<crate::scalar::NonNegativeReal>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[serde(deserialize_with = "deserialize_point_size")]
pub point_size: Option<crate::scalar::NonNegativeReal>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
#[serde(deserialize_with = "cadmpeg_core::distinct_keys::btree_map")]
pub properties: BTreeMap<NonBlankString, String>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_native_ref"
)]
pub native_ref: Option<String>,
}
use crate::ids::{
BodyId, CurveId, EdgeId, FaceId, LayerId, OccurrenceId, PmiId, PointId, ProductDefinitionId,
SurfaceId, VertexId,
};
#[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 PresentationItem {
Body {
body: BodyId,
},
Face {
face: FaceId,
},
Edge {
edge: EdgeId,
},
Vertex {
vertex: VertexId,
},
Point {
point: PointId,
},
Curve {
curve: CurveId,
},
Surface {
surface: SurfaceId,
},
Product {
product: ProductDefinitionId,
},
Occurrence {
occurrence: OccurrenceId,
},
Pmi {
annotation: PmiId,
},
Tessellation {
tessellation: String,
},
Source {
#[serde(deserialize_with = "deserialize_source_id")]
source_id: cadmpeg_core::text::NonBlankString,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct PresentationLayer {
pub id: LayerId,
pub name: String,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_description"
)]
pub description: Option<String>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_visible"
)]
pub visible: Option<bool>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub items: Vec<PresentationItem>,
}
cadmpeg_core::named_optional_field!(
deserialize_position,
crate::units::FiniteVector<3>,
"position"
);
cadmpeg_core::named_optional_field!(
deserialize_orientation,
crate::units::NonzeroVector<4>,
"orientation"
);
cadmpeg_core::named_optional_field!(
deserialize_line_width,
crate::scalar::NonNegativeReal,
"line_width"
);
cadmpeg_core::named_optional_field!(
deserialize_point_size,
crate::scalar::NonNegativeReal,
"point_size"
);
crate::units::named_field!(
deserialize_source_id,
cadmpeg_core::text::NonBlankString,
"source_id"
);
#[cfg(test)]
mod tests {
#[test]
fn a_named_optional_key_states_a_value_or_is_left_out() {
use super::ViewPresentation;
let refusal = serde_json::from_value::<ViewPresentation>(serde_json::json!({
"id": "freecad:presentation:view#1",
"order": 0,
"line_width": null
}))
.expect_err("null is not a spelling of an absent line width");
let text = refusal.to_string();
assert!(
text.contains("line_width")
&& text.contains("this key states a value or is left out; it does not state null"),
"{text}"
);
let absent = serde_json::from_value::<ViewPresentation>(serde_json::json!({
"id": "freecad:presentation:view#1",
"order": 0
}))
.expect("an absent key reaches the field default");
assert!(absent.line_width.is_none());
}
#[test]
fn camera_kinds_and_states_round_trip_without_payload_or_tag_loss() {
let kinds = [
PresentationStateKind::Camera(CameraState {
position: Some(
crate::units::FiniteVector::new([1.0, 2.0, 3.0]).expect("finite position"),
),
orientation: None,
properties: BTreeMap::new(),
}),
PresentationStateKind::Camera(CameraState {
position: Some(
crate::units::FiniteVector::new([4.0, 5.0, 6.0]).expect("finite position"),
),
orientation: None,
properties: BTreeMap::new(),
}),
PresentationStateKind::Native("Camera".to_owned()),
];
let mut states = Vec::new();
for (order, kind) in kinds.into_iter().enumerate() {
let json = serde_json::to_string(&kind).unwrap();
assert_eq!(
serde_json::from_str::<PresentationStateKind>(&json).unwrap(),
kind
);
let state = PresentationState {
kind,
order: u32::try_from(order).expect("test order fits u32"),
attributes: BTreeMap::new(),
assets: Vec::new(),
};
let json = serde_json::to_string(&state).unwrap();
assert_eq!(
serde_json::from_str::<PresentationState>(&json).unwrap(),
state
);
states.push(state);
}
let mut document = PresentationDocument::new(
PresentationId::mint("synthetic:test:presentation#presentation").unwrap(),
);
document.set_states(states).expect("distinct orders");
let json = serde_json::to_string(&document).unwrap();
assert_eq!(
serde_json::from_str::<PresentationDocument>(&json).unwrap(),
document
);
}
use std::collections::BTreeMap;
use super::{
CameraState, PresentationDocument, PresentationId, PresentationItem, PresentationLayer,
PresentationState, PresentationStateKind,
};
use crate::document::CadIr;
use crate::ids::{FaceId, LayerId};
use crate::report::check::Check;
use crate::validate::validate_neutral;
#[test]
fn source_layer_items_validate_without_fabricated_geometry() {
let mut ir = CadIr::empty();
ir.model.presentation_layers.push(PresentationLayer {
id: LayerId::mint("test:presentation:layer#construction").expect("valid identity"),
name: "construction".into(),
description: None,
visible: None,
items: vec![PresentationItem::Source {
source_id: cadmpeg_core::nonblank_literal!("#{}", 42),
}],
});
assert!(validate_neutral(&ir, Vec::new())
.expect("validate the neutral model")
.is_ok());
}
#[test]
fn empty_layer_name_is_valid() {
let mut ir = CadIr::empty();
ir.model.presentation_layers.push(PresentationLayer {
id: LayerId::mint("test:presentation:layer#unnamed").expect("valid identity"),
name: String::new(),
description: None,
visible: None,
items: vec![PresentationItem::Source {
source_id: cadmpeg_core::nonblank_literal!("#{}", 42),
}],
});
assert!(validate_neutral(&ir, Vec::new())
.expect("validate the neutral model")
.is_ok());
}
#[test]
fn missing_typed_layer_item_is_invalid() {
let mut ir = CadIr::empty();
ir.model.presentation_layers.push(PresentationLayer {
id: LayerId::mint("test:presentation:layer#missing").expect("valid identity"),
name: "missing".into(),
description: None,
visible: None,
items: vec![PresentationItem::Face {
face: FaceId::mint("test:model:face#missing").expect("valid identity"),
}],
});
assert!(validate_neutral(&ir, Vec::new())
.expect("validate the neutral model")
.findings
.iter()
.any(|finding| finding.check == Check::Presentation));
}
#[test]
fn source_identity_admission_preserves_nonempty_wire() {
let wire = serde_json::json!({"kind": "source", "source_id": "#42"});
let value: PresentationItem =
serde_json::from_value(wire.clone()).expect("nonempty source_id");
assert_eq!(serde_json::to_value(value).expect("serialize"), wire);
let error = serde_json::from_value::<PresentationItem>(
serde_json::json!({"kind": "source", "source_id": ""}),
)
.expect_err("empty source_id");
assert!(error.to_string().contains("source_id"));
assert!(cadmpeg_core::text::NonBlankString::new("").is_none());
}
#[test]
fn document_state_orders_are_checked_without_reordering_or_partial_updates() {
let state = |order| PresentationState {
kind: PresentationStateKind::Native("View".into()),
order,
attributes: BTreeMap::new(),
assets: Vec::new(),
};
let mut document = PresentationDocument::new(
PresentationId::mint("synthetic:test:presentation#presentation")
.expect("valid identity"),
);
assert!(document.states().is_empty());
let states = vec![state(9), state(2)];
document
.set_states(states.clone())
.expect("distinct orders");
assert_eq!(document.states(), states);
let wire = serde_json::to_value(&document).expect("serialize");
assert_eq!(
serde_json::from_value::<PresentationDocument>(wire.clone()).expect("valid wire"),
document
);
assert!(document.set_states(vec![state(2), state(2)]).is_err());
assert_eq!(document.states(), states);
let mut invalid = wire;
invalid["states"] =
serde_json::to_value(vec![state(2), state(2)]).expect("serialize states");
let error = serde_json::from_value::<PresentationDocument>(invalid)
.expect_err("duplicate state order");
assert!(error.to_string().contains("states"));
document.set_states(Vec::new()).expect("empty states");
assert!(document.states().is_empty());
}
}
cadmpeg_core::named_optional_field!(deserialize_schema_version, u32, "schema_version");
cadmpeg_core::named_optional_field!(deserialize_active_view, String, "active_view");
cadmpeg_core::named_optional_field!(deserialize_native_ref, String, "native_ref");
cadmpeg_core::named_optional_field!(deserialize_object, String, "object");
cadmpeg_core::named_optional_field!(deserialize_expanded, bool, "expanded");
cadmpeg_core::named_optional_field!(deserialize_visible, bool, "visible");
cadmpeg_core::named_optional_field!(deserialize_display_mode, String, "display_mode");
cadmpeg_core::named_optional_field!(deserialize_selection_style, String, "selection_style");
cadmpeg_core::named_optional_field!(deserialize_description, String, "description");
mod identity_rewrite;