Skip to main content

brep_app/
document_class.rs

1//! The three document classes: a normal part (`.nbrep`), a family seed
2//! (`.fbrep`) and a template (`.tbrep`).
3//!
4//! On the file system the EXTENSION carries the class, matched
5//! case-insensitively and written lowercase. The document carries it too, as
6//! the top-level `documentClass` field (absent on a normal part, so every
7//! existing file stays byte-identical). The two agree by construction: a load
8//! writes the field from the file's extension, and Save As writes the field
9//! the dialog's class choice names before the file gets that extension. The
10//! field is what answers for a document that has no file yet (a new family),
11//! and what the PLM, which keys documents without extensions, will read.
12
13use brep_render::engine_state::EngineState;
14
15/// The document key the class is stored under.
16pub const DOCUMENT_CLASS_KEY: &str = "documentClass";
17
18/// What a document is for.
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
20pub enum DocumentClass {
21    /// A normal part: everything before this slice, imported parts, family
22    /// members and parts spun out of templates.
23    #[default]
24    Normal,
25    /// A family seed: the model plus the table of its members.
26    Family,
27    /// A template: a seed whose marked inputs specialise a new part on insert.
28    Template,
29}
30
31impl DocumentClass {
32    pub const ALL: [DocumentClass; 3] = [Self::Normal, Self::Family, Self::Template];
33
34    /// The file extension, dot included, lowercase.
35    pub fn extension(self) -> &'static str {
36        match self {
37            Self::Normal => ".nbrep",
38            Self::Family => ".fbrep",
39            Self::Template => ".tbrep",
40        }
41    }
42
43    /// The `documentClass` field's value; `None` for a normal part, which
44    /// writes no field.
45    pub fn field(self) -> Option<&'static str> {
46        match self {
47            Self::Normal => None,
48            Self::Family => Some("family"),
49            Self::Template => Some("template"),
50        }
51    }
52
53    /// The word the UI shows.
54    pub fn label(self) -> &'static str {
55        match self {
56            Self::Normal => "Part",
57            Self::Family => "Family",
58            Self::Template => "Template",
59        }
60    }
61
62    /// A stable slug for automation keys and blobs.
63    pub fn slug(self) -> &'static str {
64        match self {
65            Self::Normal => "normal",
66            Self::Family => "family",
67            Self::Template => "template",
68        }
69    }
70
71    /// The class a file name's extension names, if it names one.
72    pub fn of_name(name: &str) -> Option<Self> {
73        let lower = name.to_ascii_lowercase();
74        Self::ALL
75            .into_iter()
76            .find(|class| lower.ends_with(class.extension()))
77    }
78
79    /// The class a `documentClass` field value names. Anything else (absent,
80    /// unknown) is a normal part.
81    pub fn of_field(value: Option<&serde_json::Value>) -> Self {
82        match value.and_then(serde_json::Value::as_str) {
83            Some("family") => Self::Family,
84            Some("template") => Self::Template,
85            _ => Self::Normal,
86        }
87    }
88
89    /// The class a document's JSON says it is.
90    pub fn of_document(document: &serde_json::Value) -> Self {
91        Self::of_field(document.get(DOCUMENT_CLASS_KEY))
92    }
93
94    /// `name` with any class extension replaced by this class's: `bolt` and
95    /// `bolt.nbrep` both become `bolt.fbrep` for a family. A directory part is
96    /// kept as it is.
97    pub fn file_name(self, name: &str) -> String {
98        format!("{}{}", strip_class_extension(name), self.extension())
99    }
100}
101
102/// `name` without a trailing class extension (any case).
103pub fn strip_class_extension(name: &str) -> &str {
104    match DocumentClass::of_name(name) {
105        Some(class) => &name[..name.len() - class.extension().len()],
106        None => name,
107    }
108}
109
110/// The class of the document the engine holds, from its `documentClass`
111/// field. Cheap enough for every frame: one map lookup.
112pub fn document_class(engine: &EngineState) -> DocumentClass {
113    DocumentClass::of_field(engine.history.document_block(DOCUMENT_CLASS_KEY))
114}
115
116/// Whether the engine holds a family seed.
117pub fn is_family(engine: &EngineState) -> bool {
118    document_class(engine) == DocumentClass::Family
119}
120
121/// Whether the engine holds a template.
122pub fn is_template(engine: &EngineState) -> bool {
123    document_class(engine) == DocumentClass::Template
124}
125
126/// Write the class into the engine's document without an undo step: this is
127/// bookkeeping (what the file is), not an edit the user could undo.
128pub fn set_document_class(engine: &mut EngineState, class: DocumentClass) {
129    if document_class(engine) == class
130        && (class != DocumentClass::Normal
131            || engine.history.document_block(DOCUMENT_CLASS_KEY).is_none())
132    {
133        return;
134    }
135    engine.history.set_document_block_no_undo(
136        DOCUMENT_CLASS_KEY,
137        class.field().map(|field| serde_json::Value::String(field.into())),
138    );
139}
140
141/// Set the class field on a document's JSON (`None` removes it).
142pub fn set_class_in_json(document: &mut serde_json::Value, class: DocumentClass) {
143    if let Some(object) = document.as_object_mut() {
144        match class.field() {
145            Some(field) => {
146                object.insert(DOCUMENT_CLASS_KEY.into(), serde_json::Value::String(field.into()));
147            }
148            None => {
149                object.remove(DOCUMENT_CLASS_KEY);
150            }
151        }
152    }
153}
154