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