use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, BTreeSet};
use serde_json::Value;
use crate::feature_pipeline::ports::{self, PortDeclaration, UnitProblem, PORTS_BLOCK};
use crate::feature_pipeline::wire_harness::WireHarnessEndpoint;
use crate::feature_pipeline::{component, PortKind, PortRecord};
pub const SYMBOL_BLOCK: &str = "symbol";
pub const PADS_BLOCK: &str = "pads";
pub const DEFAULT_PORT_NAME: &str = "Pins";
pub const DEFAULT_PORT_PURPOSE: &str = "pcb";
const PIN_PITCH: i64 = 2540;
const FIRST_PIN_TIP: (i64, i64) = (-7620, 0);
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DeclaredPoint {
pub port: String,
pub point: String,
pub purpose: String,
}
impl DeclaredPoint {
pub fn address(&self) -> String {
ports::address(&self.port, &self.point)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DeclaredPin {
pub unit: u32,
pub label: String,
}
pub fn declared_points(document: &Value) -> Vec<DeclaredPoint> {
declarations(document)
.into_iter()
.flat_map(|group| {
let (port, purpose) = (group.name.trim().to_string(), group.purpose);
group.points.into_iter().map(move |point| DeclaredPoint {
port: port.clone(),
point: point.name.trim().to_string(),
purpose: purpose.clone(),
})
})
.collect()
}
pub fn declarations(document: &Value) -> Vec<PortDeclaration> {
document
.get(PORTS_BLOCK)
.cloned()
.and_then(|block| serde_json::from_value::<Vec<PortDeclaration>>(block).ok())
.unwrap_or_default()
}
fn port_blocks(document: &Value) -> &[Value] {
document
.get(PORTS_BLOCK)
.and_then(Value::as_array)
.map(Vec::as_slice)
.unwrap_or(&[])
}
pub fn pins(document: &Value) -> Option<Vec<DeclaredPin>> {
let symbol = document.get(SYMBOL_BLOCK).filter(|block| !block.is_null())?;
Some(
symbol
.get("pins")
.and_then(Value::as_array)
.map(|pins| {
pins.iter()
.map(|pin| DeclaredPin {
unit: pin.get("unit").and_then(Value::as_u64).unwrap_or(0).max(1) as u32,
label: label_of(pin),
})
.collect()
})
.unwrap_or_default(),
)
}
pub fn pin_labels(document: &Value) -> Option<Vec<String>> {
Some(pins(document)?.into_iter().map(|pin| pin.label).collect())
}
pub fn unit_count(document: &Value) -> u32 {
document
.get(SYMBOL_BLOCK)
.and_then(|symbol| symbol.get("unit_count"))
.and_then(Value::as_u64)
.unwrap_or(0)
.max(1) as u32
}
pub fn unit_groups(document: &Value) -> (BTreeMap<u32, String>, Vec<UnitProblem>) {
let mut declarations = declarations(document);
if declarations.is_empty() {
declarations.push(PortDeclaration {
name: DEFAULT_PORT_NAME.to_string(),
purpose: DEFAULT_PORT_PURPOSE.to_string(),
..PortDeclaration::default()
});
}
let (map, problems) = ports::unit_map(
&declarations,
unit_count(document),
pins(document).is_some(),
);
let named = map
.into_iter()
.map(|(unit, index)| (unit, declarations[index].name.trim().to_string()))
.collect();
(named, problems)
}
pub fn port_group_for_unit(document: &Value, unit: u32) -> Option<String> {
unit_groups(document).0.get(&unit.max(1)).cloned()
}
fn pad_numbers(document: &Value) -> Vec<String> {
document
.get(PADS_BLOCK)
.and_then(|block| block.get("pads"))
.and_then(Value::as_array)
.map(|pads| pads.iter().map(|pad| label_of(pad)).collect())
.unwrap_or_default()
}
fn label_of(item: &Value) -> String {
item.get("number").and_then(Value::as_str).unwrap_or_default().to_string()
}
fn pins_of_unit(pins: &[DeclaredPin], unit: u32) -> Vec<usize> {
pins.iter()
.enumerate()
.filter(|(_, pin)| pin.unit == unit)
.map(|(index, _)| index)
.collect()
}
fn points_of_group(points: &[DeclaredPoint], group: &str) -> Vec<usize> {
points
.iter()
.enumerate()
.filter(|(_, point)| point.port == group)
.map(|(index, _)| index)
.collect()
}
fn labels_at(pins: &[DeclaredPin], indices: &[usize]) -> Vec<String> {
indices.iter().map(|index| pins[*index].label.clone()).collect()
}
fn names_at(points: &[DeclaredPoint], indices: &[usize]) -> Vec<String> {
indices.iter().map(|index| points[*index].point.clone()).collect()
}
fn pin_list_problem(labels: &[String]) -> Option<String> {
if labels.iter().any(String::is_empty) {
return Some("a pin has no label".into());
}
let mut seen = BTreeSet::new();
labels
.iter()
.find(|label| !seen.insert(label.as_str()))
.map(|label| format!("two pins are labelled '{label}'"))
}
fn point_list_problem(names: &[String]) -> Option<String> {
if names.iter().any(String::is_empty) {
return Some("a connection point has no name".into());
}
let mut seen = BTreeSet::new();
names
.iter()
.find(|name| !seen.insert(name.as_str()))
.map(|name| format!("two connection points are named '{name}'"))
}
fn pairing(pin_labels: &[String], point_names: &[String], point_indices: &[usize]) -> Vec<Option<usize>> {
pin_labels
.iter()
.map(|pin| {
let hits: Vec<usize> = point_names
.iter()
.enumerate()
.filter(|(_, name)| *name == pin)
.map(|(local, _)| point_indices[local])
.collect();
match hits.as_slice() {
[index] => Some(*index),
_ => None,
}
})
.collect()
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PinPoint {
pub pin: String,
pub unit: u32,
pub point: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "problem", rename_all = "kebab-case")]
pub enum PinPointProblem {
UnlabelledPin { unit: u32 },
RepeatedPin { unit: u32, pin: String },
PinWithoutPoint { unit: u32, pin: String },
RepeatedPointName { name: String, points: Vec<String> },
PointWithoutPin { point: String },
PadWithoutPin { pad: String },
Unit(UnitProblem),
}
impl PinPointProblem {
pub fn message(&self) -> String {
match self {
PinPointProblem::UnlabelledPin { unit } => format!("a pin of unit {unit} has no label"),
PinPointProblem::RepeatedPin { unit, pin } => {
format!("two pins of unit {unit} are labelled '{pin}'")
}
PinPointProblem::PinWithoutPoint { unit, pin } => {
format!("pin '{pin}' of unit {unit} has no connection point")
}
PinPointProblem::RepeatedPointName { name, points } => {
format!("{} are all named '{name}'", points.join(", "))
}
PinPointProblem::PointWithoutPin { point } => format!("{point} has no pin"),
PinPointProblem::PadWithoutPin { pad } => format!("pad '{pad}' matches no pin"),
PinPointProblem::Unit(problem) => problem.message(),
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct PinPointReport {
pub pairs: Vec<PinPoint>,
pub problems: Vec<PinPointProblem>,
}
pub fn pin_point_report(document: &Value) -> Option<PinPointReport> {
let pins = pins(document)?;
let points = declared_points(document);
let (groups, unit_problems) = unit_groups(document);
let mut report = PinPointReport::default();
report
.problems
.extend(unit_problems.into_iter().map(PinPointProblem::Unit));
let mut all_labels: BTreeSet<String> = BTreeSet::new();
let bound: BTreeSet<&str> = groups.values().map(String::as_str).collect();
for unit in units_present(&pins, &groups) {
let pin_indices = pins_of_unit(&pins, unit);
let labels = labels_at(&pins, &pin_indices);
let mut seen: BTreeSet<String> = BTreeSet::new();
let mut repeated: BTreeSet<String> = BTreeSet::new();
for label in &labels {
if label.is_empty() {
report.problems.push(PinPointProblem::UnlabelledPin { unit });
} else if !seen.insert(label.clone()) && repeated.insert(label.clone()) {
report
.problems
.push(PinPointProblem::RepeatedPin { unit, pin: label.clone() });
}
}
all_labels.extend(seen.iter().cloned());
let group = groups.get(&unit).cloned().unwrap_or_default();
let point_indices = points_of_group(&points, &group);
let names = names_at(&points, &point_indices);
let mut reported: BTreeSet<String> = BTreeSet::new();
for label in labels.iter().filter(|label| !label.is_empty()) {
if !reported.insert(label.clone()) {
continue;
}
let hits: Vec<usize> = names
.iter()
.enumerate()
.filter(|(_, name)| *name == label)
.map(|(local, _)| point_indices[local])
.collect();
match hits.as_slice() {
[index] => report.pairs.push(PinPoint {
pin: label.clone(),
unit,
point: points[*index].address(),
}),
[] => report
.problems
.push(PinPointProblem::PinWithoutPoint { unit, pin: label.clone() }),
_ => {}
}
}
for index in &point_indices {
if !seen.contains(&points[*index].point) {
report
.problems
.push(PinPointProblem::PointWithoutPin { point: points[*index].address() });
}
}
}
let mut by_group: BTreeMap<(&str, &str), Vec<String>> = BTreeMap::new();
for point in &points {
by_group
.entry((point.port.as_str(), point.point.as_str()))
.or_default()
.push(point.address());
}
for ((group, name), addresses) in &by_group {
if addresses.len() > 1 && bound.contains(group) {
report.problems.push(PinPointProblem::RepeatedPointName {
name: (*name).to_string(),
points: addresses.clone(),
});
}
}
let mut pads_reported = BTreeSet::new();
for pad in pad_numbers(document) {
if !pad.is_empty() && !all_labels.contains(pad.as_str()) && pads_reported.insert(pad.clone())
{
report.problems.push(PinPointProblem::PadWithoutPin { pad });
}
}
Some(report)
}
fn units_present(pins: &[DeclaredPin], groups: &BTreeMap<u32, String>) -> Vec<u32> {
let mut units: BTreeSet<u32> = pins.iter().map(|pin| pin.unit).collect();
units.extend(groups.keys().copied());
units.into_iter().collect()
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "change", rename_all = "kebab-case")]
pub enum PinPointChange {
PointRenamed { from: String, to: String },
PointAdded { point: String },
PointRemoved { point: String },
PinRelabelled { from: String, to: String },
PinAdded { pin: String, unit: u32 },
PinRemoved { pin: String },
PadsRenumbered { from: String, to: String, count: usize },
PadsAmbiguous { from: String, to: String, units: Vec<u32> },
}
pub fn follow_pin_edit(base: &Value, after: &mut Value) -> Result<Vec<PinPointChange>, String> {
let Some(pins_after) = pins(after) else {
return Ok(Vec::new());
};
let pins_base = pins(base).unwrap_or_default();
let points_base = declared_points(base);
let (groups, _) = unit_groups(after);
let mut document = after.clone();
let mut changes = Vec::new();
let mut doomed: Vec<usize> = Vec::new();
let mut arrivals: Vec<(String, String)> = Vec::new();
for unit in units_present(&pins_after, &groups) {
let after_indices = pins_of_unit(&pins_after, unit);
let labels_after = labels_at(&pins_after, &after_indices);
if let Some(problem) = pin_list_problem(&labels_after) {
return Err(problem);
}
let Some(group) = groups.get(&unit) else {
continue; };
let labels_base = labels_at(&pins_base, &pins_of_unit(&pins_base, unit));
let point_indices = points_of_group(&points_base, group);
let names_base = names_at(&points_base, &point_indices);
let paired = pairing(&labels_base, &names_base, &point_indices);
let (renames, removed, added) = diff_labels(&labels_base, &labels_after);
for (index, from, to) in &renames {
match paired[*index] {
Some(at) => {
let current = declared_points(&document);
let holder = points_of_group(¤t, group).into_iter().find(|other| {
*other != at && current[*other].point == *to
});
if let Some(holder) = holder {
return Err(format!(
"pin '{from}' cannot become '{to}': {} is already named '{to}'",
current[holder].address()
));
}
if rename_point(base, &mut document, at, from, to) {
changes.push(PinPointChange::PointRenamed {
from: from.clone(),
to: to.clone(),
});
}
}
None => arrivals.push((group.clone(), to.clone())),
}
let shared = units_labelled(&pins_base, from);
if shared.len() > 1 && pad_numbers(base).iter().any(|number| number == from) {
changes.push(PinPointChange::PadsAmbiguous {
from: from.clone(),
to: to.clone(),
units: shared,
});
} else {
let count = renumber_pads(base, &mut document, from, to);
if count > 0 {
changes.push(PinPointChange::PadsRenumbered {
from: from.clone(),
to: to.clone(),
count,
});
}
}
}
doomed.extend(removed.iter().filter_map(|index| paired[*index]));
arrivals.extend(
added
.iter()
.map(|index| (group.clone(), labels_after[*index].clone())),
);
}
doomed.sort_unstable();
doomed.dedup();
for at in doomed.into_iter().rev() {
if let Some(address) = remove_point(&mut document, at) {
changes.push(PinPointChange::PointRemoved { point: address });
}
}
for (group, name) in arrivals {
let current = declared_points(&document);
let holders: Vec<usize> = points_of_group(¤t, &group)
.into_iter()
.filter(|index| current[*index].point == name)
.collect();
match holders.as_slice() {
[] => {
let address = push_point(&mut document, &group, &name);
changes.push(PinPointChange::PointAdded { point: address });
}
[_] => {}
several => {
return Err(format!(
"pin '{name}' names {}, which share that name",
several
.iter()
.map(|at| current[*at].address())
.collect::<Vec<_>>()
.join(", ")
));
}
}
}
*after = document;
Ok(changes)
}
pub fn follow_point_edit(base: &Value, after: &mut Value) -> Result<Vec<PinPointChange>, String> {
let Some(pins_before) = pins(after) else {
return Ok(Vec::new());
};
let points_after = declared_points(after);
let points_base = declared_points(base);
let pins_base = pins(base).unwrap_or_default();
let (groups, _) = unit_groups(after);
let mut document = after.clone();
let mut changes = Vec::new();
let mut labels: Vec<String> = pins_before.iter().map(|pin| pin.label.clone()).collect();
let mut doomed: Vec<usize> = Vec::new();
let mut arrivals: Vec<(u32, String)> = Vec::new();
for unit in units_present(&pins_before, &groups) {
let Some(group) = groups.get(&unit) else { continue };
let after_group = points_of_group(&points_after, group);
let names_after = names_at(&points_after, &after_group);
if let Some(problem) = point_list_problem(&names_after) {
return Err(problem);
}
let names_base = names_at(&points_base, &points_of_group(&points_base, group));
let unit_pins_base = pins_of_unit(&pins_base, unit);
let (renames, removed, added) = diff_labels(&names_base, &names_after);
let locate = |labels: &[String], from: &str| -> Option<usize> {
let same_list = labels.len() == pins_base.len();
if same_list {
unit_pins_base
.iter()
.copied()
.find(|index| pins_base[*index].label == from)
} else {
labels.iter().position(|label| label == from)
}
};
for (_, from, to) in &renames {
let Some(at) = locate(&labels, from) else {
arrivals.push((unit, to.clone()));
continue;
};
if labels.iter().enumerate().any(|(other, label)| other != at && label == to) {
return Err(format!(
"a connection point cannot become '{to}': pin '{to}' already exists"
));
}
if labels[at] != *to {
labels[at] = to.clone();
changes.push(PinPointChange::PinRelabelled {
from: from.clone(),
to: to.clone(),
});
}
let shared = units_labelled(&pins_base, from);
if shared.len() > 1 && pad_numbers(base).iter().any(|number| number == from) {
changes.push(PinPointChange::PadsAmbiguous {
from: from.clone(),
to: to.clone(),
units: shared,
});
} else {
let count = renumber_pads(base, &mut document, from, to);
if count > 0 {
changes.push(PinPointChange::PadsRenumbered {
from: from.clone(),
to: to.clone(),
count,
});
}
}
}
doomed.extend(
removed
.iter()
.filter_map(|index| locate(&labels, &names_base[*index])),
);
arrivals.extend(added.iter().map(|index| (unit, names_after[*index].clone())));
}
doomed.sort_unstable();
doomed.dedup();
for at in doomed.into_iter().rev() {
changes.push(PinPointChange::PinRemoved { pin: labels[at].clone() });
remove_pin(&mut document, at);
labels.remove(at);
}
set_pin_labels(&mut document, &labels);
for (unit, name) in arrivals {
let existing = pins(&document).unwrap_or_default();
if existing
.iter()
.any(|pin| pin.unit == unit && pin.label == name)
{
continue;
}
push_pin(&mut document, unit, &name);
changes.push(PinPointChange::PinAdded { pin: name, unit });
}
*after = document;
Ok(changes)
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct PointRenames {
pub anchored: BTreeMap<String, String>,
pub positional: BTreeMap<String, String>,
}
impl PointRenames {
pub fn all(&self) -> BTreeMap<String, String> {
let mut all = self.anchored.clone();
all.extend(self.positional.clone());
all
}
pub fn is_empty(&self) -> bool {
self.anchored.is_empty() && self.positional.is_empty()
}
}
fn anchor(point: &ports::PortPoint) -> Option<String> {
let seats = [
point.point_ref.clone().unwrap_or_default(),
point.direction_ref.clone().unwrap_or_default(),
point.maps_to.clone().unwrap_or_default(),
if point.transform.is_null() { String::new() } else { point.transform.to_string() },
];
seats
.iter()
.any(|seat| !seat.is_empty())
.then(|| seats.join("\u{1}"))
}
fn anchored_pairs(base: &[ports::PortPoint], after: &[ports::PortPoint]) -> Vec<(usize, usize)> {
let unique = |points: &[ports::PortPoint]| -> BTreeMap<String, Option<usize>> {
let mut seen: BTreeMap<String, Option<usize>> = BTreeMap::new();
for (index, point) in points.iter().enumerate() {
if let Some(key) = anchor(point) {
seen.entry(key)
.and_modify(|slot| *slot = None)
.or_insert(Some(index));
}
}
seen
};
let arrived = unique(after);
unique(base)
.into_iter()
.filter_map(|(key, from)| Some((from?, (*arrived.get(&key)?)?)))
.collect()
}
fn group_points(document: &Value, group: &str) -> Vec<ports::PortPoint> {
declarations(document)
.into_iter()
.filter(|declaration| declaration.name.trim() == group)
.flat_map(|declaration| declaration.points)
.collect()
}
pub fn point_renames(base: &Value, after: &Value) -> PointRenames {
let (points_base, points_after) = (declared_points(base), declared_points(after));
let mut groups: Vec<&str> = points_base.iter().map(|point| point.port.as_str()).collect();
groups.extend(points_after.iter().map(|point| point.port.as_str()));
groups.sort_unstable();
groups.dedup();
let mut renames = PointRenames::default();
for group in groups {
let (seated_base, seated_after) = (group_points(base, group), group_points(after, group));
let pairs = anchored_pairs(&seated_base, &seated_after);
for (from, to) in &pairs {
let (from, to) = (seated_base[*from].name.trim(), seated_after[*to].name.trim());
if from != to {
renames.anchored.insert(from.to_string(), to.to_string());
}
}
let left: BTreeSet<usize> = pairs.iter().map(|(from, _)| *from).collect();
let arrived: BTreeSet<usize> = pairs.iter().map(|(_, to)| *to).collect();
let rest = |points: &[ports::PortPoint], paired: &BTreeSet<usize>| -> Vec<String> {
points
.iter()
.enumerate()
.filter(|(index, _)| !paired.contains(index))
.map(|(_, point)| point.name.trim().to_string())
.collect()
};
for (_, from, to) in diff_labels(&rest(&seated_base, &left), &rest(&seated_after, &arrived)).0
{
renames.positional.insert(from, to);
}
}
renames
}
type LabelDiff = (Vec<(usize, String, String)>, Vec<usize>, Vec<usize>);
fn diff_labels(base: &[String], after: &[String]) -> LabelDiff {
let kept: BTreeSet<&str> = base
.iter()
.filter(|label| after.contains(label))
.map(String::as_str)
.collect();
let groups = |labels: &[String]| -> BTreeMap<usize, Vec<usize>> {
let mut groups: BTreeMap<usize, Vec<usize>> = BTreeMap::new();
let mut before = 0;
for (index, label) in labels.iter().enumerate() {
if kept.contains(label.as_str()) {
before += 1;
} else {
groups.entry(before).or_default().push(index);
}
}
groups
};
let mut left = groups(base);
let mut arrived = groups(after);
let (mut renames, mut removed, mut added) = (Vec::new(), Vec::new(), Vec::new());
for (group, lefts) in &mut left {
let arrivals = arrived.remove(group).unwrap_or_default();
let pairs = lefts.len().min(arrivals.len());
for (from, to) in lefts.iter().zip(&arrivals) {
renames.push((*from, base[*from].clone(), after[*to].clone()));
}
removed.extend(lefts.drain(pairs..));
added.extend(arrivals.into_iter().skip(pairs));
}
added.extend(arrived.into_values().flatten());
added.sort_unstable();
let pairs = removed.len().min(added.len());
for (from, to) in removed.iter().zip(&added) {
renames.push((*from, base[*from].clone(), after[*to].clone()));
}
removed.drain(..pairs);
added.drain(..pairs);
(renames, removed, added)
}
fn locate(document: &Value, at: usize) -> Option<(usize, usize)> {
let mut seen = 0;
for (block_index, block) in port_blocks(document).iter().enumerate() {
let count = block.get("points").and_then(Value::as_array).map_or(0, Vec::len);
if at < seen + count {
return Some((block_index, at - seen));
}
seen += count;
}
None
}
fn point_mut<'a>(document: &'a mut Value, at: usize) -> Option<&'a mut Value> {
let (block, index) = locate(document, at)?;
document
.get_mut(PORTS_BLOCK)?
.as_array_mut()?
.get_mut(block)?
.get_mut("points")?
.as_array_mut()?
.get_mut(index)
}
fn rename_point(base: &Value, document: &mut Value, at: usize, from: &str, to: &str) -> bool {
let current = declared_points(document);
let at = if current.len() == declared_points(base).len() {
at
} else {
match current
.iter()
.enumerate()
.filter(|(_, point)| point.point == from)
.map(|(index, _)| index)
.collect::<Vec<_>>()
.as_slice()
{
[index] => *index,
_ => return false,
}
};
let Some(point) = point_mut(document, at).and_then(Value::as_object_mut) else {
return false;
};
if point.get("name").and_then(Value::as_str) == Some(to) {
return false;
}
point.insert("name".into(), Value::String(to.to_string()));
true
}
fn remove_point(document: &mut Value, at: usize) -> Option<String> {
let address = declared_points(document).get(at)?.address();
let (block, index) = locate(document, at)?;
let points = document
.get_mut(PORTS_BLOCK)?
.as_array_mut()?
.get_mut(block)?
.get_mut("points")?
.as_array_mut()?;
if index >= points.len() {
return None;
}
points.remove(index);
Some(address)
}
fn push_point(document: &mut Value, group: &str, name: &str) -> String {
let Some(object) = document.as_object_mut() else {
return ports::address(group, name);
};
let blocks = object
.entry(PORTS_BLOCK)
.or_insert_with(|| Value::Array(Vec::new()));
let Some(blocks) = blocks.as_array_mut() else {
return ports::address(group, name);
};
if !blocks
.iter()
.any(|block| block.get("name").and_then(Value::as_str).map(str::trim) == Some(group))
{
blocks.push(serde_json::json!({
"name": group, "purpose": DEFAULT_PORT_PURPOSE, "points": []
}));
}
let block = blocks
.iter_mut()
.find(|block| block.get("name").and_then(Value::as_str).map(str::trim) == Some(group));
let points = block
.and_then(Value::as_object_mut)
.map(|block| block.entry("points").or_insert_with(|| Value::Array(Vec::new())));
if let Some(points) = points.and_then(Value::as_array_mut) {
points.push(serde_json::json!({
"name": name,
"transform": { "position": [0, 0, 0], "rotationEuler": [0, 0, 0] }
}));
}
ports::address(group, name)
}
fn pins_mut(document: &mut Value) -> Option<&mut Vec<Value>> {
document.get_mut(SYMBOL_BLOCK)?.get_mut("pins")?.as_array_mut()
}
fn set_pin_labels(document: &mut Value, labels: &[String]) {
if let Some(pins) = pins_mut(document) {
for (pin, label) in pins.iter_mut().zip(labels) {
if let Some(pin) = pin.as_object_mut() {
pin.insert("number".into(), Value::String(label.clone()));
}
}
}
}
fn remove_pin(document: &mut Value, index: usize) {
if let Some(pins) = pins_mut(document) {
if index < pins.len() {
pins.remove(index);
}
}
}
fn push_pin(document: &mut Value, unit: u32, label: &str) {
let coordinate = |point: &Value, axis: &str| point.get(axis).and_then(Value::as_i64);
let Some(symbol) = document.get_mut(SYMBOL_BLOCK).and_then(Value::as_object_mut) else {
return;
};
let pins = symbol.entry("pins").or_insert_with(|| Value::Array(Vec::new()));
let Some(pins) = pins.as_array_mut() else {
return;
};
let mine = |pin: &Value| pin.get("unit").and_then(Value::as_u64).unwrap_or(0).max(1) == u64::from(unit);
let tip_x = pins
.iter()
.filter(|pin| mine(pin))
.filter_map(|pin| pin.get("at").and_then(|at| coordinate(at, "x")))
.min()
.unwrap_or(FIRST_PIN_TIP.0);
let tip_y = pins
.iter()
.filter(|pin| mine(pin))
.flat_map(|pin| ["at", "end"].map(|end| pin.get(end).and_then(|point| coordinate(point, "y"))))
.flatten()
.max()
.map_or(FIRST_PIN_TIP.1, |lowest| lowest + PIN_PITCH);
let mut pin = serde_json::json!({
"hidden": false,
"number": label,
"name": label,
"electrical_type": "passive",
"at": { "x": tip_x, "y": tip_y },
"end": { "x": tip_x + PIN_PITCH, "y": tip_y },
});
if unit > 1 {
if let Some(pin) = pin.as_object_mut() {
pin.insert("unit".into(), serde_json::json!(unit));
}
}
pins.push(pin);
}
fn units_labelled(base: &[DeclaredPin], label: &str) -> Vec<u32> {
let mut units: Vec<u32> = base
.iter()
.filter(|pin| pin.label == label)
.map(|pin| pin.unit)
.collect();
units.sort_unstable();
units.dedup();
units
}
fn renumber_pads(base: &Value, document: &mut Value, from: &str, to: &str) -> usize {
let base_pads = pad_numbers(base);
let same_list = pad_numbers(document).len() == base_pads.len();
let Some(pads) = document
.get_mut(PADS_BLOCK)
.and_then(|block| block.get_mut("pads"))
.and_then(Value::as_array_mut)
else {
return 0;
};
let mut count = 0;
for (index, pad) in pads.iter_mut().enumerate() {
let carried = if same_list { base_pads[index] == from } else { label_of(pad) == from };
if carried && label_of(pad) != to {
if let Some(pad) = pad.as_object_mut() {
pad.insert("number".into(), Value::String(to.to_string()));
count += 1;
}
}
}
count
}
fn resolve<'a>(
candidates: impl Iterator<Item = (&'a str, &'a str, &'a str, PortKind)>,
owner: &str,
group: Option<&str>,
pin: &str,
) -> Result<String, String> {
let ends: Vec<&str> = candidates
.filter(|(_, port, name, kind)| {
*name == pin
&& *kind == PortKind::Termination
&& group.is_none_or(|group| *port == group)
})
.map(|(address, _, _, _)| address)
.collect();
let where_ = group.map_or(String::new(), |group| format!(" in port '{group}'"));
match ends.as_slice() {
[address] => Ok((*address).to_string()),
[] => Err(format!("{owner} has no connection point named '{pin}'{where_}")),
several => Err(format!(
"{owner} has {} connection points named '{pin}'{where_} ({})",
several.len(),
several.join(", ")
)),
}
}
pub fn endpoint_occurrence(address: &str) -> (&str, &str) {
match address.rsplit_once(':') {
Some((chain, local)) => (chain, local),
None => ("", address),
}
}
pub fn resolve_component_pin(
endpoints: &[WireHarnessEndpoint],
occurrence: &str,
group: Option<&str>,
pin: &str,
) -> Result<String, String> {
resolve(
endpoints
.iter()
.filter(|endpoint| endpoint_occurrence(&endpoint.id).0 == occurrence)
.map(|endpoint| {
(
endpoint.id.as_str(),
endpoint.port.as_str(),
endpoint.point.as_str(),
endpoint.kind,
)
}),
occurrence,
group,
pin,
)
}
pub fn resolve_part_pin(
ports: &BTreeMap<String, PortRecord>,
part: &str,
group: Option<&str>,
pin: &str,
) -> Result<String, String> {
resolve(
ports.iter().map(|(address, record)| {
(
address.as_str(),
record.port_name.as_str(),
record.point_name.as_str(),
record.kind,
)
}),
&format!("part '{part}'"),
group,
pin,
)
}
pub fn declared_endpoints(occurrence: &str, part_document: &Value) -> Vec<WireHarnessEndpoint> {
let outermost = occurrence.split(':').next().unwrap_or(occurrence);
declared_points(part_document)
.into_iter()
.map(|declared| WireHarnessEndpoint {
id: component::namespaced(occurrence, &declared.address()),
port: declared.port,
point: declared.point,
purpose: declared.purpose,
kind: PortKind::Termination,
component: Some(outermost.to_string()),
})
.collect()
}