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
169fn sub(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
170 [a[0] - b[0], a[1] - b[1], a[2] - b[2]]
171}
172
173fn dot(a: [f64; 3], b: [f64; 3]) -> f64 {
174 a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
175}
176
177fn norm(a: [f64; 3]) -> f64 {
178 dot(a, a).sqrt()
179}
180
181fn line_distance(p: [f64; 3], origin: [f64; 3], direction: [f64; 3]) -> f64 {
183 let d = sub(p, origin);
184 let along = dot(d, direction);
185 (dot(d, d) - along * along).max(0.0).sqrt()
186}
187
188const POSITION_TOLERANCE: f64 = 1e-6;
193const ANGLE_TOLERANCE: f64 = 1e-9;
194
195pub fn geometry_agrees(old: &Value, new: &Value) -> bool {
200 if old["geom"] != new["geom"] {
201 return false;
202 }
203 let (Some(p_old), Some(p_new)) = (triple(&old["point"]), triple(&new["point"])) else {
204 return false;
205 };
206 let directions = (triple(&old["direction"]), triple(&new["direction"]));
207 let radius_agrees = match (old["radius"].as_f64(), new["radius"].as_f64()) {
208 (Some(a), Some(b)) => (a - b).abs() <= POSITION_TOLERANCE * (1.0 + a.abs()),
209 (None, None) => true,
210 _ => false,
211 };
212 let scale = POSITION_TOLERANCE * (1.0 + norm(p_old));
213 match old["geom"].as_str().unwrap_or("") {
214 "plane" => match directions {
215 (Some(n_old), Some(n_new)) => {
216 dot(n_old, n_new) >= 1.0 - ANGLE_TOLERANCE
217 && dot(sub(p_old, p_new), n_new).abs() <= scale
218 }
219 _ => false,
220 },
221 "axis" | "line" => match directions {
222 (Some(d_old), Some(d_new)) => {
223 dot(d_old, d_new).abs() >= 1.0 - ANGLE_TOLERANCE
224 && line_distance(p_old, p_new, d_new) <= scale
225 && radius_agrees
226 }
227 _ => false,
228 },
229 "circle" => match directions {
230 (Some(d_old), Some(d_new)) => {
231 dot(d_old, d_new).abs() >= 1.0 - ANGLE_TOLERANCE
232 && norm(sub(p_old, p_new)) <= scale
233 && radius_agrees
234 }
235 _ => false,
236 },
237 "sphere" | "point" => norm(sub(p_old, p_new)) <= scale && radius_agrees,
238 _ => false,
239 }
240}
241
242fn suggestion_score(old: &Value, new: &Value) -> f64 {
246 let distance = match (triple(&old["point"]), triple(&new["point"])) {
247 (Some(a), Some(b)) => norm(sub(a, b)),
248 _ => f64::INFINITY,
249 };
250 let misalignment = match (triple(&old["direction"]), triple(&new["direction"])) {
251 (Some(a), Some(b)) => {
252 if old["geom"] == "plane" {
253 1.0 - dot(a, b)
254 } else {
255 1.0 - dot(a, b).abs()
256 }
257 }
258 _ => 0.0,
259 };
260 distance + 1000.0 * misalignment
261}
262
263fn local_name<'a>(component: &str, reference: &'a str) -> &'a str {
265 reference
266 .strip_prefix(component)
267 .and_then(|rest| rest.strip_prefix(':'))
268 .unwrap_or(reference)
269}
270
271#[derive(Debug, Clone, PartialEq)]
273pub enum RefOutcome {
274 Kept { split: bool },
276 Unresolved {
280 reason: &'static str,
281 suggestion: Option<Suggestion>,
282 },
283}
284
285#[derive(Debug, Clone, PartialEq)]
286pub struct Suggestion {
287 pub reference: String,
288 pub basis: &'static str,
290 pub pre_ticked: bool,
293}
294
295impl Suggestion {
296 fn to_json(&self) -> Value {
297 json!({ "ref": self.reference, "basis": self.basis, "preTicked": self.pre_ticked })
298 }
299}
300
301pub fn classify_ref<V: std::borrow::Borrow<Value>>(
306 component: &str,
307 reference: &str,
308 pre: &Value,
309 candidates: &[V],
310 old_names: &BTreeSet<String>,
311 trusted: bool,
312) -> RefOutcome {
313 let name = local_name(component, reference);
314 let by_name = candidates
315 .iter()
316 .map(Borrow::borrow)
317 .find(|candidate| candidate["ref"].as_str().map(|r| local_name(component, r)) == Some(name));
318 if trusted {
319 match by_name {
320 Some(candidate) if candidate["geom"] == pre["geom"] => {
321 let split_piece = format!("{name}_1");
322 let split = !old_names.contains(&split_piece)
323 && candidates.iter().map(Borrow::borrow).any(|c: &Value| {
324 c["ref"].as_str().map(|r| local_name(component, r)) == Some(split_piece.as_str())
325 });
326 return RefOutcome::Kept { split };
327 }
328 Some(_) => {
329 return RefOutcome::Unresolved {
330 reason: "type-changed",
331 suggestion: nearest(pre, candidates),
332 }
333 }
334 None => {
335 return RefOutcome::Unresolved { reason: "missing", suggestion: nearest(pre, candidates) }
336 }
337 }
338 }
339 let suggestion = match by_name {
340 Some(candidate) if candidate["geom"] == pre["geom"] => Some(Suggestion {
341 reference: candidate["ref"].as_str().unwrap_or_default().to_string(),
342 basis: "same-name",
343 pre_ticked: geometry_agrees(pre, candidate),
344 }),
345 _ => nearest(pre, candidates),
346 };
347 RefOutcome::Unresolved { reason: "unconfirmed", suggestion }
348}
349
350pub fn classify_vertex_ref(reference: &str, pre: &Value, post: Option<&Value>, trusted: bool) -> RefOutcome {
356 let found = post.filter(|row| row["ok"] == true);
357 let same_point = found.is_some_and(|row| geometry_agrees(pre, row));
358 if trusted && same_point {
359 return RefOutcome::Kept { split: false };
360 }
361 let suggestion = found.map(|_| Suggestion {
362 reference: reference.to_string(),
363 basis: "same-name",
364 pre_ticked: same_point,
365 });
366 let reason = match (trusted, found) {
367 (false, _) => "unconfirmed",
368 (true, Some(_)) => "moved",
369 (true, None) => "missing",
370 };
371 RefOutcome::Unresolved { reason, suggestion }
372}
373
374fn nearest<V: std::borrow::Borrow<Value>>(pre: &Value, candidates: &[V]) -> Option<Suggestion> {
377 candidates
378 .iter()
379 .map(Borrow::borrow)
380 .filter(|candidate| candidate["geom"] == pre["geom"])
381 .map(|candidate| (suggestion_score(pre, candidate), candidate))
382 .filter(|(score, _)| score.is_finite())
383 .min_by(|a, b| a.0.total_cmp(&b.0))
384 .map(|(_, candidate)| Suggestion {
385 reference: candidate["ref"].as_str().unwrap_or_default().to_string(),
386 basis: "nearest",
387 pre_ticked: geometry_agrees(pre, candidate),
388 })
389}
390
391pub fn marker_json(origin: &str, from: &str, to: &str, was_enabled: bool, refs: Vec<Value>) -> Value {
395 json!({
396 "origin": origin,
397 "from": from,
398 "to": to,
399 "wasEnabled": was_enabled,
400 "refs": refs,
401 })
402}
403
404fn unique_carrier(pre: &Value, candidates: &[&Value]) -> Option<String> {
408 let mut agreeing = candidates.iter().filter(|candidate| geometry_agrees(pre, candidate));
409 let only = agreeing.next()?;
410 if agreeing.next().is_some() {
411 return None;
412 }
413 only["ref"].as_str().map(str::to_string)
414}
415
416#[derive(Debug, Clone)]
422pub(crate) struct AffectedMate {
423 id: String,
424 constraint_type: String,
425 was_enabled: bool,
426 refs: Vec<(String, String, Value)>,
428 home: Option<(String, String)>,
432}
433
434#[derive(Debug, Clone)]
436pub(crate) struct PendingReplace {
437 generation: u64,
438 components: Vec<String>,
439 from: BTreeMap<String, String>,
440 to: String,
441 lineage: BTreeMap<String, &'static str>,
443 affected: Vec<AffectedMate>,
444 old_names: BTreeMap<String, BTreeSet<String>>,
446 notes: Vec<String>,
448 origin: &'static str,
450 signatures: BTreeMap<String, String>,
453}
454
455
456#[derive(Debug, Clone)]
458pub struct ReplaceCapture {
459 components: Vec<String>,
460 affected: Vec<AffectedMate>,
461 old_names: BTreeMap<String, BTreeSet<String>>,
462}
463
464pub fn entries_lineage(old: &Value, new: &Value, canonicalize: &dyn Fn(&str) -> String) -> Option<&'static str> {
467 let old_keys = lineage_keys(&old["document"], old["sourceKey"].as_str().unwrap_or(""), canonicalize);
468 let new_keys = lineage_keys(&new["document"], new["sourceKey"].as_str().unwrap_or(""), canonicalize);
469 shared_lineage(&old_keys, &new_keys)
470}
471
472
473
474#[derive(Default)]
477struct EntityListings {
478 by_top: BTreeMap<String, Vec<Value>>,
479 names_by_owner: BTreeMap<String, BTreeMap<String, BTreeSet<String>>>,
484 rows_by_owner: BTreeMap<String, BTreeMap<String, Vec<usize>>>,
490}
491
492fn owners_below<'a>(top: &'a str, reference: &'a str) -> impl Iterator<Item = String> + 'a {
495 let rest = reference.strip_prefix(top).and_then(|r| r.strip_prefix(':')).unwrap_or("");
496 rest.match_indices(':').map(move |(at, _)| format!("{top}:{}", &rest[..at]))
497}
498
499impl EntityListings {
500 fn listed(&mut self, top: &str) -> &[Value] {
502 self.by_top.entry(top.to_string()).or_insert_with(|| {
503 entities_of(top)
504 })
505 }
506
507 fn index(&mut self, path: &str) {
509 let top = path.split(':').next().unwrap_or(path).to_string();
510 if self.rows_by_owner.contains_key(&top) {
511 return;
512 }
513 let rows = self.listed(&top);
514 let mut index: BTreeMap<String, Vec<usize>> = BTreeMap::new();
515 index.insert(top.clone(), (0..rows.len()).collect());
516 for (at, row) in rows.iter().enumerate() {
517 let Some(reference) = row["ref"].as_str() else { continue };
518 for owner in owners_below(&top, reference) {
519 index.entry(owner).or_default().push(at);
520 }
521 }
522 self.rows_by_owner.insert(top, index);
523 }
524
525 fn under(&self, path: &str) -> Vec<&Value> {
529 let top = path.split(':').next().unwrap_or(path);
530 let (Some(rows), Some(index)) = (self.by_top.get(top), self.rows_by_owner.get(top)) else {
531 return Vec::new();
532 };
533 index.get(path).into_iter().flatten().map(|&at| &rows[at]).collect()
534 }
535
536 fn names(&mut self, path: &str) -> BTreeSet<String> {
539 let top = path.split(':').next().unwrap_or(path).to_string();
540 if !self.names_by_owner.contains_key(&top) {
541 self.listed(&top);
542 let mut index: BTreeMap<String, BTreeSet<String>> = BTreeMap::new();
543 for row in self.by_top.get(&top).into_iter().flatten() {
544 let Some(reference) = row["ref"].as_str() else { continue };
545 let Some(rest) = reference.strip_prefix(&top).and_then(|r| r.strip_prefix(':')) else {
546 index.entry(top.clone()).or_default().insert(reference.to_string());
547 continue;
548 };
549 let mut owner = top.clone();
552 index.entry(owner.clone()).or_default().insert(rest.to_string());
553 let mut tail = rest;
554 while let Some((segment, after)) = tail.split_once(':') {
555 owner = format!("{owner}:{segment}");
556 index.entry(owner.clone()).or_default().insert(after.to_string());
557 tail = after;
558 }
559 }
560 self.names_by_owner.insert(top.clone(), index);
561 }
562 self.names_by_owner[&top].get(path).cloned().unwrap_or_default()
563 }
564}
565
566fn entities_of(path: &str) -> Vec<Value> {
569 let top = path.split(':').next().unwrap_or(path);
570 let prefix = format!("{path}:");
571 parse(&brep_kernel::assembly_component_entities_json(top))
572 .as_array()
573 .cloned()
574 .unwrap_or_default()
575 .into_iter()
576 .filter(|row| top == path || row["ref"].as_str().is_some_and(|r| r.starts_with(&prefix)))
577 .collect()
578}
579
580fn document_in_chain_mut<'a>(document: &'a mut Value, chain: &[String]) -> Option<&'a mut Value> {
583 let mut current = document;
584 for part in chain {
585 current = current.get_mut("partsLibrary")?.get_mut(part.as_str())?.get_mut("document")?;
586 }
587 Some(current)
588}
589
590fn owner_among<'a>(ids: &'a [String], reference: &str) -> Option<&'a String> {
592 ids.iter().find(|id| {
593 reference == id.as_str()
594 || reference
595 .strip_prefix(id.as_str())
596 .is_some_and(|rest| rest.starts_with(':'))
597 })
598}
599
600pub fn follow_pattern_seeds(
608 document: &Value,
609 from: &BTreeMap<String, String>,
610 to: &str,
611 instances: &[String],
612) -> Vec<String> {
613 let mut notes = Vec::new();
614 for feature in document["features"].as_array().into_iter().flatten() {
615 if feature["type"] != "APAT" {
616 continue;
617 }
618 let pattern = feature["inputParams"]["id"].as_str().unwrap_or_default().to_string();
619 let mut seeded = BTreeSet::new();
620 for name in feature["inputParams"]["seeds"].as_array().into_iter().flatten().filter_map(Value::as_str) {
621 let owner = name.split(':').next().unwrap_or(name).to_string();
622 if from.contains_key(&owner) {
623 seeded.insert(owner);
624 }
625 }
626 for owner in seeded {
627 let count = instances
628 .iter()
629 .filter_map(|id| brep_kernel::pattern_component_instance(id))
630 .filter(|(p, s, _)| *p == pattern && *s == owner)
631 .count();
632 notes.push(format!("{pattern}: its {count} instance(s) of {owner} follow the seed and now show '{to}'"));
633 }
634 }
635 notes
636}
637
638fn parse(json: &str) -> Value {
639 serde_json::from_str(json).unwrap_or(Value::Null)
640}
641
642impl EngineState {
643 pub fn replace_components(
650 &mut self,
651 component_ids: &[String],
652 insert: ComponentInsert<'_>,
653 canonicalize: &dyn Fn(&str) -> String,
654 ) -> Result<String, String> {
655 if component_ids.is_empty() {
656 return Err("replace: no component given".into());
657 }
658 let mut ids: Vec<String> = Vec::new();
662 let mut instance_notes = Vec::new();
663 for id in component_ids {
664 let id = match brep_kernel::pattern_component_instance(id) {
665 Some((pattern, seed, n)) => {
666 instance_notes.push(format!(
667 "{id} is instance {n} of {pattern} and follows its seed {seed}: the seed was replaced"
668 ));
669 seed
670 }
671 None => id.clone(),
672 };
673 if !ids.contains(&id) {
674 ids.push(id);
675 }
676 }
677 let component_ids = ids.as_slice();
678 let mut from = BTreeMap::new();
679 for id in component_ids {
680 let info = self
681 .component_info(id)
682 .ok_or_else(|| format!("replace: '{id}' is not an assembly component"))?;
683 from.insert(id.clone(), info.part_name);
684 }
685 let capture = self.capture_replace(component_ids);
686
687 let old_library = self.history.parts_library().clone();
689 let to = self.resolve_component_insert(insert)?;
690 let library = parse(&brep_kernel::parts_library_json());
691 let new_entry = &library[&to];
692 let mut lineage = BTreeMap::new();
693 for (id, part) in &from {
694 if part == &to {
695 continue;
696 }
697 let old_entry = if old_library[part].is_object() { &old_library[part] } else { &library[part] };
698 if let Some(word) = entries_lineage(old_entry, new_entry, canonicalize) {
699 lineage.insert(id.clone(), word);
700 }
701 }
702 if from.values().all(|part| part == &to) {
703 return Err(format!("replace: the component already shows '{to}'"));
704 }
705
706 let mut document = parse(&self.history_request_json());
710 document["partsLibrary"] = library.clone();
711 for feature in document["features"].as_array_mut().into_iter().flatten() {
712 let id = feature["inputParams"]["id"].as_str().unwrap_or_default().to_string();
713 if from.contains_key(&id) {
714 feature["inputParams"]["partName"] = Value::String(to.clone());
715 }
716 }
717 let mut notes = instance_notes;
718 notes.extend(follow_pattern_seeds(&document, &from, &to, &self.pattern_instance_ids()));
719 self.commit_replace(capture, document, from, to.clone(), lineage, notes)?;
720 Ok(to)
721 }
722
723 pub fn capture_replace(&mut self, paths: &[String]) -> ReplaceCapture {
730 self.capture_replace_listed(paths, &mut EntityListings::default())
731 }
732
733 fn capture_replace_listed(&mut self, paths: &[String], listings: &mut EntityListings) -> ReplaceCapture {
737 self.ensure_assembly_session();
738 let state = parse(&brep_kernel::assembly_state_json());
739 let mut affected = Vec::new();
740 for entry in state["constraints"].as_array().into_iter().flatten() {
741 let refs: Vec<(String, String)> = entry["inputParams"]["elements"]
742 .as_array()
743 .into_iter()
744 .flatten()
745 .filter_map(Value::as_str)
746 .filter_map(|r| owner_among(paths, r).map(|owner| (owner.clone(), r.to_string())))
747 .collect();
748 if refs.is_empty() {
749 continue;
750 }
751 let enabled = entry["enabled"].as_bool().unwrap_or(true);
752 let status = entry["persistentData"]["status"].as_str().unwrap_or("");
753 if enabled && BROKEN.contains(&status) {
754 continue; }
756 let names: Vec<&String> = refs.iter().map(|(_, r)| r).collect();
757 let rows = parse(&brep_kernel::assembly_resolve_refs_json(
758 &serde_json::to_string(&names).unwrap_or_default(),
759 ));
760 let rows = rows.as_array().cloned().unwrap_or_default();
761 affected.push(AffectedMate {
762 id: entry["inputParams"]["id"].as_str().unwrap_or_default().to_string(),
763 constraint_type: entry["type"].as_str().unwrap_or_default().to_string(),
764 was_enabled: enabled,
765 refs: refs
766 .into_iter()
767 .zip(rows.into_iter().chain(std::iter::repeat(Value::Null)))
768 .map(|((owner, r), row)| (owner, r, row))
769 .collect(),
770 home: None,
771 });
772 }
773 let mut old_names = BTreeMap::new();
774 for path in paths {
775 old_names.insert(path.clone(), listings.names(path));
776 }
777 ReplaceCapture { components: paths.to_vec(), affected, old_names }
778 }
779
780 pub fn capture_part_edit(&mut self, paths: &[String]) -> ReplaceCapture {
787 let mut listings = EntityListings::default();
791 let mut capture = self.capture_replace_listed(paths, &mut listings);
792 let mut seen: BTreeSet<Vec<String>> = BTreeSet::new();
795 let mut stack: Vec<String> = paths.to_vec();
796 while let Some(path) = stack.pop() {
797 let Some(chain) = self.part_chain(&path) else { continue };
798 if !seen.insert(chain.clone()) {
799 continue;
800 }
801 let Some(document) = self.document_in_chain(&chain) else { continue };
802 for feature in document["features"].as_array().into_iter().flatten() {
803 if super::components::is_acomp_feature_type(feature["type"].as_str().unwrap_or("")) {
804 if let Some(id) = feature["inputParams"]["id"].as_str() {
805 stack.push(format!("{path}:{id}"));
806 }
807 }
808 }
809 for entry in document["assembly"]["constraints"].as_array().into_iter().flatten() {
810 let inner_id = entry["inputParams"]["id"].as_str().unwrap_or_default();
811 let refs: Vec<(String, String)> = entry["inputParams"]["elements"]
812 .as_array()
813 .into_iter()
814 .flatten()
815 .filter_map(Value::as_str)
816 .map(|r| {
817 let occurrence = r.split(':').next().unwrap_or(r);
818 (format!("{path}:{occurrence}"), format!("{path}:{r}"))
819 })
820 .collect();
821 if inner_id.is_empty() || refs.is_empty() {
822 continue;
823 }
824 let enabled = entry["enabled"].as_bool().unwrap_or(true);
825 let status = entry["persistentData"]["status"].as_str().unwrap_or("");
826 if enabled && BROKEN.contains(&status) {
827 continue; }
829 let names: Vec<&String> = refs.iter().map(|(_, r)| r).collect();
830 let rows = parse(&brep_kernel::assembly_resolve_refs_json(
831 &serde_json::to_string(&names).unwrap_or_default(),
832 ));
833 let rows = rows.as_array().cloned().unwrap_or_default();
834 if rows.len() != refs.len() || rows.iter().any(|row| row["ok"] != true) {
838 continue;
839 }
840 for (owner, _) in &refs {
841 if !capture.old_names.contains_key(owner) {
842 let names = listings.names(owner);
843 capture.old_names.insert(owner.clone(), names);
844 }
845 }
846 capture.affected.push(AffectedMate {
847 id: format!("{path}:{inner_id}"),
848 constraint_type: entry["type"].as_str().unwrap_or_default().to_string(),
849 was_enabled: enabled,
850 refs: refs
851 .into_iter()
852 .zip(rows.into_iter().chain(std::iter::repeat(Value::Null)))
853 .map(|((owner, r), row)| (owner, r, row))
854 .collect(),
855 home: Some((path.clone(), inner_id.to_string())),
856 });
857 }
858 }
859 capture
860 }
861
862 fn write_back_inner_mates(
869 &mut self,
870 affected: &[AffectedMate],
871 reattached: &[(String, String, String)],
872 markers: &BTreeMap<String, Value>,
873 ) {
874 let touched: Vec<&AffectedMate> = affected
875 .iter()
876 .filter(|mate| mate.home.is_some())
877 .filter(|mate| markers.contains_key(&mate.id) || reattached.iter().any(|(id, _, _)| *id == mate.id))
878 .collect();
879 if touched.is_empty() {
880 return;
881 }
882 let Ok(mut document) = serde_json::from_str::<Value>(&self.history.request_json()) else { return };
883 let mut chains: BTreeSet<Vec<String>> = BTreeSet::new();
884 for mate in touched {
885 let (home, inner_id) = mate.home.as_ref().expect("filtered");
886 let Some(chain) = self.part_chain(home) else { continue };
887 let Some(inner) = document_in_chain_mut(&mut document, &chain) else { continue };
888 let prefix = format!("{home}:");
889 let local = |reference: &str| reference.strip_prefix(&prefix).unwrap_or(reference).to_string();
890 for entry in inner["assembly"]["constraints"].as_array_mut().into_iter().flatten() {
891 if entry["inputParams"]["id"].as_str() != Some(inner_id.as_str()) {
892 continue;
893 }
894 for (_, old_ref, new_ref) in reattached.iter().filter(|(id, _, _)| *id == mate.id) {
895 for element in entry["inputParams"]["elements"].as_array_mut().into_iter().flatten() {
896 if element == local(old_ref).as_str() {
897 *element = Value::String(local(new_ref));
898 }
899 }
900 }
901 if let Some(marker) = markers.get(&mate.id) {
902 let mut marker = marker.clone();
903 for row in marker["refs"].as_array_mut().into_iter().flatten() {
904 if let Some(reference) = row["ref"].as_str().map(|r| local(r)) {
905 row["ref"] = Value::String(reference);
906 }
907 if let Some(reference) = row["suggestion"]["ref"].as_str().map(|r| local(r)) {
908 row["suggestion"]["ref"] = Value::String(reference);
909 }
910 }
911 entry["enabled"] = Value::Bool(false);
912 if !entry["persistentData"].is_object() {
913 entry["persistentData"] = json!({});
914 }
915 entry["persistentData"][REPLACE_MARKER] = marker;
916 }
917 }
918 chains.insert(chain);
919 }
920 let mut prefixes: BTreeSet<Vec<String>> = BTreeSet::new();
922 for chain in &chains {
923 for depth in 1..=chain.len() {
924 prefixes.insert(chain[..depth].to_vec());
925 }
926 }
927 let mut prefixes: Vec<Vec<String>> = prefixes.into_iter().collect();
928 prefixes.sort_by_key(|chain| std::cmp::Reverse(chain.len()));
929 for chain in prefixes {
930 let (last, parent) = chain.split_last().expect("non-empty");
931 let library = if parent.is_empty() {
932 Some(&mut document["partsLibrary"])
933 } else {
934 document_in_chain_mut(&mut document, parent).map(|doc| &mut doc["partsLibrary"])
935 };
936 let Some(entry) = library.and_then(|library| library.get_mut(last.as_str())) else { continue };
937 let signature = super::assembly_ops::document_signature(&entry["document"].to_string());
938 entry["sourceSignature"] = Value::String(signature);
939 entry["snapshot"] = Value::String(String::new());
940 if let Some(object) = entry.as_object_mut() {
941 object.remove("nestedRows");
942 }
943 }
944 let written = serde_json::from_value::<brep_kernel::PartsLibraryMap>(document["partsLibrary"].clone())
947 .map_err(|error| error.to_string())
948 .and_then(|library| self.history.adopt_document(&document.to_string()).map(|()| library));
949 match written {
950 Ok(library) => {
951 brep_kernel::install_parts_library(&library);
952 }
953 Err(error) => {
954 self.push_notice(format!("part edit: the sub-assembly mates could not be written: {error}"));
955 }
956 }
957 }
958
959 fn document_in_chain(&self, chain: &[String]) -> Option<&Value> {
962 let mut library = self.history.parts_library();
963 let mut document = None;
964 for part in chain {
965 let entry = library.get(part.as_str())?;
966 document = Some(&entry["document"]);
967 library = &entry["document"]["partsLibrary"];
968 }
969 document
970 }
971
972 fn part_chain(&self, path: &str) -> Option<Vec<String>> {
976 let mut segments = path.split(':');
977 let top = segments.next()?;
978 let index = self.history.index_of(top)?;
979 let mut part = self.history.feature_params(index)?["partName"].as_str()?.to_string();
980 let mut chain = vec![part.clone()];
981 let mut library = self.history.parts_library();
982 for id in segments {
983 let document = &library.get(part.as_str())?["document"];
984 part = document["features"]
985 .as_array()?
986 .iter()
987 .find(|f| f["inputParams"]["id"].as_str() == Some(id))?["inputParams"]["partName"]
988 .as_str()?
989 .to_string();
990 chain.push(part.clone());
991 library = &document["partsLibrary"];
992 }
993 Some(chain)
994 }
995
996 pub fn commit_replace(
1002 &mut self,
1003 capture: ReplaceCapture,
1004 mut document: Value,
1005 from: BTreeMap<String, String>,
1006 to: String,
1007 lineage: BTreeMap<String, &'static str>,
1008 notes: Vec<String>,
1009 ) -> Result<(), String> {
1010 let untrusted: BTreeSet<&String> =
1011 capture.components.iter().filter(|id| !lineage.contains_key(*id)).collect();
1012 if let Some(constraints) = document["assembly"]["constraints"].as_array_mut() {
1013 for entry in constraints.iter_mut() {
1014 let id = entry["inputParams"]["id"].as_str().unwrap_or_default();
1015 let Some(mate) = capture.affected.iter().find(|m| m.id == id) else { continue };
1016 if mate.refs.iter().any(|(owner, _, _)| untrusted.contains(owner)) {
1017 entry["enabled"] = Value::Bool(false);
1018 }
1019 }
1020 }
1021 self.edit_document(document)?;
1022 self.replace_pending = Some(PendingReplace {
1023 generation: self.run_generation,
1024 components: capture.components,
1025 from,
1026 to,
1027 lineage,
1028 affected: capture.affected,
1029 old_names: capture.old_names,
1030 notes,
1031 origin: "replace",
1032 signatures: BTreeMap::new(),
1033 });
1034 self.last_replace_report = None;
1035 if !self.run_pending() {
1036 self.finish_component_replace();
1037 }
1038 Ok(())
1039 }
1040
1041 pub fn commit_part_edit(&mut self, capture: ReplaceCapture, document: Value, parts: &[String]) -> Result<(), String> {
1048 self.finish_component_replace();
1051 if self.replace_pending.is_some() {
1052 return Err("a component replace is still landing; try again in a moment".into());
1053 }
1054 let signatures: BTreeMap<String, String> = parts
1055 .iter()
1056 .map(|part| {
1057 let signature = document["partsLibrary"][part]["sourceSignature"].as_str().unwrap_or_default();
1058 (part.clone(), signature.to_string())
1059 })
1060 .collect();
1061 let from: BTreeMap<String, String> = capture
1062 .components
1063 .iter()
1064 .filter_map(|path| Some((path.clone(), self.part_at_path(path)?)))
1065 .collect();
1066 self.edit_document(document)?;
1067 if capture.affected.is_empty() {
1068 return Ok(());
1069 }
1070 self.replace_pending = Some(PendingReplace {
1071 generation: self.run_generation,
1072 lineage: capture.components.iter().map(|path| (path.clone(), "edit")).collect(),
1073 components: capture.components,
1074 to: parts.join(", "),
1075 from,
1076 affected: capture.affected,
1077 old_names: capture.old_names,
1078 notes: Vec::new(),
1079 origin: "edit",
1080 signatures,
1081 });
1082 self.last_replace_report = None;
1083 if !self.run_pending() {
1084 self.finish_component_replace();
1085 }
1086 Ok(())
1087 }
1088
1089 pub fn components_of_parts(&self, parts: &[String]) -> Vec<String> {
1091 self.history
1092 .features()
1093 .iter()
1094 .filter(|feature| super::components::is_acomp_feature_type(feature["type"].as_str().unwrap_or("")))
1095 .filter(|feature| {
1096 feature["inputParams"]["partName"].as_str().is_some_and(|part| parts.iter().any(|p| p == part))
1097 })
1098 .filter_map(|feature| feature["inputParams"]["id"].as_str().map(str::to_string))
1099 .collect()
1100 }
1101
1102 pub(crate) fn finish_component_replace(&mut self) {
1106 let Some(pending) = self.replace_pending.as_ref() else { return };
1107 if self.run_pending() || self.applied_generation < pending.generation {
1108 return;
1109 }
1110 let pending = self.replace_pending.take().expect("checked above");
1111 let landed = if pending.origin == "edit" {
1116 pending.signatures.iter().all(|(part, signature)| {
1117 self.history.parts_library()[part.as_str()]["sourceSignature"].as_str() == Some(signature.as_str())
1118 })
1119 } else {
1120 pending.components.iter().all(|path| self.part_at_path(path).as_deref() == Some(pending.to.as_str()))
1121 };
1122 if !landed {
1123 self.last_replace_report = None;
1124 return;
1125 }
1126 self.ensure_assembly_session();
1127 let statuses = parse(&brep_kernel::assembly_statuses_json());
1128 let nested = parse(&brep_kernel::assembly_dof_json())["nestedConstraints"].clone();
1130 let status_of = |id: &str| -> String {
1131 statuses
1132 .as_array()
1133 .into_iter()
1134 .flatten()
1135 .chain(nested.as_array().into_iter().flatten())
1136 .find(|row| row["id"] == id)
1137 .and_then(|row| row["status"].as_str())
1138 .unwrap_or("")
1139 .to_string()
1140 };
1141 let vertex_refs: Vec<&String> = pending
1144 .affected
1145 .iter()
1146 .flat_map(|mate| mate.refs.iter().map(|(_, r, _)| r))
1147 .filter(|r| r.contains('@'))
1148 .collect();
1149 let post_vertex: BTreeMap<String, Value> = if vertex_refs.is_empty() {
1150 BTreeMap::new()
1151 } else {
1152 parse(&brep_kernel::assembly_resolve_refs_json(&serde_json::to_string(&vertex_refs).unwrap_or_default()))
1153 .as_array()
1154 .cloned()
1155 .unwrap_or_default()
1156 .into_iter()
1157 .filter_map(|row| Some((row["ref"].as_str()?.to_string(), row)))
1158 .collect()
1159 };
1160 let mut listings = EntityListings::default();
1165 let owners: BTreeSet<&String> = pending
1166 .affected
1167 .iter()
1168 .flat_map(|mate| mate.refs.iter().map(move |(owner, _, _)| (mate, owner)))
1169 .filter(|(mate, owner)| mate.home.is_some() || pending.components.contains(owner))
1170 .map(|(_, owner)| owner)
1171 .collect();
1172 for owner in &owners {
1173 listings.index(owner);
1174 }
1175 let entities: BTreeMap<&String, Vec<&Value>> = owners.iter().map(|owner| (*owner, listings.under(owner))).collect();
1176
1177 let mut kept = Vec::new();
1178 let mut unresolved = Vec::new();
1179 let mut notes = pending.notes.clone();
1180 let mut markers: BTreeMap<String, Value> = BTreeMap::new();
1181 let mut reattached: Vec<(String, String, String)> = Vec::new();
1183 for mate in &pending.affected {
1184 let mut ref_rows = Vec::new();
1185 for (owner, reference, pre) in &mate.refs {
1186 let trusted = pending.lineage.contains_key(owner)
1189 || (mate.home.is_some() && owner_among(&pending.components, owner).is_some_and(|top| pending.lineage.contains_key(top)));
1190 let outcome = if pre["ok"] != true {
1191 RefOutcome::Unresolved { reason: "missing", suggestion: None }
1195 } else if reference.contains('@') {
1196 classify_vertex_ref(reference, pre, post_vertex.get(reference.as_str()), trusted)
1197 } else {
1198 classify_ref(
1199 owner,
1200 reference,
1201 pre,
1202 entities.get(owner).map(Vec::as_slice).unwrap_or(&[]),
1203 pending.old_names.get(owner).unwrap_or(&BTreeSet::new()),
1204 trusted,
1205 )
1206 };
1207 match outcome {
1208 RefOutcome::Kept { split } => {
1209 if split {
1210 notes.push(format!(
1211 "{}: '{}' was split by the new part; the mate keeps the piece that kept the name (same carrier)",
1212 mate.id, reference
1213 ));
1214 }
1215 }
1216 RefOutcome::Unresolved { reason, suggestion } => {
1217 let only = (pending.origin == "edit" && !reference.contains('@'))
1218 .then(|| unique_carrier(pre, entities.get(owner).map(Vec::as_slice).unwrap_or(&[])))
1219 .flatten();
1220 match only {
1221 Some(new_ref) => reattached.push((mate.id.clone(), reference.clone(), new_ref)),
1222 None => ref_rows.push(json!({
1223 "ref": reference,
1224 "reason": reason,
1225 "suggestion": suggestion.as_ref().map(Suggestion::to_json),
1226 })),
1227 }
1228 }
1229 }
1230 }
1231 let status = status_of(&mate.id);
1235 let mate_reattached = reattached.iter().any(|(id, _, _)| *id == mate.id);
1238 if ref_rows.is_empty() && !mate_reattached && mate.was_enabled && BROKEN.contains(&status.as_str()) {
1239 for (_, reference, _) in &mate.refs {
1240 ref_rows.push(json!({ "ref": reference, "reason": "no-longer-maps", "suggestion": null }));
1241 }
1242 }
1243 if ref_rows.is_empty() {
1244 kept.push(json!({ "id": mate.id, "type": mate.constraint_type }));
1245 continue;
1246 }
1247 let owner = &mate.refs[0].0;
1248 let marker = marker_json(
1249 pending.origin,
1250 &pending.from.get(owner).cloned().or_else(|| self.part_at_path(owner)).unwrap_or_default(),
1251 &pending.to,
1252 mate.was_enabled,
1253 ref_rows,
1254 );
1255 unresolved.push(json!({
1256 "id": mate.id,
1257 "type": mate.constraint_type,
1258 "refs": marker["refs"],
1259 }));
1260 markers.insert(mate.id.clone(), marker);
1261 }
1262
1263 if !markers.is_empty() || !reattached.is_empty() {
1264 let mut block = self.history.assembly_block().cloned().unwrap_or(Value::Null);
1265 let mut disabled_now = false;
1266 for entry in block["constraints"].as_array_mut().into_iter().flatten() {
1267 let id = entry["inputParams"]["id"].as_str().unwrap_or_default().to_string();
1268 for (mate, old_ref, new_ref) in &reattached {
1269 if *mate != id {
1270 continue;
1271 }
1272 for element in entry["inputParams"]["elements"].as_array_mut().into_iter().flatten() {
1273 if element == old_ref.as_str() {
1274 *element = Value::String(new_ref.clone());
1275 }
1276 }
1277 }
1278 let Some(marker) = markers.get(&id) else { continue };
1279 if entry["enabled"] != false {
1280 entry["enabled"] = Value::Bool(false);
1281 disabled_now = true;
1282 }
1283 if !entry["persistentData"].is_object() {
1284 entry["persistentData"] = json!({});
1285 }
1286 entry["persistentData"][REPLACE_MARKER] = marker.clone();
1287 }
1288 self.history.set_assembly_block(block, false);
1289 self.write_back_inner_mates(&pending.affected, &reattached, &markers);
1290 let _ = disabled_now;
1294 self.rerun_history();
1295 }
1296
1297 if pending.origin == "edit" && (!reattached.is_empty() || !unresolved.is_empty()) {
1300 let mut lines = Vec::new();
1301 if !reattached.is_empty() {
1302 let moves: Vec<String> = reattached.iter().map(|(id, from, to)| format!("{id} {from} → {to}")).collect();
1303 lines.push(format!("re-attached {}", moves.join(", ")));
1304 }
1305 if !unresolved.is_empty() {
1306 let ids: Vec<&str> = unresolved.iter().filter_map(|row| row["id"].as_str()).collect();
1307 lines.push(format!(
1308 "held for Repair (Constraints panel): {} — the face or edge it used is gone and no single one replaces it",
1309 ids.join(", ")
1310 ));
1311 }
1312 let severity = if unresolved.is_empty() { NoticeSeverity::Info } else { NoticeSeverity::Warning };
1313 self.push_notice_as(severity, format!("Mates after editing {}: {}.", pending.to, lines.join("; ")));
1314 }
1315 let report = json!({
1316 "ok": true,
1317 "origin": pending.origin,
1318 "reattached": reattached.iter().map(|(id, from, to)| json!({ "id": id, "from": from, "to": to })).collect::<Vec<_>>(),
1319 "components": pending.components,
1320 "from": pending.from,
1321 "to": pending.to,
1322 "lineage": pending.components.iter().map(|id| {
1323 (id.clone(), Value::String(pending.lineage.get(id).copied().unwrap_or("none").to_string()))
1324 }).collect::<serde_json::Map<String, Value>>(),
1325 "kept": kept,
1326 "unresolved": unresolved,
1327 "notes": notes,
1328 });
1329 self.last_replace_report = Some(report);
1330 }
1331
1332 pub fn occurrence_entities(&mut self, path: &str) -> Vec<Value> {
1336 self.ensure_assembly_session();
1337 entities_of(path)
1338 }
1339
1340 pub fn resolve_refs(&mut self, refs: &[String]) -> Vec<Value> {
1343 self.ensure_assembly_session();
1344 parse(&brep_kernel::assembly_resolve_refs_json(&serde_json::to_string(refs).unwrap_or_default()))
1345 .as_array()
1346 .cloned()
1347 .unwrap_or_default()
1348 }
1349
1350 pub fn replace_report(&mut self) -> Value {
1355 self.finish_component_replace();
1356 match &self.last_replace_report {
1359 Some(report) if self.report_in_effect(report) => report.clone(),
1360 _ => Value::Null,
1361 }
1362 }
1363
1364 fn report_in_effect(&self, report: &Value) -> bool {
1365 let to = report["to"].as_str().unwrap_or_default();
1366 report["components"]
1367 .as_array()
1368 .is_some_and(|paths| paths.iter().filter_map(Value::as_str).all(|path| self.part_at_path(path).as_deref() == Some(to)))
1369 }
1370
1371 pub fn part_at_path(&self, path: &str) -> Option<String> {
1375 let mut segments = path.split(':');
1376 let top = segments.next()?;
1377 let index = self.history.index_of(top)?;
1378 let mut part = self.history.feature_params(index)?["partName"].as_str()?.to_string();
1379 let mut library = self.history.parts_library();
1380 for id in segments {
1381 let document = &library[&part]["document"];
1382 part = document["features"]
1383 .as_array()?
1384 .iter()
1385 .find(|f| f["inputParams"]["id"].as_str() == Some(id))?["inputParams"]["partName"]
1386 .as_str()?
1387 .to_string();
1388 library = &document["partsLibrary"];
1389 }
1390 Some(part)
1391 }
1392
1393 pub fn replace_unresolved(&self) -> Value {
1396 let rows: Vec<Value> = self
1397 .history
1398 .assembly_block()
1399 .and_then(|block| block["constraints"].as_array().cloned())
1400 .unwrap_or_default()
1401 .into_iter()
1402 .filter(|entry| entry["persistentData"][REPLACE_MARKER].is_object())
1403 .map(|entry| {
1404 let marker = &entry["persistentData"][REPLACE_MARKER];
1405 json!({
1406 "id": entry["inputParams"]["id"],
1407 "type": entry["type"],
1408 "origin": marker["origin"].as_str().unwrap_or("replace"),
1409 "from": marker["from"],
1410 "to": marker["to"],
1411 "refs": marker["refs"],
1412 })
1413 })
1414 .collect();
1415 Value::Array(rows)
1416 }
1417
1418 pub fn replace_repair(&mut self, repairs: &[(String, String, String)]) -> Result<(), String> {
1424 let mut block = self
1425 .history
1426 .assembly_block()
1427 .cloned()
1428 .ok_or("replace repair: the document has no assembly")?;
1429 for (id, old_ref, new_ref) in repairs {
1430 let entry = block["constraints"]
1431 .as_array_mut()
1432 .into_iter()
1433 .flatten()
1434 .find(|entry| entry["inputParams"]["id"] == id.as_str())
1435 .ok_or_else(|| format!("replace repair: no constraint '{id}'"))?;
1436 let marker = &mut entry["persistentData"][REPLACE_MARKER];
1437 let refs = marker["refs"]
1438 .as_array_mut()
1439 .ok_or_else(|| format!("replace repair: '{id}' has nothing unresolved"))?;
1440 let before = refs.len();
1441 refs.retain(|row| row["ref"] != old_ref.as_str());
1442 if refs.len() == before {
1443 return Err(format!("replace repair: '{id}' has no unresolved ref '{old_ref}'"));
1444 }
1445 let done = refs.is_empty();
1446 let was_enabled = marker["wasEnabled"].as_bool().unwrap_or(true);
1447 for element in entry["inputParams"]["elements"].as_array_mut().into_iter().flatten() {
1448 if element == old_ref.as_str() {
1449 *element = Value::String(new_ref.clone());
1450 }
1451 }
1452 if done {
1453 if let Some(data) = entry["persistentData"].as_object_mut() {
1454 data.remove(REPLACE_MARKER);
1455 }
1456 entry["enabled"] = Value::Bool(was_enabled);
1457 }
1458 }
1459 self.history.set_assembly_block(block, true);
1460 self.rerun_history();
1461 Ok(())
1462 }
1463
1464 pub fn replace_accept_suggestions(&mut self, ids: &[String]) -> Result<usize, String> {
1468 let unresolved = self.replace_unresolved();
1469 let mut repairs = Vec::new();
1470 for mate in unresolved.as_array().into_iter().flatten() {
1471 let id = mate["id"].as_str().unwrap_or_default().to_string();
1472 if !ids.is_empty() && !ids.contains(&id) {
1473 continue;
1474 }
1475 for row in mate["refs"].as_array().into_iter().flatten() {
1476 if row["suggestion"]["preTicked"] == true {
1477 repairs.push((
1478 id.clone(),
1479 row["ref"].as_str().unwrap_or_default().to_string(),
1480 row["suggestion"]["ref"].as_str().unwrap_or_default().to_string(),
1481 ));
1482 }
1483 }
1484 }
1485 if repairs.is_empty() {
1486 return Ok(0);
1487 }
1488 self.replace_repair(&repairs)?;
1489 Ok(repairs.len())
1490 }
1491}
1492