1use super::*;
60use serde_json::{json, Value};
61use std::borrow::Borrow;
62use std::collections::{BTreeMap, BTreeSet};
63
64pub const DERIVED_FROM_KEY: &str = "derivedFrom";
66
67pub const REPLACE_MARKER: &str = "replace";
69
70const BROKEN: [&str; 3] = ["invalid-selection", "unsupported-selection", "error"];
73
74pub const BROKEN_STATUSES: [&str; 3] = BROKEN;
76
77pub fn lineage_keys(
82 document: &Value,
83 identity: &str,
84 canonicalize: &dyn Fn(&str) -> String,
85) -> BTreeSet<String> {
86 let mut keys = BTreeSet::new();
87 if !identity.trim().is_empty() {
88 keys.insert(canonicalize(identity.trim()));
89 }
90 for entry in document[DERIVED_FROM_KEY].as_array().into_iter().flatten() {
91 if let Some(entry) = entry.as_str().map(str::trim).filter(|e| !e.is_empty()) {
92 if entry.starts_with("family:") {
94 keys.insert(entry.to_string());
95 } else {
96 keys.insert(canonicalize(entry));
97 }
98 }
99 }
100 if let Some(family) = family_key(document) {
101 keys.insert(family);
102 }
103 keys
104}
105
106pub fn family_key(document: &Value) -> Option<String> {
108 document["familySource"]["family"]
109 .as_str()
110 .map(str::trim)
111 .filter(|family| !family.is_empty())
112 .map(|family| format!("family:{family}"))
113}
114
115pub fn shared_lineage(old: &BTreeSet<String>, new: &BTreeSet<String>) -> Option<&'static str> {
117 let common: Vec<&String> = old.intersection(new).collect();
118 if common.is_empty() {
119 None
120 } else if common.iter().any(|key| !key.starts_with("family:")) {
121 Some("derived")
123 } else {
124 Some("family")
125 }
126}
127
128pub fn stamp_derived_from(document: &mut Value, source_identity: &str) {
133 let Some(object) = document.as_object_mut() else { return };
134 let family = object
135 .get("familySource")
136 .and_then(|source| source.get("family"))
137 .and_then(Value::as_str)
138 .map(str::trim)
139 .filter(|family| !family.is_empty())
140 .map(|family| format!("family:{family}"));
141 let chain = object
142 .entry(DERIVED_FROM_KEY)
143 .or_insert_with(|| Value::Array(Vec::new()));
144 if !chain.is_array() {
145 *chain = Value::Array(Vec::new());
146 }
147 let list = chain.as_array_mut().expect("array");
148 for entry in [Some(source_identity.trim().to_string()), family].into_iter().flatten() {
149 if !entry.is_empty() && !list.iter().any(|e| e.as_str() == Some(&entry)) {
150 list.push(Value::String(entry));
151 }
152 }
153}
154
155fn triple(value: &Value) -> Option<[f64; 3]> {
162 let list = value.as_array()?;
163 if list.len() != 3 {
164 return None;
165 }
166 Some([list[0].as_f64()?, list[1].as_f64()?, list[2].as_f64()?])
167}
168
169use crate::geometry3d::sub3 as sub;
170
171use crate::geometry3d::dot3 as dot;
172
173use crate::geometry3d::len3 as norm;
174
175fn line_distance(p: [f64; 3], origin: [f64; 3], direction: [f64; 3]) -> f64 {
177 let d = sub(p, origin);
178 let along = dot(d, direction);
179 (dot(d, d) - along * along).max(0.0).sqrt()
180}
181
182const POSITION_TOLERANCE: f64 = 1e-6;
187const ANGLE_TOLERANCE: f64 = 1e-9;
188
189pub fn geometry_agrees(old: &Value, new: &Value) -> bool {
194 if old["geom"] != new["geom"] {
195 return false;
196 }
197 let (Some(p_old), Some(p_new)) = (triple(&old["point"]), triple(&new["point"])) else {
198 return false;
199 };
200 let directions = (triple(&old["direction"]), triple(&new["direction"]));
201 let radius_agrees = match (old["radius"].as_f64(), new["radius"].as_f64()) {
202 (Some(a), Some(b)) => (a - b).abs() <= POSITION_TOLERANCE * (1.0 + a.abs()),
203 (None, None) => true,
204 _ => false,
205 };
206 let scale = POSITION_TOLERANCE * (1.0 + norm(p_old));
207 match old["geom"].as_str().unwrap_or("") {
208 "plane" => match directions {
209 (Some(n_old), Some(n_new)) => {
210 dot(n_old, n_new) >= 1.0 - ANGLE_TOLERANCE
211 && dot(sub(p_old, p_new), n_new).abs() <= scale
212 }
213 _ => false,
214 },
215 "axis" | "line" => match directions {
216 (Some(d_old), Some(d_new)) => {
217 dot(d_old, d_new).abs() >= 1.0 - ANGLE_TOLERANCE
218 && line_distance(p_old, p_new, d_new) <= scale
219 && radius_agrees
220 }
221 _ => false,
222 },
223 "circle" => match directions {
224 (Some(d_old), Some(d_new)) => {
225 dot(d_old, d_new).abs() >= 1.0 - ANGLE_TOLERANCE
226 && norm(sub(p_old, p_new)) <= scale
227 && radius_agrees
228 }
229 _ => false,
230 },
231 "sphere" | "point" => norm(sub(p_old, p_new)) <= scale && radius_agrees,
232 _ => false,
233 }
234}
235
236fn suggestion_score(old: &Value, new: &Value) -> f64 {
240 let distance = match (triple(&old["point"]), triple(&new["point"])) {
241 (Some(a), Some(b)) => norm(sub(a, b)),
242 _ => f64::INFINITY,
243 };
244 let misalignment = match (triple(&old["direction"]), triple(&new["direction"])) {
245 (Some(a), Some(b)) => {
246 if old["geom"] == "plane" {
247 1.0 - dot(a, b)
248 } else {
249 1.0 - dot(a, b).abs()
250 }
251 }
252 _ => 0.0,
253 };
254 distance + 1000.0 * misalignment
255}
256
257fn local_name<'a>(component: &str, reference: &'a str) -> &'a str {
259 reference
260 .strip_prefix(component)
261 .and_then(|rest| rest.strip_prefix(':'))
262 .unwrap_or(reference)
263}
264
265#[derive(Debug, Clone, PartialEq)]
267pub enum RefOutcome {
268 Kept { split: bool },
270 Unresolved {
274 reason: &'static str,
275 suggestion: Option<Suggestion>,
276 },
277}
278
279#[derive(Debug, Clone, PartialEq)]
280pub struct Suggestion {
281 pub reference: String,
282 pub basis: &'static str,
284 pub pre_ticked: bool,
287}
288
289impl Suggestion {
290 fn to_json(&self) -> Value {
291 json!({ "ref": self.reference, "basis": self.basis, "preTicked": self.pre_ticked })
292 }
293}
294
295pub fn classify_ref<V: std::borrow::Borrow<Value>>(
300 component: &str,
301 reference: &str,
302 pre: &Value,
303 candidates: &[V],
304 old_names: &BTreeSet<String>,
305 trusted: bool,
306) -> RefOutcome {
307 let name = local_name(component, reference);
308 let by_name = candidates
309 .iter()
310 .map(Borrow::borrow)
311 .find(|candidate| candidate["ref"].as_str().map(|r| local_name(component, r)) == Some(name));
312 if trusted {
313 match by_name {
314 Some(candidate) if candidate["geom"] == pre["geom"] => {
315 let split_piece = format!("{name}_1");
316 let split = !old_names.contains(&split_piece)
317 && candidates.iter().map(Borrow::borrow).any(|c: &Value| {
318 c["ref"].as_str().map(|r| local_name(component, r)) == Some(split_piece.as_str())
319 });
320 return RefOutcome::Kept { split };
321 }
322 Some(_) => {
323 return RefOutcome::Unresolved {
324 reason: "type-changed",
325 suggestion: nearest(pre, candidates),
326 }
327 }
328 None => {
329 return RefOutcome::Unresolved { reason: "missing", suggestion: nearest(pre, candidates) }
330 }
331 }
332 }
333 let suggestion = match by_name {
334 Some(candidate) if candidate["geom"] == pre["geom"] => Some(Suggestion {
335 reference: candidate["ref"].as_str().unwrap_or_default().to_string(),
336 basis: "same-name",
337 pre_ticked: geometry_agrees(pre, candidate),
338 }),
339 _ => nearest(pre, candidates),
340 };
341 RefOutcome::Unresolved { reason: "unconfirmed", suggestion }
342}
343
344pub fn classify_vertex_ref(reference: &str, pre: &Value, post: Option<&Value>, trusted: bool) -> RefOutcome {
350 let found = post.filter(|row| row["ok"] == true);
351 let same_point = found.is_some_and(|row| geometry_agrees(pre, row));
352 if trusted && same_point {
353 return RefOutcome::Kept { split: false };
354 }
355 let suggestion = found.map(|_| Suggestion {
356 reference: reference.to_string(),
357 basis: "same-name",
358 pre_ticked: same_point,
359 });
360 let reason = match (trusted, found) {
361 (false, _) => "unconfirmed",
362 (true, Some(_)) => "moved",
363 (true, None) => "missing",
364 };
365 RefOutcome::Unresolved { reason, suggestion }
366}
367
368fn nearest<V: std::borrow::Borrow<Value>>(pre: &Value, candidates: &[V]) -> Option<Suggestion> {
371 candidates
372 .iter()
373 .map(Borrow::borrow)
374 .filter(|candidate| candidate["geom"] == pre["geom"])
375 .map(|candidate| (suggestion_score(pre, candidate), candidate))
376 .filter(|(score, _)| score.is_finite())
377 .min_by(|a, b| a.0.total_cmp(&b.0))
378 .map(|(_, candidate)| Suggestion {
379 reference: candidate["ref"].as_str().unwrap_or_default().to_string(),
380 basis: "nearest",
381 pre_ticked: geometry_agrees(pre, candidate),
382 })
383}
384
385pub fn marker_json(origin: &str, from: &str, to: &str, was_enabled: bool, refs: Vec<Value>) -> Value {
389 json!({
390 "origin": origin,
391 "from": from,
392 "to": to,
393 "wasEnabled": was_enabled,
394 "refs": refs,
395 })
396}
397
398fn unique_carrier(pre: &Value, candidates: &[&Value]) -> Option<String> {
402 let mut agreeing = candidates.iter().filter(|candidate| geometry_agrees(pre, candidate));
403 let only = agreeing.next()?;
404 if agreeing.next().is_some() {
405 return None;
406 }
407 only["ref"].as_str().map(str::to_string)
408}
409
410#[derive(Debug, Clone)]
416pub(crate) struct AffectedMate {
417 id: String,
418 constraint_type: String,
419 was_enabled: bool,
420 refs: Vec<(String, String, Value)>,
422 home: Option<(String, String)>,
426}
427
428#[derive(Debug, Clone)]
430pub(crate) struct PendingReplace {
431 generation: u64,
432 components: Vec<String>,
433 from: BTreeMap<String, String>,
434 to: String,
435 lineage: BTreeMap<String, &'static str>,
437 affected: Vec<AffectedMate>,
438 old_names: BTreeMap<String, BTreeSet<String>>,
440 notes: Vec<String>,
442 origin: &'static str,
444 signatures: BTreeMap<String, String>,
447}
448
449
450#[derive(Debug, Clone)]
452pub struct ReplaceCapture {
453 components: Vec<String>,
454 affected: Vec<AffectedMate>,
455 old_names: BTreeMap<String, BTreeSet<String>>,
456}
457
458pub fn entries_lineage(old: &Value, new: &Value, canonicalize: &dyn Fn(&str) -> String) -> Option<&'static str> {
461 let old_keys = lineage_keys(&old["document"], old["sourceKey"].as_str().unwrap_or(""), canonicalize);
462 let new_keys = lineage_keys(&new["document"], new["sourceKey"].as_str().unwrap_or(""), canonicalize);
463 shared_lineage(&old_keys, &new_keys)
464}
465
466
467
468#[derive(Default)]
471struct EntityListings {
472 by_top: BTreeMap<String, Vec<Value>>,
473 names_by_owner: BTreeMap<String, BTreeMap<String, BTreeSet<String>>>,
478 rows_by_owner: BTreeMap<String, BTreeMap<String, Vec<usize>>>,
484}
485
486fn owners_below<'a>(top: &'a str, reference: &'a str) -> impl Iterator<Item = String> + 'a {
489 let rest = reference.strip_prefix(top).and_then(|r| r.strip_prefix(':')).unwrap_or("");
490 rest.match_indices(':').map(move |(at, _)| format!("{top}:{}", &rest[..at]))
491}
492
493impl EntityListings {
494 fn listed(&mut self, top: &str) -> &[Value] {
496 self.by_top.entry(top.to_string()).or_insert_with(|| {
497 entities_of(top)
498 })
499 }
500
501 fn index(&mut self, path: &str) {
503 let top = path.split(':').next().unwrap_or(path).to_string();
504 if self.rows_by_owner.contains_key(&top) {
505 return;
506 }
507 let rows = self.listed(&top);
508 let mut index: BTreeMap<String, Vec<usize>> = BTreeMap::new();
509 index.insert(top.clone(), (0..rows.len()).collect());
510 for (at, row) in rows.iter().enumerate() {
511 let Some(reference) = row["ref"].as_str() else { continue };
512 for owner in owners_below(&top, reference) {
513 index.entry(owner).or_default().push(at);
514 }
515 }
516 self.rows_by_owner.insert(top, index);
517 }
518
519 fn under(&self, path: &str) -> Vec<&Value> {
523 let top = path.split(':').next().unwrap_or(path);
524 let (Some(rows), Some(index)) = (self.by_top.get(top), self.rows_by_owner.get(top)) else {
525 return Vec::new();
526 };
527 index.get(path).into_iter().flatten().map(|&at| &rows[at]).collect()
528 }
529
530 fn names(&mut self, path: &str) -> BTreeSet<String> {
533 let top = path.split(':').next().unwrap_or(path).to_string();
534 if !self.names_by_owner.contains_key(&top) {
535 self.listed(&top);
536 let mut index: BTreeMap<String, BTreeSet<String>> = BTreeMap::new();
537 for row in self.by_top.get(&top).into_iter().flatten() {
538 let Some(reference) = row["ref"].as_str() else { continue };
539 let Some(rest) = reference.strip_prefix(&top).and_then(|r| r.strip_prefix(':')) else {
540 index.entry(top.clone()).or_default().insert(reference.to_string());
541 continue;
542 };
543 let mut owner = top.clone();
546 index.entry(owner.clone()).or_default().insert(rest.to_string());
547 let mut tail = rest;
548 while let Some((segment, after)) = tail.split_once(':') {
549 owner = format!("{owner}:{segment}");
550 index.entry(owner.clone()).or_default().insert(after.to_string());
551 tail = after;
552 }
553 }
554 self.names_by_owner.insert(top.clone(), index);
555 }
556 self.names_by_owner[&top].get(path).cloned().unwrap_or_default()
557 }
558}
559
560fn entities_of(path: &str) -> Vec<Value> {
563 let top = path.split(':').next().unwrap_or(path);
564 let prefix = format!("{path}:");
565 parse(&brep_kernel::assembly_component_entities_json(top))
566 .as_array()
567 .cloned()
568 .unwrap_or_default()
569 .into_iter()
570 .filter(|row| top == path || row["ref"].as_str().is_some_and(|r| r.starts_with(&prefix)))
571 .collect()
572}
573
574fn document_in_chain_mut<'a>(document: &'a mut Value, chain: &[String]) -> Option<&'a mut Value> {
577 let mut current = document;
578 for part in chain {
579 current = current.get_mut("partsLibrary")?.get_mut(part.as_str())?.get_mut("document")?;
580 }
581 Some(current)
582}
583
584fn owner_among<'a>(ids: &'a [String], reference: &str) -> Option<&'a String> {
586 ids.iter().find(|id| {
587 reference == id.as_str()
588 || reference
589 .strip_prefix(id.as_str())
590 .is_some_and(|rest| rest.starts_with(':'))
591 })
592}
593
594pub fn follow_pattern_seeds(
602 document: &Value,
603 from: &BTreeMap<String, String>,
604 to: &str,
605 instances: &[String],
606) -> Vec<String> {
607 let mut notes = Vec::new();
608 for feature in document["features"].as_array().into_iter().flatten() {
609 if feature["type"] != "APAT" {
610 continue;
611 }
612 let pattern = feature["inputParams"]["id"].as_str().unwrap_or_default().to_string();
613 let mut seeded = BTreeSet::new();
614 for name in feature["inputParams"]["seeds"].as_array().into_iter().flatten().filter_map(Value::as_str) {
615 let owner = name.split(':').next().unwrap_or(name).to_string();
616 if from.contains_key(&owner) {
617 seeded.insert(owner);
618 }
619 }
620 for owner in seeded {
621 let count = instances
622 .iter()
623 .filter_map(|id| brep_kernel::pattern_component_instance(id))
624 .filter(|(p, s, _)| *p == pattern && *s == owner)
625 .count();
626 notes.push(format!("{pattern}: its {count} instance(s) of {owner} follow the seed and now show '{to}'"));
627 }
628 }
629 notes
630}
631
632fn parse(json: &str) -> Value {
633 serde_json::from_str(json).unwrap_or(Value::Null)
634}
635
636impl EngineState {
637 pub fn replace_components(
644 &mut self,
645 component_ids: &[String],
646 insert: ComponentInsert<'_>,
647 canonicalize: &dyn Fn(&str) -> String,
648 ) -> Result<String, String> {
649 if component_ids.is_empty() {
650 return Err("replace: no component given".into());
651 }
652 let mut ids: Vec<String> = Vec::new();
656 let mut instance_notes = Vec::new();
657 for id in component_ids {
658 let id = match brep_kernel::pattern_component_instance(id) {
659 Some((pattern, seed, n)) => {
660 instance_notes.push(format!(
661 "{id} is instance {n} of {pattern} and follows its seed {seed}: the seed was replaced"
662 ));
663 seed
664 }
665 None => id.clone(),
666 };
667 if !ids.contains(&id) {
668 ids.push(id);
669 }
670 }
671 let component_ids = ids.as_slice();
672 let mut from = BTreeMap::new();
673 for id in component_ids {
674 let info = self
675 .component_info(id)
676 .ok_or_else(|| format!("replace: '{id}' is not an assembly component"))?;
677 from.insert(id.clone(), info.part_name);
678 }
679 let capture = self.capture_replace(component_ids);
680
681 let old_library = self.history.parts_library().clone();
683 let to = self.resolve_component_insert(insert)?;
684 let library = parse(&brep_kernel::parts_library_json());
685 let new_entry = &library[&to];
686 let mut lineage = BTreeMap::new();
687 for (id, part) in &from {
688 if part == &to {
689 continue;
690 }
691 let old_entry = if old_library[part].is_object() { &old_library[part] } else { &library[part] };
692 if let Some(word) = entries_lineage(old_entry, new_entry, canonicalize) {
693 lineage.insert(id.clone(), word);
694 }
695 }
696 if from.values().all(|part| part == &to) {
697 return Err(format!("replace: the component already shows '{to}'"));
698 }
699
700 let mut document = parse(&self.history_request_json());
704 document["partsLibrary"] = library.clone();
705 for feature in document["features"].as_array_mut().into_iter().flatten() {
706 let id = feature["inputParams"]["id"].as_str().unwrap_or_default().to_string();
707 if from.contains_key(&id) {
708 feature["inputParams"]["partName"] = Value::String(to.clone());
709 }
710 }
711 let mut notes = instance_notes;
712 notes.extend(follow_pattern_seeds(&document, &from, &to, &self.pattern_instance_ids()));
713 self.commit_replace(capture, document, from, to.clone(), lineage, notes)?;
714 Ok(to)
715 }
716
717 pub fn capture_replace(&mut self, paths: &[String]) -> ReplaceCapture {
724 self.capture_replace_listed(paths, &mut EntityListings::default())
725 }
726
727 fn capture_replace_listed(&mut self, paths: &[String], listings: &mut EntityListings) -> ReplaceCapture {
731 self.ensure_assembly_session();
732 let state = parse(&brep_kernel::assembly_state_json());
733 let mut affected = Vec::new();
734 for entry in state["constraints"].as_array().into_iter().flatten() {
735 let refs: Vec<(String, String)> = entry["inputParams"]["elements"]
736 .as_array()
737 .into_iter()
738 .flatten()
739 .filter_map(Value::as_str)
740 .filter_map(|r| owner_among(paths, r).map(|owner| (owner.clone(), r.to_string())))
741 .collect();
742 if refs.is_empty() {
743 continue;
744 }
745 let enabled = entry["enabled"].as_bool().unwrap_or(true);
746 let status = entry["persistentData"]["status"].as_str().unwrap_or("");
747 if enabled && BROKEN.contains(&status) {
748 continue; }
750 let names: Vec<&String> = refs.iter().map(|(_, r)| r).collect();
751 let rows = parse(&brep_kernel::assembly_resolve_refs_json(
752 &serde_json::to_string(&names).unwrap_or_default(),
753 ));
754 let rows = rows.as_array().cloned().unwrap_or_default();
755 affected.push(AffectedMate {
756 id: entry["inputParams"]["id"].as_str().unwrap_or_default().to_string(),
757 constraint_type: entry["type"].as_str().unwrap_or_default().to_string(),
758 was_enabled: enabled,
759 refs: refs
760 .into_iter()
761 .zip(rows.into_iter().chain(std::iter::repeat(Value::Null)))
762 .map(|((owner, r), row)| (owner, r, row))
763 .collect(),
764 home: None,
765 });
766 }
767 let mut old_names = BTreeMap::new();
768 for path in paths {
769 old_names.insert(path.clone(), listings.names(path));
770 }
771 ReplaceCapture { components: paths.to_vec(), affected, old_names }
772 }
773
774 pub fn capture_part_edit(&mut self, paths: &[String]) -> ReplaceCapture {
781 let mut listings = EntityListings::default();
785 let mut capture = self.capture_replace_listed(paths, &mut listings);
786 let mut seen: BTreeSet<Vec<String>> = BTreeSet::new();
789 let mut stack: Vec<String> = paths.to_vec();
790 while let Some(path) = stack.pop() {
791 let Some(chain) = self.part_chain(&path) else { continue };
792 if !seen.insert(chain.clone()) {
793 continue;
794 }
795 let Some(document) = self.document_in_chain(&chain) else { continue };
796 for feature in document["features"].as_array().into_iter().flatten() {
797 if super::components::is_acomp_feature_type(feature["type"].as_str().unwrap_or("")) {
798 if let Some(id) = feature["inputParams"]["id"].as_str() {
799 stack.push(format!("{path}:{id}"));
800 }
801 }
802 }
803 for entry in document["assembly"]["constraints"].as_array().into_iter().flatten() {
804 let inner_id = entry["inputParams"]["id"].as_str().unwrap_or_default();
805 let refs: Vec<(String, String)> = entry["inputParams"]["elements"]
806 .as_array()
807 .into_iter()
808 .flatten()
809 .filter_map(Value::as_str)
810 .map(|r| {
811 let occurrence = r.split(':').next().unwrap_or(r);
812 (format!("{path}:{occurrence}"), format!("{path}:{r}"))
813 })
814 .collect();
815 if inner_id.is_empty() || refs.is_empty() {
816 continue;
817 }
818 let enabled = entry["enabled"].as_bool().unwrap_or(true);
819 let status = entry["persistentData"]["status"].as_str().unwrap_or("");
820 if enabled && BROKEN.contains(&status) {
821 continue; }
823 let names: Vec<&String> = refs.iter().map(|(_, r)| r).collect();
824 let rows = parse(&brep_kernel::assembly_resolve_refs_json(
825 &serde_json::to_string(&names).unwrap_or_default(),
826 ));
827 let rows = rows.as_array().cloned().unwrap_or_default();
828 if rows.len() != refs.len() || rows.iter().any(|row| row["ok"] != true) {
832 continue;
833 }
834 for (owner, _) in &refs {
835 if !capture.old_names.contains_key(owner) {
836 let names = listings.names(owner);
837 capture.old_names.insert(owner.clone(), names);
838 }
839 }
840 capture.affected.push(AffectedMate {
841 id: format!("{path}:{inner_id}"),
842 constraint_type: entry["type"].as_str().unwrap_or_default().to_string(),
843 was_enabled: enabled,
844 refs: refs
845 .into_iter()
846 .zip(rows.into_iter().chain(std::iter::repeat(Value::Null)))
847 .map(|((owner, r), row)| (owner, r, row))
848 .collect(),
849 home: Some((path.clone(), inner_id.to_string())),
850 });
851 }
852 }
853 capture
854 }
855
856 fn write_back_inner_mates(
863 &mut self,
864 affected: &[AffectedMate],
865 reattached: &[(String, String, String)],
866 markers: &BTreeMap<String, Value>,
867 ) {
868 let touched: Vec<&AffectedMate> = affected
869 .iter()
870 .filter(|mate| mate.home.is_some())
871 .filter(|mate| markers.contains_key(&mate.id) || reattached.iter().any(|(id, _, _)| *id == mate.id))
872 .collect();
873 if touched.is_empty() {
874 return;
875 }
876 let Ok(mut document) = serde_json::from_str::<Value>(&self.history.request_json()) else { return };
877 let mut chains: BTreeSet<Vec<String>> = BTreeSet::new();
878 for mate in touched {
879 let (home, inner_id) = mate.home.as_ref().expect("filtered");
880 let Some(chain) = self.part_chain(home) else { continue };
881 let Some(inner) = document_in_chain_mut(&mut document, &chain) else { continue };
882 let prefix = format!("{home}:");
883 let local = |reference: &str| reference.strip_prefix(&prefix).unwrap_or(reference).to_string();
884 for entry in inner["assembly"]["constraints"].as_array_mut().into_iter().flatten() {
885 if entry["inputParams"]["id"].as_str() != Some(inner_id.as_str()) {
886 continue;
887 }
888 for (_, old_ref, new_ref) in reattached.iter().filter(|(id, _, _)| *id == mate.id) {
889 for element in entry["inputParams"]["elements"].as_array_mut().into_iter().flatten() {
890 if element == local(old_ref).as_str() {
891 *element = Value::String(local(new_ref));
892 }
893 }
894 }
895 if let Some(marker) = markers.get(&mate.id) {
896 let mut marker = marker.clone();
897 for row in marker["refs"].as_array_mut().into_iter().flatten() {
898 if let Some(reference) = row["ref"].as_str().map(|r| local(r)) {
899 row["ref"] = Value::String(reference);
900 }
901 if let Some(reference) = row["suggestion"]["ref"].as_str().map(|r| local(r)) {
902 row["suggestion"]["ref"] = Value::String(reference);
903 }
904 }
905 entry["enabled"] = Value::Bool(false);
906 if !entry["persistentData"].is_object() {
907 entry["persistentData"] = json!({});
908 }
909 entry["persistentData"][REPLACE_MARKER] = marker;
910 }
911 }
912 chains.insert(chain);
913 }
914 let mut prefixes: BTreeSet<Vec<String>> = BTreeSet::new();
916 for chain in &chains {
917 for depth in 1..=chain.len() {
918 prefixes.insert(chain[..depth].to_vec());
919 }
920 }
921 let mut prefixes: Vec<Vec<String>> = prefixes.into_iter().collect();
922 prefixes.sort_by_key(|chain| std::cmp::Reverse(chain.len()));
923 for chain in prefixes {
924 let (last, parent) = chain.split_last().expect("non-empty");
925 let library = if parent.is_empty() {
926 Some(&mut document["partsLibrary"])
927 } else {
928 document_in_chain_mut(&mut document, parent).map(|doc| &mut doc["partsLibrary"])
929 };
930 let Some(entry) = library.and_then(|library| library.get_mut(last.as_str())) else { continue };
931 let signature = super::assembly_ops::document_signature(&entry["document"].to_string());
932 entry["sourceSignature"] = Value::String(signature);
933 entry["snapshot"] = Value::String(String::new());
934 if let Some(object) = entry.as_object_mut() {
935 object.remove("nestedRows");
936 }
937 }
938 let written = serde_json::from_value::<brep_kernel::PartsLibraryMap>(document["partsLibrary"].clone())
941 .map_err(|error| error.to_string())
942 .and_then(|library| self.history.adopt_document(&document.to_string()).map(|()| library));
943 match written {
944 Ok(library) => {
945 brep_kernel::install_parts_library(&library);
946 }
947 Err(error) => {
948 self.push_notice(format!("part edit: the sub-assembly mates could not be written: {error}"));
949 }
950 }
951 }
952
953 fn document_in_chain(&self, chain: &[String]) -> Option<&Value> {
956 let mut library = self.history.parts_library();
957 let mut document = None;
958 for part in chain {
959 let entry = library.get(part.as_str())?;
960 document = Some(&entry["document"]);
961 library = &entry["document"]["partsLibrary"];
962 }
963 document
964 }
965
966 fn part_chain(&self, path: &str) -> Option<Vec<String>> {
970 let mut segments = path.split(':');
971 let top = segments.next()?;
972 let index = self.history.index_of(top)?;
973 let mut part = self.history.feature_params(index)?["partName"].as_str()?.to_string();
974 let mut chain = vec![part.clone()];
975 let mut library = self.history.parts_library();
976 for id in segments {
977 let document = &library.get(part.as_str())?["document"];
978 part = document["features"]
979 .as_array()?
980 .iter()
981 .find(|f| f["inputParams"]["id"].as_str() == Some(id))?["inputParams"]["partName"]
982 .as_str()?
983 .to_string();
984 chain.push(part.clone());
985 library = &document["partsLibrary"];
986 }
987 Some(chain)
988 }
989
990 pub fn commit_replace(
996 &mut self,
997 capture: ReplaceCapture,
998 mut document: Value,
999 from: BTreeMap<String, String>,
1000 to: String,
1001 lineage: BTreeMap<String, &'static str>,
1002 notes: Vec<String>,
1003 ) -> Result<(), String> {
1004 let untrusted: BTreeSet<&String> =
1005 capture.components.iter().filter(|id| !lineage.contains_key(*id)).collect();
1006 if let Some(constraints) = document["assembly"]["constraints"].as_array_mut() {
1007 for entry in constraints.iter_mut() {
1008 let id = entry["inputParams"]["id"].as_str().unwrap_or_default();
1009 let Some(mate) = capture.affected.iter().find(|m| m.id == id) else { continue };
1010 if mate.refs.iter().any(|(owner, _, _)| untrusted.contains(owner)) {
1011 entry["enabled"] = Value::Bool(false);
1012 }
1013 }
1014 }
1015 self.edit_document(document)?;
1016 self.replace_pending = Some(PendingReplace {
1017 generation: self.run_generation,
1018 components: capture.components,
1019 from,
1020 to,
1021 lineage,
1022 affected: capture.affected,
1023 old_names: capture.old_names,
1024 notes,
1025 origin: "replace",
1026 signatures: BTreeMap::new(),
1027 });
1028 self.last_replace_report = None;
1029 if !self.run_pending() {
1030 self.finish_component_replace();
1031 }
1032 Ok(())
1033 }
1034
1035 pub fn commit_part_edit(&mut self, capture: ReplaceCapture, document: Value, parts: &[String]) -> Result<(), String> {
1042 self.finish_component_replace();
1045 if self.replace_pending.is_some() {
1046 return Err("a component replace is still landing; try again in a moment".into());
1047 }
1048 let signatures: BTreeMap<String, String> = parts
1049 .iter()
1050 .map(|part| {
1051 let signature = document["partsLibrary"][part]["sourceSignature"].as_str().unwrap_or_default();
1052 (part.clone(), signature.to_string())
1053 })
1054 .collect();
1055 let from: BTreeMap<String, String> = capture
1056 .components
1057 .iter()
1058 .filter_map(|path| Some((path.clone(), self.part_at_path(path)?)))
1059 .collect();
1060 self.edit_document(document)?;
1061 if capture.affected.is_empty() {
1062 return Ok(());
1063 }
1064 self.replace_pending = Some(PendingReplace {
1065 generation: self.run_generation,
1066 lineage: capture.components.iter().map(|path| (path.clone(), "edit")).collect(),
1067 components: capture.components,
1068 to: parts.join(", "),
1069 from,
1070 affected: capture.affected,
1071 old_names: capture.old_names,
1072 notes: Vec::new(),
1073 origin: "edit",
1074 signatures,
1075 });
1076 self.last_replace_report = None;
1077 if !self.run_pending() {
1078 self.finish_component_replace();
1079 }
1080 Ok(())
1081 }
1082
1083 pub fn components_of_parts(&self, parts: &[String]) -> Vec<String> {
1085 self.history
1086 .features()
1087 .iter()
1088 .filter(|feature| super::components::is_acomp_feature_type(feature["type"].as_str().unwrap_or("")))
1089 .filter(|feature| {
1090 feature["inputParams"]["partName"].as_str().is_some_and(|part| parts.iter().any(|p| p == part))
1091 })
1092 .filter_map(|feature| feature["inputParams"]["id"].as_str().map(str::to_string))
1093 .collect()
1094 }
1095
1096 pub(crate) fn finish_component_replace(&mut self) {
1100 let Some(pending) = self.replace_pending.as_ref() else { return };
1101 if self.run_pending() || self.applied_generation < pending.generation {
1102 return;
1103 }
1104 let pending = self.replace_pending.take().expect("checked above");
1105 let landed = if pending.origin == "edit" {
1110 pending.signatures.iter().all(|(part, signature)| {
1111 self.history.parts_library()[part.as_str()]["sourceSignature"].as_str() == Some(signature.as_str())
1112 })
1113 } else {
1114 pending.components.iter().all(|path| self.part_at_path(path).as_deref() == Some(pending.to.as_str()))
1115 };
1116 if !landed {
1117 self.last_replace_report = None;
1118 return;
1119 }
1120 self.ensure_assembly_session();
1121 let statuses = parse(&brep_kernel::assembly_statuses_json());
1122 let nested = parse(&brep_kernel::assembly_dof_json())["nestedConstraints"].clone();
1124 let status_of = |id: &str| -> String {
1125 statuses
1126 .as_array()
1127 .into_iter()
1128 .flatten()
1129 .chain(nested.as_array().into_iter().flatten())
1130 .find(|row| row["id"] == id)
1131 .and_then(|row| row["status"].as_str())
1132 .unwrap_or("")
1133 .to_string()
1134 };
1135 let vertex_refs: Vec<&String> = pending
1138 .affected
1139 .iter()
1140 .flat_map(|mate| mate.refs.iter().map(|(_, r, _)| r))
1141 .filter(|r| r.contains('@'))
1142 .collect();
1143 let post_vertex: BTreeMap<String, Value> = if vertex_refs.is_empty() {
1144 BTreeMap::new()
1145 } else {
1146 parse(&brep_kernel::assembly_resolve_refs_json(&serde_json::to_string(&vertex_refs).unwrap_or_default()))
1147 .as_array()
1148 .cloned()
1149 .unwrap_or_default()
1150 .into_iter()
1151 .filter_map(|row| Some((row["ref"].as_str()?.to_string(), row)))
1152 .collect()
1153 };
1154 let mut listings = EntityListings::default();
1159 let owners: BTreeSet<&String> = pending
1160 .affected
1161 .iter()
1162 .flat_map(|mate| mate.refs.iter().map(move |(owner, _, _)| (mate, owner)))
1163 .filter(|(mate, owner)| mate.home.is_some() || pending.components.contains(owner))
1164 .map(|(_, owner)| owner)
1165 .collect();
1166 for owner in &owners {
1167 listings.index(owner);
1168 }
1169 let entities: BTreeMap<&String, Vec<&Value>> = owners.iter().map(|owner| (*owner, listings.under(owner))).collect();
1170
1171 let mut kept = Vec::new();
1172 let mut unresolved = Vec::new();
1173 let mut notes = pending.notes.clone();
1174 let mut markers: BTreeMap<String, Value> = BTreeMap::new();
1175 let mut reattached: Vec<(String, String, String)> = Vec::new();
1177 for mate in &pending.affected {
1178 let mut ref_rows = Vec::new();
1179 for (owner, reference, pre) in &mate.refs {
1180 let trusted = pending.lineage.contains_key(owner)
1183 || (mate.home.is_some() && owner_among(&pending.components, owner).is_some_and(|top| pending.lineage.contains_key(top)));
1184 let outcome = if pre["ok"] != true {
1185 RefOutcome::Unresolved { reason: "missing", suggestion: None }
1189 } else if reference.contains('@') {
1190 classify_vertex_ref(reference, pre, post_vertex.get(reference.as_str()), trusted)
1191 } else {
1192 classify_ref(
1193 owner,
1194 reference,
1195 pre,
1196 entities.get(owner).map(Vec::as_slice).unwrap_or(&[]),
1197 pending.old_names.get(owner).unwrap_or(&BTreeSet::new()),
1198 trusted,
1199 )
1200 };
1201 match outcome {
1202 RefOutcome::Kept { split } => {
1203 if split {
1204 notes.push(format!(
1205 "{}: '{}' was split by the new part; the mate keeps the piece that kept the name (same carrier)",
1206 mate.id, reference
1207 ));
1208 }
1209 }
1210 RefOutcome::Unresolved { reason, suggestion } => {
1211 let only = (pending.origin == "edit" && !reference.contains('@'))
1212 .then(|| unique_carrier(pre, entities.get(owner).map(Vec::as_slice).unwrap_or(&[])))
1213 .flatten();
1214 match only {
1215 Some(new_ref) => reattached.push((mate.id.clone(), reference.clone(), new_ref)),
1216 None => ref_rows.push(json!({
1217 "ref": reference,
1218 "reason": reason,
1219 "suggestion": suggestion.as_ref().map(Suggestion::to_json),
1220 })),
1221 }
1222 }
1223 }
1224 }
1225 let status = status_of(&mate.id);
1229 let mate_reattached = reattached.iter().any(|(id, _, _)| *id == mate.id);
1232 if ref_rows.is_empty() && !mate_reattached && mate.was_enabled && BROKEN.contains(&status.as_str()) {
1233 for (_, reference, _) in &mate.refs {
1234 ref_rows.push(json!({ "ref": reference, "reason": "no-longer-maps", "suggestion": null }));
1235 }
1236 }
1237 if ref_rows.is_empty() {
1238 kept.push(json!({ "id": mate.id, "type": mate.constraint_type }));
1239 continue;
1240 }
1241 let owner = &mate.refs[0].0;
1242 let marker = marker_json(
1243 pending.origin,
1244 &pending.from.get(owner).cloned().or_else(|| self.part_at_path(owner)).unwrap_or_default(),
1245 &pending.to,
1246 mate.was_enabled,
1247 ref_rows,
1248 );
1249 unresolved.push(json!({
1250 "id": mate.id,
1251 "type": mate.constraint_type,
1252 "refs": marker["refs"],
1253 }));
1254 markers.insert(mate.id.clone(), marker);
1255 }
1256
1257 if !markers.is_empty() || !reattached.is_empty() {
1258 let mut block = self.history.assembly_block().cloned().unwrap_or(Value::Null);
1259 let mut disabled_now = false;
1260 for entry in block["constraints"].as_array_mut().into_iter().flatten() {
1261 let id = entry["inputParams"]["id"].as_str().unwrap_or_default().to_string();
1262 for (mate, old_ref, new_ref) in &reattached {
1263 if *mate != id {
1264 continue;
1265 }
1266 for element in entry["inputParams"]["elements"].as_array_mut().into_iter().flatten() {
1267 if element == old_ref.as_str() {
1268 *element = Value::String(new_ref.clone());
1269 }
1270 }
1271 }
1272 let Some(marker) = markers.get(&id) else { continue };
1273 if entry["enabled"] != false {
1274 entry["enabled"] = Value::Bool(false);
1275 disabled_now = true;
1276 }
1277 if !entry["persistentData"].is_object() {
1278 entry["persistentData"] = json!({});
1279 }
1280 entry["persistentData"][REPLACE_MARKER] = marker.clone();
1281 }
1282 self.history.set_assembly_block(block, false);
1283 self.write_back_inner_mates(&pending.affected, &reattached, &markers);
1284 let _ = disabled_now;
1288 self.rerun_history();
1289 }
1290
1291 if pending.origin == "edit" && (!reattached.is_empty() || !unresolved.is_empty()) {
1294 let mut lines = Vec::new();
1295 if !reattached.is_empty() {
1296 let moves: Vec<String> = reattached.iter().map(|(id, from, to)| format!("{id} {from} → {to}")).collect();
1297 lines.push(format!("re-attached {}", moves.join(", ")));
1298 }
1299 if !unresolved.is_empty() {
1300 let ids: Vec<&str> = unresolved.iter().filter_map(|row| row["id"].as_str()).collect();
1301 lines.push(format!(
1302 "held for Repair (Constraints panel): {} — the face or edge it used is gone and no single one replaces it",
1303 ids.join(", ")
1304 ));
1305 }
1306 let severity = if unresolved.is_empty() { NoticeSeverity::Info } else { NoticeSeverity::Warning };
1307 self.push_notice_as(severity, format!("Mates after editing {}: {}.", pending.to, lines.join("; ")));
1308 }
1309 let report = json!({
1310 "ok": true,
1311 "origin": pending.origin,
1312 "reattached": reattached.iter().map(|(id, from, to)| json!({ "id": id, "from": from, "to": to })).collect::<Vec<_>>(),
1313 "components": pending.components,
1314 "from": pending.from,
1315 "to": pending.to,
1316 "lineage": pending.components.iter().map(|id| {
1317 (id.clone(), Value::String(pending.lineage.get(id).copied().unwrap_or("none").to_string()))
1318 }).collect::<serde_json::Map<String, Value>>(),
1319 "kept": kept,
1320 "unresolved": unresolved,
1321 "notes": notes,
1322 });
1323 self.last_replace_report = Some(report);
1324 }
1325
1326 pub fn occurrence_entities(&mut self, path: &str) -> Vec<Value> {
1330 self.ensure_assembly_session();
1331 entities_of(path)
1332 }
1333
1334 pub fn resolve_refs(&mut self, refs: &[String]) -> Vec<Value> {
1337 self.ensure_assembly_session();
1338 parse(&brep_kernel::assembly_resolve_refs_json(&serde_json::to_string(refs).unwrap_or_default()))
1339 .as_array()
1340 .cloned()
1341 .unwrap_or_default()
1342 }
1343
1344 pub fn replace_report(&mut self) -> Value {
1349 self.finish_component_replace();
1350 match &self.last_replace_report {
1353 Some(report) if self.report_in_effect(report) => report.clone(),
1354 _ => Value::Null,
1355 }
1356 }
1357
1358 fn report_in_effect(&self, report: &Value) -> bool {
1359 let to = report["to"].as_str().unwrap_or_default();
1360 report["components"]
1361 .as_array()
1362 .is_some_and(|paths| paths.iter().filter_map(Value::as_str).all(|path| self.part_at_path(path).as_deref() == Some(to)))
1363 }
1364
1365 pub fn part_at_path(&self, path: &str) -> Option<String> {
1369 let mut segments = path.split(':');
1370 let top = segments.next()?;
1371 let index = self.history.index_of(top)?;
1372 let mut part = self.history.feature_params(index)?["partName"].as_str()?.to_string();
1373 let mut library = self.history.parts_library();
1374 for id in segments {
1375 let document = &library[&part]["document"];
1376 part = document["features"]
1377 .as_array()?
1378 .iter()
1379 .find(|f| f["inputParams"]["id"].as_str() == Some(id))?["inputParams"]["partName"]
1380 .as_str()?
1381 .to_string();
1382 library = &document["partsLibrary"];
1383 }
1384 Some(part)
1385 }
1386
1387 pub fn replace_unresolved(&self) -> Value {
1390 let rows: Vec<Value> = self
1391 .history
1392 .assembly_block()
1393 .and_then(|block| block["constraints"].as_array().cloned())
1394 .unwrap_or_default()
1395 .into_iter()
1396 .filter(|entry| entry["persistentData"][REPLACE_MARKER].is_object())
1397 .map(|entry| {
1398 let marker = &entry["persistentData"][REPLACE_MARKER];
1399 json!({
1400 "id": entry["inputParams"]["id"],
1401 "type": entry["type"],
1402 "origin": marker["origin"].as_str().unwrap_or("replace"),
1403 "from": marker["from"],
1404 "to": marker["to"],
1405 "refs": marker["refs"],
1406 })
1407 })
1408 .collect();
1409 Value::Array(rows)
1410 }
1411
1412 pub fn begin_ref_select_for_replace_repair(&mut self, mate: &str, old_ref: &str) {
1418 let filter = vec!["FACE".to_string(), "EDGE".to_string()];
1419 let restore_index = self.history.rollback();
1420 self.selection_filter = SelectionFilter::from_ref_filter(&filter);
1421 self.replace_repair_pick = None;
1422 self.ref_select = Some(RefSelectState {
1423 feature_id: mate.to_string(),
1424 path: vec![old_ref.to_string()],
1425 label: format!("Replacement for {old_ref}"),
1426 filter,
1427 multiple: false,
1428 names: Vec::new(),
1429 restore_index,
1430 target: RefSelectTarget::ReplaceRepair,
1431 picks: 0,
1432 claimed: false,
1433 finish_requested: false,
1434 });
1435 self.sync_ref_select_emphasis();
1436 }
1437
1438 pub fn take_replace_repair_pick(&mut self) -> Option<(String, String, String)> {
1441 self.replace_repair_pick.take()
1442 }
1443
1444 pub fn replace_repair(&mut self, repairs: &[(String, String, String)]) -> Result<(), String> {
1450 let mut block = self
1451 .history
1452 .assembly_block()
1453 .cloned()
1454 .ok_or("replace repair: the document has no assembly")?;
1455 for (id, old_ref, new_ref) in repairs {
1456 let entry = block["constraints"]
1457 .as_array_mut()
1458 .into_iter()
1459 .flatten()
1460 .find(|entry| entry["inputParams"]["id"] == id.as_str())
1461 .ok_or_else(|| format!("replace repair: no constraint '{id}'"))?;
1462 let marker = &mut entry["persistentData"][REPLACE_MARKER];
1463 let refs = marker["refs"]
1464 .as_array_mut()
1465 .ok_or_else(|| format!("replace repair: '{id}' has nothing unresolved"))?;
1466 let before = refs.len();
1467 refs.retain(|row| row["ref"] != old_ref.as_str());
1468 if refs.len() == before {
1469 return Err(format!("replace repair: '{id}' has no unresolved ref '{old_ref}'"));
1470 }
1471 let done = refs.is_empty();
1472 let was_enabled = marker["wasEnabled"].as_bool().unwrap_or(true);
1473 for element in entry["inputParams"]["elements"].as_array_mut().into_iter().flatten() {
1474 if element == old_ref.as_str() {
1475 *element = Value::String(new_ref.clone());
1476 }
1477 }
1478 if done {
1479 if let Some(data) = entry["persistentData"].as_object_mut() {
1480 data.remove(REPLACE_MARKER);
1481 }
1482 entry["enabled"] = Value::Bool(was_enabled);
1483 }
1484 }
1485 self.history.set_assembly_block(block, true);
1486 self.rerun_history();
1487 Ok(())
1488 }
1489
1490 pub fn replace_accept_suggestions(&mut self, ids: &[String]) -> Result<usize, String> {
1494 let unresolved = self.replace_unresolved();
1495 let mut repairs = Vec::new();
1496 for mate in unresolved.as_array().into_iter().flatten() {
1497 let id = mate["id"].as_str().unwrap_or_default().to_string();
1498 if !ids.is_empty() && !ids.contains(&id) {
1499 continue;
1500 }
1501 for row in mate["refs"].as_array().into_iter().flatten() {
1502 if row["suggestion"]["preTicked"] == true {
1503 repairs.push((
1504 id.clone(),
1505 row["ref"].as_str().unwrap_or_default().to_string(),
1506 row["suggestion"]["ref"].as_str().unwrap_or_default().to_string(),
1507 ));
1508 }
1509 }
1510 }
1511 if repairs.is_empty() {
1512 return Ok(0);
1513 }
1514 self.replace_repair(&repairs)?;
1515 Ok(repairs.len())
1516 }
1517}
1518