1use serde::{Deserialize, Serialize};
72use std::collections::{BTreeMap, BTreeSet};
73
74use serde_json::Value;
75
76use crate::feature_pipeline::ports::{self, PortDeclaration, UnitProblem, PORTS_BLOCK};
77use crate::feature_pipeline::wire_harness::WireHarnessEndpoint;
78use crate::feature_pipeline::{component, PortKind, PortRecord};
79
80pub const SYMBOL_BLOCK: &str = "symbol";
82pub const PADS_BLOCK: &str = "pads";
84pub const DEFAULT_PORT_NAME: &str = "Pins";
88pub const DEFAULT_PORT_PURPOSE: &str = "pcb";
90
91const PIN_PITCH: i64 = 2540;
93const FIRST_PIN_TIP: (i64, i64) = (-7620, 0);
96
97#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
103pub struct DeclaredPoint {
104 pub port: String,
106 pub point: String,
108 pub purpose: String,
110}
111
112impl DeclaredPoint {
113 pub fn address(&self) -> String {
115 ports::address(&self.port, &self.point)
116 }
117}
118
119#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
121pub struct DeclaredPin {
122 pub unit: u32,
125 pub label: String,
127}
128
129pub fn declared_points(document: &Value) -> Vec<DeclaredPoint> {
133 declarations(document)
134 .into_iter()
135 .flat_map(|group| {
136 let (port, purpose) = (group.name.trim().to_string(), group.purpose);
137 group.points.into_iter().map(move |point| DeclaredPoint {
138 port: port.clone(),
139 point: point.name.trim().to_string(),
140 purpose: purpose.clone(),
141 })
142 })
143 .collect()
144}
145
146pub fn declarations(document: &Value) -> Vec<PortDeclaration> {
149 document
150 .get(PORTS_BLOCK)
151 .cloned()
152 .and_then(|block| serde_json::from_value::<Vec<PortDeclaration>>(block).ok())
153 .unwrap_or_default()
154}
155
156fn port_blocks(document: &Value) -> &[Value] {
157 document
158 .get(PORTS_BLOCK)
159 .and_then(Value::as_array)
160 .map(Vec::as_slice)
161 .unwrap_or(&[])
162}
163
164pub fn pins(document: &Value) -> Option<Vec<DeclaredPin>> {
167 let symbol = document.get(SYMBOL_BLOCK).filter(|block| !block.is_null())?;
168 Some(
169 symbol
170 .get("pins")
171 .and_then(Value::as_array)
172 .map(|pins| {
173 pins.iter()
174 .map(|pin| DeclaredPin {
175 unit: pin.get("unit").and_then(Value::as_u64).unwrap_or(0).max(1) as u32,
176 label: label_of(pin),
177 })
178 .collect()
179 })
180 .unwrap_or_default(),
181 )
182}
183
184pub fn pin_labels(document: &Value) -> Option<Vec<String>> {
187 Some(pins(document)?.into_iter().map(|pin| pin.label).collect())
188}
189
190pub fn unit_count(document: &Value) -> u32 {
193 document
194 .get(SYMBOL_BLOCK)
195 .and_then(|symbol| symbol.get("unit_count"))
196 .and_then(Value::as_u64)
197 .unwrap_or(0)
198 .max(1) as u32
199}
200
201pub fn unit_groups(document: &Value) -> (BTreeMap<u32, String>, Vec<UnitProblem>) {
209 let mut declarations = declarations(document);
210 if declarations.is_empty() {
211 declarations.push(PortDeclaration {
212 name: DEFAULT_PORT_NAME.to_string(),
213 purpose: DEFAULT_PORT_PURPOSE.to_string(),
214 ..PortDeclaration::default()
215 });
216 }
217 let (map, problems) = ports::unit_map(
218 &declarations,
219 unit_count(document),
220 pins(document).is_some(),
221 );
222 let named = map
223 .into_iter()
224 .map(|(unit, index)| (unit, declarations[index].name.trim().to_string()))
225 .collect();
226 (named, problems)
227}
228
229pub fn port_group_for_unit(document: &Value, unit: u32) -> Option<String> {
231 unit_groups(document).0.get(&unit.max(1)).cloned()
232}
233
234fn pad_numbers(document: &Value) -> Vec<String> {
236 document
237 .get(PADS_BLOCK)
238 .and_then(|block| block.get("pads"))
239 .and_then(Value::as_array)
240 .map(|pads| pads.iter().map(|pad| label_of(pad)).collect())
241 .unwrap_or_default()
242}
243
244fn label_of(item: &Value) -> String {
246 item.get("number").and_then(Value::as_str).unwrap_or_default().to_string()
247}
248
249fn pins_of_unit(pins: &[DeclaredPin], unit: u32) -> Vec<usize> {
255 pins.iter()
256 .enumerate()
257 .filter(|(_, pin)| pin.unit == unit)
258 .map(|(index, _)| index)
259 .collect()
260}
261
262fn points_of_group(points: &[DeclaredPoint], group: &str) -> Vec<usize> {
264 points
265 .iter()
266 .enumerate()
267 .filter(|(_, point)| point.port == group)
268 .map(|(index, _)| index)
269 .collect()
270}
271
272fn labels_at(pins: &[DeclaredPin], indices: &[usize]) -> Vec<String> {
273 indices.iter().map(|index| pins[*index].label.clone()).collect()
274}
275
276fn names_at(points: &[DeclaredPoint], indices: &[usize]) -> Vec<String> {
277 indices.iter().map(|index| points[*index].point.clone()).collect()
278}
279
280fn pin_list_problem(labels: &[String]) -> Option<String> {
283 if labels.iter().any(String::is_empty) {
284 return Some("a pin has no label".into());
285 }
286 let mut seen = BTreeSet::new();
287 labels
288 .iter()
289 .find(|label| !seen.insert(label.as_str()))
290 .map(|label| format!("two pins are labelled '{label}'"))
291}
292
293fn point_list_problem(names: &[String]) -> Option<String> {
296 if names.iter().any(String::is_empty) {
297 return Some("a connection point has no name".into());
298 }
299 let mut seen = BTreeSet::new();
300 names
301 .iter()
302 .find(|name| !seen.insert(name.as_str()))
303 .map(|name| format!("two connection points are named '{name}'"))
304}
305
306fn pairing(pin_labels: &[String], point_names: &[String], point_indices: &[usize]) -> Vec<Option<usize>> {
309 pin_labels
310 .iter()
311 .map(|pin| {
312 let hits: Vec<usize> = point_names
313 .iter()
314 .enumerate()
315 .filter(|(_, name)| *name == pin)
316 .map(|(local, _)| point_indices[local])
317 .collect();
318 match hits.as_slice() {
319 [index] => Some(*index),
320 _ => None,
321 }
322 })
323 .collect()
324}
325
326#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
332pub struct PinPoint {
333 pub pin: String,
334 pub unit: u32,
336 pub point: String,
337}
338
339#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
342#[serde(tag = "problem", rename_all = "kebab-case")]
343pub enum PinPointProblem {
344 UnlabelledPin { unit: u32 },
346 RepeatedPin { unit: u32, pin: String },
348 PinWithoutPoint { unit: u32, pin: String },
350 RepeatedPointName { name: String, points: Vec<String> },
352 PointWithoutPin { point: String },
354 PadWithoutPin { pad: String },
357 Unit(UnitProblem),
359}
360
361impl PinPointProblem {
362 pub fn message(&self) -> String {
363 match self {
364 PinPointProblem::UnlabelledPin { unit } => format!("a pin of unit {unit} has no label"),
365 PinPointProblem::RepeatedPin { unit, pin } => {
366 format!("two pins of unit {unit} are labelled '{pin}'")
367 }
368 PinPointProblem::PinWithoutPoint { unit, pin } => {
369 format!("pin '{pin}' of unit {unit} has no connection point")
370 }
371 PinPointProblem::RepeatedPointName { name, points } => {
372 format!("{} are all named '{name}'", points.join(", "))
373 }
374 PinPointProblem::PointWithoutPin { point } => format!("{point} has no pin"),
375 PinPointProblem::PadWithoutPin { pad } => format!("pad '{pad}' matches no pin"),
376 PinPointProblem::Unit(problem) => problem.message(),
377 }
378 }
379}
380
381#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
383pub struct PinPointReport {
384 pub pairs: Vec<PinPoint>,
385 pub problems: Vec<PinPointProblem>,
386}
387
388pub fn pin_point_report(document: &Value) -> Option<PinPointReport> {
396 let pins = pins(document)?;
397 let points = declared_points(document);
398 let (groups, unit_problems) = unit_groups(document);
399 let mut report = PinPointReport::default();
400 report
401 .problems
402 .extend(unit_problems.into_iter().map(PinPointProblem::Unit));
403
404 let mut all_labels: BTreeSet<String> = BTreeSet::new();
406 let bound: BTreeSet<&str> = groups.values().map(String::as_str).collect();
409
410 for unit in units_present(&pins, &groups) {
411 let pin_indices = pins_of_unit(&pins, unit);
412 let labels = labels_at(&pins, &pin_indices);
413 let mut seen: BTreeSet<String> = BTreeSet::new();
414 let mut repeated: BTreeSet<String> = BTreeSet::new();
415 for label in &labels {
416 if label.is_empty() {
417 report.problems.push(PinPointProblem::UnlabelledPin { unit });
418 } else if !seen.insert(label.clone()) && repeated.insert(label.clone()) {
419 report
420 .problems
421 .push(PinPointProblem::RepeatedPin { unit, pin: label.clone() });
422 }
423 }
424 all_labels.extend(seen.iter().cloned());
425
426 let group = groups.get(&unit).cloned().unwrap_or_default();
427 let point_indices = points_of_group(&points, &group);
428 let names = names_at(&points, &point_indices);
429 let mut reported: BTreeSet<String> = BTreeSet::new();
430 for label in labels.iter().filter(|label| !label.is_empty()) {
431 if !reported.insert(label.clone()) {
432 continue;
433 }
434 let hits: Vec<usize> = names
435 .iter()
436 .enumerate()
437 .filter(|(_, name)| *name == label)
438 .map(|(local, _)| point_indices[local])
439 .collect();
440 match hits.as_slice() {
441 [index] => report.pairs.push(PinPoint {
442 pin: label.clone(),
443 unit,
444 point: points[*index].address(),
445 }),
446 [] => report
447 .problems
448 .push(PinPointProblem::PinWithoutPoint { unit, pin: label.clone() }),
449 _ => {}
451 }
452 }
453 for index in &point_indices {
455 if !seen.contains(&points[*index].point) {
456 report
457 .problems
458 .push(PinPointProblem::PointWithoutPin { point: points[*index].address() });
459 }
460 }
461 }
462
463 let mut by_group: BTreeMap<(&str, &str), Vec<String>> = BTreeMap::new();
465 for point in &points {
466 by_group
467 .entry((point.port.as_str(), point.point.as_str()))
468 .or_default()
469 .push(point.address());
470 }
471 for ((group, name), addresses) in &by_group {
472 if addresses.len() > 1 && bound.contains(group) {
473 report.problems.push(PinPointProblem::RepeatedPointName {
474 name: (*name).to_string(),
475 points: addresses.clone(),
476 });
477 }
478 }
479
480 let mut pads_reported = BTreeSet::new();
481 for pad in pad_numbers(document) {
482 if !pad.is_empty() && !all_labels.contains(pad.as_str()) && pads_reported.insert(pad.clone())
483 {
484 report.problems.push(PinPointProblem::PadWithoutPin { pad });
485 }
486 }
487 Some(report)
488}
489
490fn units_present(pins: &[DeclaredPin], groups: &BTreeMap<u32, String>) -> Vec<u32> {
493 let mut units: BTreeSet<u32> = pins.iter().map(|pin| pin.unit).collect();
494 units.extend(groups.keys().copied());
495 units.into_iter().collect()
496}
497
498#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
504#[serde(tag = "change", rename_all = "kebab-case")]
505pub enum PinPointChange {
506 PointRenamed { from: String, to: String },
507 PointAdded { point: String },
508 PointRemoved { point: String },
509 PinRelabelled { from: String, to: String },
510 PinAdded { pin: String, unit: u32 },
511 PinRemoved { pin: String },
512 PadsRenumbered { from: String, to: String, count: usize },
513 PadsAmbiguous { from: String, to: String, units: Vec<u32> },
516}
517
518pub fn follow_pin_edit(base: &Value, after: &mut Value) -> Result<Vec<PinPointChange>, String> {
531 let Some(pins_after) = pins(after) else {
532 return Ok(Vec::new());
533 };
534 let pins_base = pins(base).unwrap_or_default();
535 let points_base = declared_points(base);
536 let (groups, _) = unit_groups(after);
539
540 let mut document = after.clone();
541 let mut changes = Vec::new();
542 let mut doomed: Vec<usize> = Vec::new();
545 let mut arrivals: Vec<(String, String)> = Vec::new();
546
547 for unit in units_present(&pins_after, &groups) {
548 let after_indices = pins_of_unit(&pins_after, unit);
549 let labels_after = labels_at(&pins_after, &after_indices);
550 if let Some(problem) = pin_list_problem(&labels_after) {
551 return Err(problem);
552 }
553 let Some(group) = groups.get(&unit) else {
554 continue; };
556 let labels_base = labels_at(&pins_base, &pins_of_unit(&pins_base, unit));
557 let point_indices = points_of_group(&points_base, group);
558 let names_base = names_at(&points_base, &point_indices);
559 let paired = pairing(&labels_base, &names_base, &point_indices);
560 let (renames, removed, added) = diff_labels(&labels_base, &labels_after);
561
562 for (index, from, to) in &renames {
563 match paired[*index] {
564 Some(at) => {
565 let current = declared_points(&document);
568 let holder = points_of_group(¤t, group).into_iter().find(|other| {
569 *other != at && current[*other].point == *to
570 });
571 if let Some(holder) = holder {
572 return Err(format!(
573 "pin '{from}' cannot become '{to}': {} is already named '{to}'",
574 current[holder].address()
575 ));
576 }
577 if rename_point(base, &mut document, at, from, to) {
578 changes.push(PinPointChange::PointRenamed {
579 from: from.clone(),
580 to: to.clone(),
581 });
582 }
583 }
584 None => arrivals.push((group.clone(), to.clone())),
586 }
587 let shared = units_labelled(&pins_base, from);
590 if shared.len() > 1 && pad_numbers(base).iter().any(|number| number == from) {
591 changes.push(PinPointChange::PadsAmbiguous {
592 from: from.clone(),
593 to: to.clone(),
594 units: shared,
595 });
596 } else {
597 let count = renumber_pads(base, &mut document, from, to);
598 if count > 0 {
599 changes.push(PinPointChange::PadsRenumbered {
600 from: from.clone(),
601 to: to.clone(),
602 count,
603 });
604 }
605 }
606 }
607 doomed.extend(removed.iter().filter_map(|index| paired[*index]));
608 arrivals.extend(
609 added
610 .iter()
611 .map(|index| (group.clone(), labels_after[*index].clone())),
612 );
613 }
614
615 doomed.sort_unstable();
617 doomed.dedup();
618 for at in doomed.into_iter().rev() {
619 if let Some(address) = remove_point(&mut document, at) {
620 changes.push(PinPointChange::PointRemoved { point: address });
621 }
622 }
623 for (group, name) in arrivals {
624 let current = declared_points(&document);
625 let holders: Vec<usize> = points_of_group(¤t, &group)
626 .into_iter()
627 .filter(|index| current[*index].point == name)
628 .collect();
629 match holders.as_slice() {
630 [] => {
631 let address = push_point(&mut document, &group, &name);
632 changes.push(PinPointChange::PointAdded { point: address });
633 }
634 [_] => {}
635 several => {
636 return Err(format!(
637 "pin '{name}' names {}, which share that name",
638 several
639 .iter()
640 .map(|at| current[*at].address())
641 .collect::<Vec<_>>()
642 .join(", ")
643 ));
644 }
645 }
646 }
647 *after = document;
648 Ok(changes)
649}
650
651pub fn follow_point_edit(base: &Value, after: &mut Value) -> Result<Vec<PinPointChange>, String> {
663 let Some(pins_before) = pins(after) else {
664 return Ok(Vec::new());
665 };
666 let points_after = declared_points(after);
667 let points_base = declared_points(base);
668 let pins_base = pins(base).unwrap_or_default();
669 let (groups, _) = unit_groups(after);
670
671 let mut document = after.clone();
672 let mut changes = Vec::new();
673 let mut labels: Vec<String> = pins_before.iter().map(|pin| pin.label.clone()).collect();
677 let mut doomed: Vec<usize> = Vec::new();
678 let mut arrivals: Vec<(u32, String)> = Vec::new();
679
680 for unit in units_present(&pins_before, &groups) {
681 let Some(group) = groups.get(&unit) else { continue };
682 let after_group = points_of_group(&points_after, group);
683 let names_after = names_at(&points_after, &after_group);
684 if let Some(problem) = point_list_problem(&names_after) {
685 return Err(problem);
686 }
687 let names_base = names_at(&points_base, &points_of_group(&points_base, group));
688 let unit_pins_base = pins_of_unit(&pins_base, unit);
689 let (renames, removed, added) = diff_labels(&names_base, &names_after);
690
691 let locate = |labels: &[String], from: &str| -> Option<usize> {
693 let same_list = labels.len() == pins_base.len();
694 if same_list {
695 unit_pins_base
696 .iter()
697 .copied()
698 .find(|index| pins_base[*index].label == from)
699 } else {
700 labels.iter().position(|label| label == from)
701 }
702 };
703 for (_, from, to) in &renames {
704 let Some(at) = locate(&labels, from) else {
705 arrivals.push((unit, to.clone()));
706 continue;
707 };
708 if labels.iter().enumerate().any(|(other, label)| other != at && label == to) {
709 return Err(format!(
710 "a connection point cannot become '{to}': pin '{to}' already exists"
711 ));
712 }
713 if labels[at] != *to {
714 labels[at] = to.clone();
715 changes.push(PinPointChange::PinRelabelled {
716 from: from.clone(),
717 to: to.clone(),
718 });
719 }
720 let shared = units_labelled(&pins_base, from);
723 if shared.len() > 1 && pad_numbers(base).iter().any(|number| number == from) {
724 changes.push(PinPointChange::PadsAmbiguous {
725 from: from.clone(),
726 to: to.clone(),
727 units: shared,
728 });
729 } else {
730 let count = renumber_pads(base, &mut document, from, to);
731 if count > 0 {
732 changes.push(PinPointChange::PadsRenumbered {
733 from: from.clone(),
734 to: to.clone(),
735 count,
736 });
737 }
738 }
739 }
740 doomed.extend(
741 removed
742 .iter()
743 .filter_map(|index| locate(&labels, &names_base[*index])),
744 );
745 arrivals.extend(added.iter().map(|index| (unit, names_after[*index].clone())));
746 }
747
748 doomed.sort_unstable();
749 doomed.dedup();
750 for at in doomed.into_iter().rev() {
751 changes.push(PinPointChange::PinRemoved { pin: labels[at].clone() });
752 remove_pin(&mut document, at);
753 labels.remove(at);
754 }
755 set_pin_labels(&mut document, &labels);
756 for (unit, name) in arrivals {
757 let existing = pins(&document).unwrap_or_default();
758 if existing
759 .iter()
760 .any(|pin| pin.unit == unit && pin.label == name)
761 {
762 continue;
763 }
764 push_pin(&mut document, unit, &name);
765 changes.push(PinPointChange::PinAdded { pin: name, unit });
766 }
767 *after = document;
768 Ok(changes)
769}
770
771#[derive(Debug, Clone, Default, PartialEq, Eq)]
774pub struct PointRenames {
775 pub anchored: BTreeMap<String, String>,
778 pub positional: BTreeMap<String, String>,
781}
782
783impl PointRenames {
784 pub fn all(&self) -> BTreeMap<String, String> {
786 let mut all = self.anchored.clone();
787 all.extend(self.positional.clone());
788 all
789 }
790
791 pub fn is_empty(&self) -> bool {
792 self.anchored.is_empty() && self.positional.is_empty()
793 }
794}
795
796fn anchor(point: &ports::PortPoint) -> Option<String> {
805 let seats = [
806 point.point_ref.clone().unwrap_or_default(),
807 point.direction_ref.clone().unwrap_or_default(),
808 point.maps_to.clone().unwrap_or_default(),
809 if point.transform.is_null() { String::new() } else { point.transform.to_string() },
810 ];
811 seats
812 .iter()
813 .any(|seat| !seat.is_empty())
814 .then(|| seats.join("\u{1}"))
815}
816
817fn anchored_pairs(base: &[ports::PortPoint], after: &[ports::PortPoint]) -> Vec<(usize, usize)> {
821 let unique = |points: &[ports::PortPoint]| -> BTreeMap<String, Option<usize>> {
822 let mut seen: BTreeMap<String, Option<usize>> = BTreeMap::new();
823 for (index, point) in points.iter().enumerate() {
824 if let Some(key) = anchor(point) {
825 seen.entry(key)
826 .and_modify(|slot| *slot = None)
827 .or_insert(Some(index));
828 }
829 }
830 seen
831 };
832 let arrived = unique(after);
833 unique(base)
834 .into_iter()
835 .filter_map(|(key, from)| Some((from?, (*arrived.get(&key)?)?)))
836 .collect()
837}
838
839fn group_points(document: &Value, group: &str) -> Vec<ports::PortPoint> {
842 declarations(document)
843 .into_iter()
844 .filter(|declaration| declaration.name.trim() == group)
845 .flat_map(|declaration| declaration.points)
846 .collect()
847}
848
849pub fn point_renames(base: &Value, after: &Value) -> PointRenames {
863 let (points_base, points_after) = (declared_points(base), declared_points(after));
864 let mut groups: Vec<&str> = points_base.iter().map(|point| point.port.as_str()).collect();
865 groups.extend(points_after.iter().map(|point| point.port.as_str()));
866 groups.sort_unstable();
867 groups.dedup();
868 let mut renames = PointRenames::default();
869 for group in groups {
870 let (seated_base, seated_after) = (group_points(base, group), group_points(after, group));
871 let pairs = anchored_pairs(&seated_base, &seated_after);
872 for (from, to) in &pairs {
873 let (from, to) = (seated_base[*from].name.trim(), seated_after[*to].name.trim());
874 if from != to {
875 renames.anchored.insert(from.to_string(), to.to_string());
876 }
877 }
878 let left: BTreeSet<usize> = pairs.iter().map(|(from, _)| *from).collect();
879 let arrived: BTreeSet<usize> = pairs.iter().map(|(_, to)| *to).collect();
880 let rest = |points: &[ports::PortPoint], paired: &BTreeSet<usize>| -> Vec<String> {
881 points
882 .iter()
883 .enumerate()
884 .filter(|(index, _)| !paired.contains(index))
885 .map(|(_, point)| point.name.trim().to_string())
886 .collect()
887 };
888 for (_, from, to) in diff_labels(&rest(&seated_base, &left), &rest(&seated_after, &arrived)).0
889 {
890 renames.positional.insert(from, to);
891 }
892 }
893 renames
894}
895
896type LabelDiff = (Vec<(usize, String, String)>, Vec<usize>, Vec<usize>);
912
913fn diff_labels(base: &[String], after: &[String]) -> LabelDiff {
914 let kept: BTreeSet<&str> = base
915 .iter()
916 .filter(|label| after.contains(label))
917 .map(String::as_str)
918 .collect();
919 let groups = |labels: &[String]| -> BTreeMap<usize, Vec<usize>> {
920 let mut groups: BTreeMap<usize, Vec<usize>> = BTreeMap::new();
921 let mut before = 0;
922 for (index, label) in labels.iter().enumerate() {
923 if kept.contains(label.as_str()) {
924 before += 1;
925 } else {
926 groups.entry(before).or_default().push(index);
927 }
928 }
929 groups
930 };
931 let mut left = groups(base);
932 let mut arrived = groups(after);
933 let (mut renames, mut removed, mut added) = (Vec::new(), Vec::new(), Vec::new());
934 for (group, lefts) in &mut left {
935 let arrivals = arrived.remove(group).unwrap_or_default();
936 let pairs = lefts.len().min(arrivals.len());
937 for (from, to) in lefts.iter().zip(&arrivals) {
938 renames.push((*from, base[*from].clone(), after[*to].clone()));
939 }
940 removed.extend(lefts.drain(pairs..));
941 added.extend(arrivals.into_iter().skip(pairs));
942 }
943 added.extend(arrived.into_values().flatten());
944 added.sort_unstable();
945 let pairs = removed.len().min(added.len());
946 for (from, to) in removed.iter().zip(&added) {
947 renames.push((*from, base[*from].clone(), after[*to].clone()));
948 }
949 removed.drain(..pairs);
950 added.drain(..pairs);
951 (renames, removed, added)
952}
953
954fn locate(document: &Value, at: usize) -> Option<(usize, usize)> {
960 let mut seen = 0;
961 for (block_index, block) in port_blocks(document).iter().enumerate() {
962 let count = block.get("points").and_then(Value::as_array).map_or(0, Vec::len);
963 if at < seen + count {
964 return Some((block_index, at - seen));
965 }
966 seen += count;
967 }
968 None
969}
970
971fn point_mut<'a>(document: &'a mut Value, at: usize) -> Option<&'a mut Value> {
972 let (block, index) = locate(document, at)?;
973 document
974 .get_mut(PORTS_BLOCK)?
975 .as_array_mut()?
976 .get_mut(block)?
977 .get_mut("points")?
978 .as_array_mut()?
979 .get_mut(index)
980}
981
982fn rename_point(base: &Value, document: &mut Value, at: usize, from: &str, to: &str) -> bool {
987 let current = declared_points(document);
988 let at = if current.len() == declared_points(base).len() {
989 at
990 } else {
991 match current
992 .iter()
993 .enumerate()
994 .filter(|(_, point)| point.point == from)
995 .map(|(index, _)| index)
996 .collect::<Vec<_>>()
997 .as_slice()
998 {
999 [index] => *index,
1000 _ => return false,
1001 }
1002 };
1003 let Some(point) = point_mut(document, at).and_then(Value::as_object_mut) else {
1004 return false;
1005 };
1006 if point.get("name").and_then(Value::as_str) == Some(to) {
1007 return false;
1008 }
1009 point.insert("name".into(), Value::String(to.to_string()));
1010 true
1011}
1012
1013fn remove_point(document: &mut Value, at: usize) -> Option<String> {
1015 let address = declared_points(document).get(at)?.address();
1016 let (block, index) = locate(document, at)?;
1017 let points = document
1018 .get_mut(PORTS_BLOCK)?
1019 .as_array_mut()?
1020 .get_mut(block)?
1021 .get_mut("points")?
1022 .as_array_mut()?;
1023 if index >= points.len() {
1024 return None;
1025 }
1026 points.remove(index);
1027 Some(address)
1028}
1029
1030fn push_point(document: &mut Value, group: &str, name: &str) -> String {
1034 let Some(object) = document.as_object_mut() else {
1035 return ports::address(group, name);
1036 };
1037 let blocks = object
1038 .entry(PORTS_BLOCK)
1039 .or_insert_with(|| Value::Array(Vec::new()));
1040 let Some(blocks) = blocks.as_array_mut() else {
1041 return ports::address(group, name);
1042 };
1043 if !blocks
1044 .iter()
1045 .any(|block| block.get("name").and_then(Value::as_str).map(str::trim) == Some(group))
1046 {
1047 blocks.push(serde_json::json!({
1048 "name": group, "purpose": DEFAULT_PORT_PURPOSE, "points": []
1049 }));
1050 }
1051 let block = blocks
1052 .iter_mut()
1053 .find(|block| block.get("name").and_then(Value::as_str).map(str::trim) == Some(group));
1054 let points = block
1055 .and_then(Value::as_object_mut)
1056 .map(|block| block.entry("points").or_insert_with(|| Value::Array(Vec::new())));
1057 if let Some(points) = points.and_then(Value::as_array_mut) {
1058 points.push(serde_json::json!({
1059 "name": name,
1060 "transform": { "position": [0, 0, 0], "rotationEuler": [0, 0, 0] }
1061 }));
1062 }
1063 ports::address(group, name)
1064}
1065
1066fn pins_mut(document: &mut Value) -> Option<&mut Vec<Value>> {
1067 document.get_mut(SYMBOL_BLOCK)?.get_mut("pins")?.as_array_mut()
1068}
1069
1070fn set_pin_labels(document: &mut Value, labels: &[String]) {
1071 if let Some(pins) = pins_mut(document) {
1072 for (pin, label) in pins.iter_mut().zip(labels) {
1073 if let Some(pin) = pin.as_object_mut() {
1074 pin.insert("number".into(), Value::String(label.clone()));
1075 }
1076 }
1077 }
1078}
1079
1080fn remove_pin(document: &mut Value, index: usize) {
1081 if let Some(pins) = pins_mut(document) {
1082 if index < pins.len() {
1083 pins.remove(index);
1084 }
1085 }
1086}
1087
1088fn push_pin(document: &mut Value, unit: u32, label: &str) {
1093 let coordinate = |point: &Value, axis: &str| point.get(axis).and_then(Value::as_i64);
1094 let Some(symbol) = document.get_mut(SYMBOL_BLOCK).and_then(Value::as_object_mut) else {
1095 return;
1096 };
1097 let pins = symbol.entry("pins").or_insert_with(|| Value::Array(Vec::new()));
1098 let Some(pins) = pins.as_array_mut() else {
1099 return;
1100 };
1101 let mine = |pin: &Value| pin.get("unit").and_then(Value::as_u64).unwrap_or(0).max(1) == u64::from(unit);
1102 let tip_x = pins
1103 .iter()
1104 .filter(|pin| mine(pin))
1105 .filter_map(|pin| pin.get("at").and_then(|at| coordinate(at, "x")))
1106 .min()
1107 .unwrap_or(FIRST_PIN_TIP.0);
1108 let tip_y = pins
1109 .iter()
1110 .filter(|pin| mine(pin))
1111 .flat_map(|pin| ["at", "end"].map(|end| pin.get(end).and_then(|point| coordinate(point, "y"))))
1112 .flatten()
1113 .max()
1114 .map_or(FIRST_PIN_TIP.1, |lowest| lowest + PIN_PITCH);
1115 let mut pin = serde_json::json!({
1116 "hidden": false,
1117 "number": label,
1118 "name": label,
1119 "electrical_type": "passive",
1120 "at": { "x": tip_x, "y": tip_y },
1121 "end": { "x": tip_x + PIN_PITCH, "y": tip_y },
1122 });
1123 if unit > 1 {
1126 if let Some(pin) = pin.as_object_mut() {
1127 pin.insert("unit".into(), serde_json::json!(unit));
1128 }
1129 }
1130 pins.push(pin);
1131}
1132
1133fn units_labelled(base: &[DeclaredPin], label: &str) -> Vec<u32> {
1135 let mut units: Vec<u32> = base
1136 .iter()
1137 .filter(|pin| pin.label == label)
1138 .map(|pin| pin.unit)
1139 .collect();
1140 units.sort_unstable();
1141 units.dedup();
1142 units
1143}
1144
1145fn renumber_pads(base: &Value, document: &mut Value, from: &str, to: &str) -> usize {
1162 let base_pads = pad_numbers(base);
1163 let same_list = pad_numbers(document).len() == base_pads.len();
1164 let Some(pads) = document
1165 .get_mut(PADS_BLOCK)
1166 .and_then(|block| block.get_mut("pads"))
1167 .and_then(Value::as_array_mut)
1168 else {
1169 return 0;
1170 };
1171 let mut count = 0;
1172 for (index, pad) in pads.iter_mut().enumerate() {
1173 let carried = if same_list { base_pads[index] == from } else { label_of(pad) == from };
1174 if carried && label_of(pad) != to {
1175 if let Some(pad) = pad.as_object_mut() {
1176 pad.insert("number".into(), Value::String(to.to_string()));
1177 count += 1;
1178 }
1179 }
1180 }
1181 count
1182}
1183
1184fn resolve<'a>(
1194 candidates: impl Iterator<Item = (&'a str, &'a str, &'a str, PortKind)>,
1195 owner: &str,
1196 group: Option<&str>,
1197 pin: &str,
1198) -> Result<String, String> {
1199 let ends: Vec<&str> = candidates
1200 .filter(|(_, port, name, kind)| {
1201 *name == pin
1202 && *kind == PortKind::Termination
1203 && group.is_none_or(|group| *port == group)
1204 })
1205 .map(|(address, _, _, _)| address)
1206 .collect();
1207 let where_ = group.map_or(String::new(), |group| format!(" in port '{group}'"));
1208 match ends.as_slice() {
1209 [address] => Ok((*address).to_string()),
1210 [] => Err(format!("{owner} has no connection point named '{pin}'{where_}")),
1211 several => Err(format!(
1212 "{owner} has {} connection points named '{pin}'{where_} ({})",
1213 several.len(),
1214 several.join(", ")
1215 )),
1216 }
1217}
1218
1219pub fn endpoint_occurrence(address: &str) -> (&str, &str) {
1230 match address.rsplit_once(':') {
1231 Some((chain, local)) => (chain, local),
1232 None => ("", address),
1233 }
1234}
1235
1236pub fn resolve_component_pin(
1254 endpoints: &[WireHarnessEndpoint],
1255 occurrence: &str,
1256 group: Option<&str>,
1257 pin: &str,
1258) -> Result<String, String> {
1259 resolve(
1260 endpoints
1261 .iter()
1262 .filter(|endpoint| endpoint_occurrence(&endpoint.id).0 == occurrence)
1263 .map(|endpoint| {
1264 (
1265 endpoint.id.as_str(),
1266 endpoint.port.as_str(),
1267 endpoint.point.as_str(),
1268 endpoint.kind,
1269 )
1270 }),
1271 occurrence,
1272 group,
1273 pin,
1274 )
1275}
1276
1277pub fn resolve_part_pin(
1282 ports: &BTreeMap<String, PortRecord>,
1283 part: &str,
1284 group: Option<&str>,
1285 pin: &str,
1286) -> Result<String, String> {
1287 resolve(
1288 ports.iter().map(|(address, record)| {
1289 (
1290 address.as_str(),
1291 record.port_name.as_str(),
1292 record.point_name.as_str(),
1293 record.kind,
1294 )
1295 }),
1296 &format!("part '{part}'"),
1297 group,
1298 pin,
1299 )
1300}
1301
1302pub fn declared_endpoints(occurrence: &str, part_document: &Value) -> Vec<WireHarnessEndpoint> {
1312 let outermost = occurrence.split(':').next().unwrap_or(occurrence);
1313 declared_points(part_document)
1314 .into_iter()
1315 .map(|declared| WireHarnessEndpoint {
1316 id: component::namespaced(occurrence, &declared.address()),
1317 port: declared.port,
1318 point: declared.point,
1319 purpose: declared.purpose,
1320 kind: PortKind::Termination,
1321 component: Some(outermost.to_string()),
1322 })
1323 .collect()
1324}
1325