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 = brep_render::brep_kernel::family::DOCUMENT_CLASS_KEY;
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    /// Manufacturing instructions: an assembly revision placed as one grounded
30    /// component, with the steps in the `instructions` block. Not one of the
31    /// part classes a Save As converts between ([`Self::ALL`]): instructions
32    /// are made from an assembly, never from a part.
33    Instructions,
34}
35
36impl DocumentClass {
37    /// The part classes Save As and New offer.
38    pub const ALL: [DocumentClass; 3] = [Self::Normal, Self::Family, Self::Template];
39    /// Every class a file or a field can name.
40    pub const KNOWN: [DocumentClass; 4] = [Self::Normal, Self::Family, Self::Template, Self::Instructions];
41
42    /// The file extension, dot included, lowercase.
43    pub fn extension(self) -> &'static str {
44        match self {
45            Self::Normal => ".nbrep",
46            Self::Family => ".fbrep",
47            Self::Template => ".tbrep",
48            Self::Instructions => ".ibrep",
49        }
50    }
51
52    /// The `documentClass` field's value; `None` for a normal part, which
53    /// writes no field.
54    pub fn field(self) -> Option<&'static str> {
55        match self {
56            Self::Normal => None,
57            Self::Family => Some("family"),
58            Self::Template => Some("template"),
59            Self::Instructions => Some("instructions"),
60        }
61    }
62
63    /// The word the UI shows.
64    pub fn label(self) -> &'static str {
65        match self {
66            Self::Normal => "Part",
67            Self::Family => "Family",
68            Self::Template => "Template",
69            Self::Instructions => "Instructions",
70        }
71    }
72
73    /// A stable slug for automation keys and blobs.
74    pub fn slug(self) -> &'static str {
75        match self {
76            Self::Normal => "normal",
77            Self::Family => "family",
78            Self::Template => "template",
79            Self::Instructions => "instructions",
80        }
81    }
82
83    /// The class a file name's extension names, if it names one.
84    pub fn of_name(name: &str) -> Option<Self> {
85        let lower = name.to_ascii_lowercase();
86        Self::KNOWN
87            .into_iter()
88            .find(|class| lower.ends_with(class.extension()))
89    }
90
91    /// The class a `documentClass` field value names. Anything else (absent,
92    /// unknown) is a normal part.
93    pub fn of_field(value: Option<&serde_json::Value>) -> Self {
94        match value.and_then(serde_json::Value::as_str) {
95            Some("family") => Self::Family,
96            Some("template") => Self::Template,
97            Some("instructions") => Self::Instructions,
98            _ => Self::Normal,
99        }
100    }
101
102    /// The class a document's JSON says it is.
103    pub fn of_document(document: &serde_json::Value) -> Self {
104        Self::of_field(document.get(DOCUMENT_CLASS_KEY))
105    }
106
107    /// `name` with any class extension replaced by this class's: `bolt` and
108    /// `bolt.nbrep` both become `bolt.fbrep` for a family. A directory part is
109    /// kept as it is.
110    pub fn file_name(self, name: &str) -> String {
111        format!("{}{}", strip_class_extension(name), self.extension())
112    }
113}
114
115/// `name` without a trailing class extension (any case).
116pub fn strip_class_extension(name: &str) -> &str {
117    match DocumentClass::of_name(name) {
118        Some(class) => &name[..name.len() - class.extension().len()],
119        None => name,
120    }
121}
122
123/// The class of the document the engine holds, from its `documentClass`
124/// field. Cheap enough for every frame: one map lookup.
125pub fn document_class(engine: &EngineState) -> DocumentClass {
126    DocumentClass::of_field(engine.history.document_block(DOCUMENT_CLASS_KEY))
127}
128
129/// Whether the engine holds a family seed.
130pub fn is_family(engine: &EngineState) -> bool {
131    document_class(engine) == DocumentClass::Family
132}
133
134/// Whether the engine holds a template.
135pub fn is_template(engine: &EngineState) -> bool {
136    document_class(engine) == DocumentClass::Template
137}
138
139/// Write the class into the engine's document without an undo step: this is
140/// bookkeeping (what the file is), not an edit the user could undo.
141pub fn set_document_class(engine: &mut EngineState, class: DocumentClass) {
142    if document_class(engine) == class
143        && (class != DocumentClass::Normal
144            || engine.history.document_block(DOCUMENT_CLASS_KEY).is_none())
145    {
146        return;
147    }
148    engine.history.set_document_block_no_undo(
149        DOCUMENT_CLASS_KEY,
150        class.field().map(|field| serde_json::Value::String(field.into())),
151    );
152}
153
154/// Set the class field on a document's JSON (`None` removes it).
155pub fn set_class_in_json(document: &mut serde_json::Value, class: DocumentClass) {
156    if let Some(object) = document.as_object_mut() {
157        match class.field() {
158            Some(field) => {
159                object.insert(DOCUMENT_CLASS_KEY.into(), serde_json::Value::String(field.into()));
160            }
161            None => {
162                object.remove(DOCUMENT_CLASS_KEY);
163            }
164        }
165    }
166}
167