1use 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 taken: Option<(String, String)>,
15 overwrite: Option<String>,
17}
18
19impl FileDialog {
20 pub fn edit_component_part(
21 &mut self,
22 docs: &mut Documents,
23 store: &dyn ModelStore,
24 component_id: &str,
25 ) {
26 if component_id.contains(':') {
29 match docs.occurrence_part(component_id) {
30 Ok((part, source)) if source.as_deref().is_none_or(str::is_empty) => {
31 self.name_buf = part;
32 self.status.clear();
33 self.use_folder(Purpose::Models, store);
34 self.component_part = Some(Recovery {
35 assembly_id: docs.active_id(),
36 component_id: component_id.into(),
37 source: None,
38 destination: None,
39 taken: None,
40 overwrite: None,
41 });
42 self.open_modal(Mode::CreateComponentPart);
43 }
44 Ok((_, Some(source))) => self.open_document(docs, store, &source),
45 Ok(_) => {}
46 Err(error) => docs.engine_mut().push_notice(format!("'{component_id}': {error}")),
47 }
48 return;
49 }
50 let source = part_source_key(docs.engine(), component_id);
51 if let Some(key) = &source {
52 match read_now(store, key) {
53 ReadNow::Ready(_) => {
54 self.open_document(docs, store, key);
55 if self.status.starts_with("open failed:") {
56 docs.engine_mut().push_notice(self.status.clone());
57 }
58 return;
59 }
60 ReadNow::Loading => {
64 self.open_document(docs, store, key);
65 return;
66 }
67 ReadNow::Absent => {}
68 }
69 }
70 let Some(info) = docs.engine().component_info(component_id) else {
71 docs.engine_mut()
72 .push_notice(format!("'{component_id}' is not an assembly component"));
73 return;
74 };
75 let destination = source.clone().filter(|key| {
76 let file = crate::store::file_name_of(key);
77 !key.starts_with('@')
78 && key.trim() == key
79 && !file.is_empty()
80 && file != "."
81 && file != ".."
82 });
83 self.name_buf = destination
84 .as_deref()
85 .map(model_display_name)
86 .unwrap_or(info.part_name);
87 self.status.clear();
88 self.use_folder(Purpose::Models, store);
89 self.component_part = Some(Recovery {
90 assembly_id: docs.active_id(),
91 component_id: component_id.into(),
92 source,
93 destination,
94 taken: None,
95 overwrite: None,
96 });
97 self.open_modal(Mode::CreateComponentPart);
98 }
99
100 fn cancel_component_part(&mut self) {
101 self.component_part = None;
102 self.open = false;
103 }
104
105 pub(super) fn show_create_component_part(
106 &mut self,
107 ctx: &egui::Context,
108 docs: &mut Documents,
109 store: &dyn ModelStore,
110 ) {
111 let mut create = false;
112 let mut cancel = false;
113 let mut choice: Option<bool> = None;
115 let source = self
116 .component_part
117 .as_ref()
118 .and_then(|p| p.destination.clone());
119 let taken = self.component_part.as_ref().and_then(|p| p.taken.clone());
120 let modal = egui::Modal::new(egui::Id::new("brep-create-component-part")).show(ctx, |ui| {
121 ui.heading("Create part file?");
122 ui.label("The component has no available source file. Save its embedded part definition and open it in its own tab.");
123 if let Some(key) = source {
124 ui.label(format!("Source: {key}"));
125 } else {
126 ui.label(format!("Folder: {}", store.browser_location()));
127 ui.label("File name");
128 let field = ui.add(egui::TextEdit::singleline(&mut self.name_buf).hint_text("part name"));
129 self.hit("field:name", &field);
130 if self.want_focus {
131 field.request_focus();
132 self.want_focus = false;
133 }
134 }
135 match taken.clone() {
136 Some((existing, free)) => {
138 ui.label(format!(
139 "'{}' exists: overwrite it, or save as '{}'?",
140 model_display_name(&existing),
141 model_display_name(&free)
142 ));
143 ui.horizontal(|ui| {
144 let button = ui.button(format!("Save as '{}'", model_display_name(&free)));
145 self.hit("createpart:free", &button);
146 if button.clicked() || ui.input(|i| i.key_pressed(egui::Key::Enter)) {
147 choice = Some(false);
148 }
149 let button = ui.button(format!("Overwrite '{}'", model_display_name(&existing)));
150 self.hit("createpart:overwrite", &button);
151 if button.clicked() {
152 choice = Some(true);
153 }
154 let button = ui.button("Cancel");
155 self.hit("cancel", &button);
156 cancel = button.clicked();
157 });
158 }
159 None => {
160 ui.horizontal(|ui| {
161 let button = ui.button("Create part file");
162 self.hit("createpart", &button);
163 create = button.clicked();
164 let button = ui.button("Cancel");
165 self.hit("cancel", &button);
166 cancel = button.clicked();
167 });
168 }
169 }
170 if !self.status.is_empty() { ui.colored_label(ui.visuals().error_fg_color, &self.status); }
171 });
172 if let (Some(overwrite), Some((existing, free))) = (choice, taken) {
173 if let Some(pending) = self.component_part.as_mut() {
174 pending.taken = None;
175 pending.overwrite = overwrite.then(|| existing.clone());
176 }
177 if !overwrite {
178 self.name_buf = model_display_name(&free);
179 }
180 create = true;
181 }
182 if cancel || modal.should_close() {
183 self.cancel_component_part();
184 } else if create {
185 if let Err(error) = self.create_component_part(docs, store) {
186 self.status = format!("Create part failed: {error}");
187 }
188 }
189 }
190
191 fn create_component_part(
192 &mut self,
193 docs: &mut Documents,
194 store: &dyn ModelStore,
195 ) -> Result<(), String> {
196 let pending = self
197 .component_part
198 .as_ref()
199 .ok_or("no component selected")?;
200 let embedded_only = pending.source.as_deref().is_none_or(str::is_empty);
201 let overwrite = pending.overwrite.clone();
202 let index = docs
203 .iter()
204 .position(|doc| doc.id() == pending.assembly_id)
205 .ok_or("the assembly tab was closed")?;
206 let nested = pending.component_id.contains(':');
209 let assembly = docs.get(index).unwrap();
210 if !nested {
211 assembly
212 .engine
213 .component_info(&pending.component_id)
214 .ok_or("the component was removed")?;
215 if part_source_key(&assembly.engine, &pending.component_id) != pending.source {
216 return Err("the component source changed; cancel and Edit Part again".into());
217 }
218 }
219 if store.plm_client().is_some()
222 || pending
223 .source
224 .as_deref()
225 .is_some_and(|key| crate::plm::uses::revision_key_in(key).is_some())
226 {
227 return Err("this storage cannot create a missing PLM revision here; create or restore the part through PLM, then retry Edit Part".into());
228 }
229 let identity = match &pending.destination {
230 Some(key) => {
231 if key.starts_with('@') || key.trim().is_empty() {
232 return Err("invalid source file identity".into());
233 }
234 key.clone()
235 }
236 None => {
237 let name = self.name_buf.trim();
238 if name.is_empty()
239 || name.starts_with('@')
240 || name.contains(['/', '\\'])
241 || name == "."
242 || name == ".."
243 {
244 return Err("enter a valid file name without folders".into());
245 }
246 let file = crate::store::model_file_name(name);
247 let folder = store.browser_location();
248 if std::path::Path::new(&folder).is_absolute() {
251 sibling_identity(&format!("{}/_", folder.trim_end_matches('/')), &file)
252 } else {
253 store.canonical_identity(&file)
254 }
255 }
256 };
257 if docs.iter().any(|doc| {
258 doc.name().is_some_and(|name| {
259 store.canonical_identity(name) == store.canonical_identity(&identity)
260 })
261 }) {
262 return Err("that part already has an open tab; cancel and open that tab".into());
263 }
264 let overwriting = overwrite.as_deref() == Some(identity.as_str());
265 if embedded_only && !overwriting && matches!(read_now(store, &identity), ReadNow::Ready(_)) {
266 let free = free_beside(store, &identity);
267 if let Some(pending) = self.component_part.as_mut() {
268 pending.taken = Some((identity, free));
269 }
270 return Ok(());
271 }
272 if !overwriting {
273 ensure_absent(store, &identity)?;
274 }
275 if nested {
276 let component_id = pending.component_id.clone();
277 docs.save_part_as(index, store, &component_id, &identity, overwriting)?;
278 self.open_document(docs, store, &identity);
279 self.save_generation += 1;
280 self.status = format!("created and opened {identity}");
281 self.cancel_component_part();
282 return Ok(());
283 }
284 let info = docs
285 .get(index)
286 .unwrap()
287 .engine
288 .component_info(&pending.component_id)
289 .ok_or("the component was removed")?;
290 let assembly = docs.get(index).unwrap();
291 let mut document: serde_json::Value =
292 serde_json::from_str(&assembly.engine.history_request_json())
293 .map_err(|e| e.to_string())?;
294 let entry = &mut document["partsLibrary"][&info.part_name];
295 let part = entry
296 .get("document")
297 .filter(|part| {
298 part.is_object()
299 && part
300 .get("features")
301 .and_then(|v| v.as_array())
302 .is_some_and(|features| !features.is_empty())
303 })
304 .ok_or(
305 "the component has no recoverable embedded part history; no blank file was created",
306 )?;
307 let contents = part.to_string();
308 let mut engine = docs.spawn_engine();
311 engine
312 .load_model_and_fit(&contents)
313 .map_err(|e| format!("embedded part cannot be opened: {e}"))?;
314 let mut standalone = Document::new(engine);
315 standalone.set_name(Some(identity.clone()));
316 let signature = document_signature(&contents);
317 let reconnect = entry["sourceKey"].as_str() != Some(&identity)
318 || entry["sourceSignature"].as_str() != Some(&signature);
319 if reconnect && !assembly.access().is_editable() {
320 return Err("the assembly is read-only; its source link cannot be updated".into());
321 }
322 if !overwriting {
324 ensure_absent(store, &identity)?;
325 }
326 if embedded_only {
327 let component_id = pending.component_id.clone();
331 docs.save_part_as(index, store, &component_id, &identity, overwriting)?;
332 } else {
333 entry["sourceKey"] = serde_json::json!(identity);
334 entry["sourceSignature"] = serde_json::json!(signature);
335 store.write(&identity, &contents)?;
336 if reconnect {
337 docs.get_mut(index).unwrap().engine.edit_document_json(&document.to_string())
338 .map_err(|e| format!("file saved at {identity}, but reconnect failed: {e}; cancel and open the saved part"))?;
339 }
340 }
341 docs.open_document(standalone);
342 self.save_generation += 1;
343 self.status = format!("created and opened {identity}");
344 self.cancel_component_part();
345 Ok(())
346 }
347}
348
349fn free_beside(store: &dyn ModelStore, identity: &str) -> String {
352 let file = crate::store::file_name_of(identity);
353 let (stem, extension) = match file.rfind('.') {
354 Some(dot) if dot > 0 => (&file[..dot], &file[dot..]),
355 _ => (file.as_str(), ".nbrep"),
356 };
357 (2..)
358 .map(|n| crate::store::sibling_identity(identity, &format!("{stem}-{n}{extension}")))
359 .find(|candidate| matches!(read_now(store, candidate), ReadNow::Absent))
360 .unwrap_or_default()
361}
362
363fn ensure_absent(store: &dyn ModelStore, identity: &str) -> Result<(), String> {
364 match read_now(store, identity) {
367 ReadNow::Ready(_) => {
368 return Err("the file now exists; cancel and open it (it was not overwritten)".into())
369 }
370 ReadNow::Loading => return Err("the file is still loading; retry in a moment".into()),
371 ReadNow::Absent => {}
372 }
373 #[cfg(not(target_arch = "wasm32"))]
374 {
375 let path = std::path::PathBuf::from(store.canonical_identity(identity));
376 if path.is_absolute()
377 && path
378 .try_exists()
379 .map_err(|e| format!("cannot check destination: {e}"))?
380 {
381 return Err(
382 "the destination exists but could not be read; it was not overwritten".into(),
383 );
384 }
385 }
386 Ok(())
387}
388