brep_app/panels/file/
component_part.rs1use super::*;
4use crate::panels::component_actions::part_source_key;
5use crate::store::{read_now, ReadNow};
6
7pub(super) struct Recovery {
8 assembly_id: u64,
9 component_id: String,
10 source: Option<String>,
11 destination: Option<String>,
12}
13
14impl FileDialog {
15 pub fn edit_component_part(
16 &mut self,
17 docs: &mut Documents,
18 store: &dyn ModelStore,
19 component_id: &str,
20 ) {
21 let source = part_source_key(docs.engine(), component_id);
22 if let Some(key) = &source {
23 match read_now(store, key) {
24 ReadNow::Ready(_) => {
25 self.open_document(docs, store, key);
26 if self.status.starts_with("open failed:") {
27 docs.engine_mut().push_notice(self.status.clone());
28 }
29 return;
30 }
31 ReadNow::Loading => {
32 docs.engine_mut().push_notice(format!("This part's source document '{key}' is still loading — Edit Part again in a moment"));
33 return;
34 }
35 ReadNow::Absent => {}
36 }
37 }
38 let Some(info) = docs.engine().component_info(component_id) else {
39 docs.engine_mut()
40 .push_notice(format!("'{component_id}' is not an assembly component"));
41 return;
42 };
43 let destination = source.clone().filter(|key| {
44 let file = crate::store::file_name_of(key);
45 !key.starts_with('@')
46 && key.trim() == key
47 && !file.is_empty()
48 && file != "."
49 && file != ".."
50 });
51 self.name_buf = destination
52 .as_deref()
53 .map(model_display_name)
54 .unwrap_or(info.part_name);
55 self.status.clear();
56 self.use_folder(Purpose::Models, store);
57 self.component_part = Some(Recovery {
58 assembly_id: docs.active_id(),
59 component_id: component_id.into(),
60 source,
61 destination,
62 });
63 self.open_modal(Mode::CreateComponentPart);
64 }
65
66 fn cancel_component_part(&mut self) {
67 self.component_part = None;
68 self.open = false;
69 }
70
71 pub(super) fn show_create_component_part(
72 &mut self,
73 ctx: &egui::Context,
74 docs: &mut Documents,
75 store: &dyn ModelStore,
76 ) {
77 let mut create = false;
78 let mut cancel = false;
79 let source = self
80 .component_part
81 .as_ref()
82 .and_then(|p| p.destination.clone());
83 let modal = egui::Modal::new(egui::Id::new("brep-create-component-part")).show(ctx, |ui| {
84 ui.heading("Create part file?");
85 ui.label("The component has no available source file. Save its embedded part definition and open it in its own tab.");
86 if let Some(key) = source {
87 ui.label(format!("Source: {key}"));
88 } else {
89 ui.label(format!("Folder: {}", store.browser_location()));
90 ui.label("File name");
91 let field = ui.add(egui::TextEdit::singleline(&mut self.name_buf).hint_text("part name"));
92 self.hit("field:name", &field);
93 if self.want_focus {
94 field.request_focus();
95 self.want_focus = false;
96 }
97 }
98 ui.horizontal(|ui| {
99 let button = ui.button("Create part file");
100 self.hit("createpart", &button);
101 create = button.clicked();
102 let button = ui.button("Cancel");
103 self.hit("cancel", &button);
104 cancel = button.clicked();
105 });
106 if !self.status.is_empty() { ui.colored_label(ui.visuals().error_fg_color, &self.status); }
107 });
108 if cancel || modal.should_close() {
109 self.cancel_component_part();
110 } else if create {
111 if let Err(error) = self.create_component_part(docs, store) {
112 self.status = format!("Create part failed: {error}");
113 }
114 }
115 }
116
117 fn create_component_part(
118 &mut self,
119 docs: &mut Documents,
120 store: &dyn ModelStore,
121 ) -> Result<(), String> {
122 let pending = self
123 .component_part
124 .as_ref()
125 .ok_or("no component selected")?;
126 let index = docs
127 .iter()
128 .position(|doc| doc.id() == pending.assembly_id)
129 .ok_or("the assembly tab was closed")?;
130 let assembly = docs.get(index).unwrap();
131 let info = assembly
132 .engine
133 .component_info(&pending.component_id)
134 .ok_or("the component was removed")?;
135 if part_source_key(&assembly.engine, &pending.component_id) != pending.source {
136 return Err("the component source changed; cancel and Edit Part again".into());
137 }
138 if store.plm_client().is_some()
141 || pending
142 .source
143 .as_deref()
144 .is_some_and(|key| crate::plm::uses::revision_key_in(key).is_some())
145 {
146 return Err("this storage cannot create a missing PLM revision here; create or restore the part through PLM, then retry Edit Part".into());
147 }
148 let identity = match &pending.destination {
149 Some(key) => {
150 if key.starts_with('@') || key.trim().is_empty() {
151 return Err("invalid source file identity".into());
152 }
153 key.clone()
154 }
155 None => {
156 let name = self.name_buf.trim();
157 if name.is_empty()
158 || name.starts_with('@')
159 || name.contains(['/', '\\'])
160 || name == "."
161 || name == ".."
162 {
163 return Err("enter a valid file name without folders".into());
164 }
165 let file = crate::store::model_file_name(name);
166 let folder = store.browser_location();
167 if std::path::Path::new(&folder).is_absolute() {
170 sibling_identity(&format!("{}/_", folder.trim_end_matches('/')), &file)
171 } else {
172 store.canonical_identity(&file)
173 }
174 }
175 };
176 if docs.iter().any(|doc| {
177 doc.name().is_some_and(|name| {
178 store.canonical_identity(name) == store.canonical_identity(&identity)
179 })
180 }) {
181 return Err("that part already has an open tab; cancel and open that tab".into());
182 }
183 ensure_absent(store, &identity)?;
184 let mut document: serde_json::Value =
185 serde_json::from_str(&assembly.engine.history_request_json())
186 .map_err(|e| e.to_string())?;
187 let entry = &mut document["partsLibrary"][&info.part_name];
188 let part = entry
189 .get("document")
190 .filter(|part| {
191 part.is_object()
192 && part
193 .get("features")
194 .and_then(|v| v.as_array())
195 .is_some_and(|features| !features.is_empty())
196 })
197 .ok_or(
198 "the component has no recoverable embedded part history; no blank file was created",
199 )?;
200 let contents = part.to_string();
201 let mut engine = docs.spawn_engine();
204 engine
205 .load_model_and_fit(&contents)
206 .map_err(|e| format!("embedded part cannot be opened: {e}"))?;
207 let mut standalone = Document::new(engine);
208 standalone.set_name(Some(identity.clone()));
209 let signature = document_signature(&contents);
210 let reconnect = entry["sourceKey"].as_str() != Some(&identity)
211 || entry["sourceSignature"].as_str() != Some(&signature);
212 if reconnect && !assembly.access().is_editable() {
213 return Err("the assembly is read-only; its source link cannot be updated".into());
214 }
215 entry["sourceKey"] = serde_json::json!(identity);
216 entry["sourceSignature"] = serde_json::json!(signature);
217 ensure_absent(store, &identity)?;
219 store.write(&identity, &contents)?;
220 if reconnect {
221 docs.get_mut(index).unwrap().engine.edit_document_json(&document.to_string())
222 .map_err(|e| format!("file saved at {identity}, but reconnect failed: {e}; cancel and open the saved part"))?;
223 }
224 docs.open_document(standalone);
225 self.save_generation += 1;
226 self.status = format!("created and opened {identity}");
227 self.cancel_component_part();
228 Ok(())
229 }
230}
231
232fn ensure_absent(store: &dyn ModelStore, identity: &str) -> Result<(), String> {
233 match read_now(store, identity) {
236 ReadNow::Ready(_) => {
237 return Err("the file now exists; cancel and open it (it was not overwritten)".into())
238 }
239 ReadNow::Loading => return Err("the file is still loading; retry in a moment".into()),
240 ReadNow::Absent => {}
241 }
242 #[cfg(not(target_arch = "wasm32"))]
243 {
244 let path = std::path::PathBuf::from(store.canonical_identity(identity));
245 if path.is_absolute()
246 && path
247 .try_exists()
248 .map_err(|e| format!("cannot check destination: {e}"))?
249 {
250 return Err(
251 "the destination exists but could not be read; it was not overwritten".into(),
252 );
253 }
254 }
255 Ok(())
256}
257