use crate::document::Document;
use brep_ecad_egui::Part;
use brep_render::engine_state::NoticeSeverity;
use serde_json::Value;
use std::collections::{BTreeMap, BTreeSet};
pub(crate) struct Device {
pub(crate) occurrence: String,
pub(crate) key: String,
pub(crate) signature: String,
pub(crate) reference: String,
pub(crate) blocks: Value,
pub(crate) known: bool,
pub(crate) local_index: Option<usize>,
}
fn is_component(feature: &Value) -> bool {
matches!(feature["type"].as_str(), Some("ACOMP" | "ASSEMBLY COMPONENT"))
}
fn features_of(document: &Value) -> &[Value] {
document["features"].as_array().map(Vec::as_slice).unwrap_or(&[])
}
fn blocks_of(document: &Value) -> Value {
let mut blocks = serde_json::Map::new();
for key in ["symbol", "pads", "ports"] {
if let Some(value) = document.get(key).filter(|value| !value.is_null()) {
blocks.insert(key.to_owned(), value.clone());
}
}
Value::Object(blocks)
}
pub(crate) fn devices(features: &[Value], library: &Value) -> Vec<Device> {
let mut found = Vec::new();
walk(features, library, "", "", &mut found);
found
}
fn walk(features: &[Value], library: &Value, occurrence: &str, label: &str, found: &mut Vec<Device>) {
for (index, feature) in features.iter().enumerate() {
if !is_component(feature) { continue; }
let params = &feature["inputParams"];
let (Some(id), Some(key)) = (params["id"].as_str(), params["partName"].as_str()) else { continue; };
let chain = if occurrence.is_empty() { id.to_owned() } else { format!("{occurrence}:{id}") };
let designator = params["bom"]["Reference_Designator"].as_str().unwrap_or("").trim();
let own = if designator.is_empty() { id } else { designator };
let reference = if label.is_empty() { own.to_owned() } else { format!("{label}/{own}") };
let entry = &library[key];
let document = &entry["document"];
let blocks = blocks_of(document);
let signature = entry["sourceSignature"].as_str().filter(|s| !s.is_empty())
.map(str::to_owned)
.unwrap_or_else(|| match blocks.get("symbol").or(blocks.get("pads")) {
Some(_) => super::parts_library::document_signature(&document.to_string()),
None => String::new(),
});
found.push(Device {
occurrence: chain.clone(), key: key.to_owned(), signature, reference: reference.clone(),
blocks, known: !entry.is_null(),
local_index: occurrence.is_empty().then_some(index),
});
if document.is_null() || brep_render::brep_kernel::document_is_boundary(document) { continue; }
walk(features_of(document), &document["partsLibrary"], &chain, &reference, found);
}
}
struct Offer { part: Part, symbol: bool }
fn named_pads(mut pads: brep_ecad_core::board::Footprint, key: &str) -> brep_ecad_core::board::Footprint {
if pads.name.trim().is_empty() { pads.name = key.to_owned(); }
pads
}
fn unplaceable(part: &Part) -> Option<String> {
let mut probe = brep_ecad_core::Document::default();
probe.place_part(part.source.clone(), part.symbol.clone(), part.pads.clone(), None,
brep_ecad_core::Point::default(), 0).err()
.or_else(|| probe.validate().err())
}
fn offers(reached: Vec<Device>) -> Vec<Offer> {
offers_and_refusals(reached).0
}
fn offers_and_refusals(reached: Vec<Device>) -> (Vec<Offer>, Vec<String>) {
let mut used = BTreeSet::new();
let mut refused = Vec::new();
let offered = reached.into_iter().filter_map(|device| {
let mut reference = device.reference;
if used.contains(&reference) { reference = format!("{reference} ({})", device.occurrence); }
while !used.insert(reference.clone()) { reference.push('_'); }
let key = device.key;
let mut symbol: Option<brep_ecad_core::Symbol> = device.blocks.get("symbol").filter(|v| !v.is_null())
.map(|v| serde_json::from_value(v.clone())).transpose().ok()?;
if let Some(symbol) = &mut symbol { symbol.migrate_legacy_units(); }
let pads = match device.blocks.get("pads").filter(|v| !v.is_null()) {
Some(value) => Some(named_pads(serde_json::from_value(value.clone()).ok()?, &key)),
None => None,
};
if symbol.is_none() && pads.is_none() { return None; }
let signature = device.signature;
let offer = Offer {
symbol: symbol.is_some(),
part: Part {
source: brep_ecad_core::PartSource {
key: key.clone(), signature, instance: Some(device.occurrence),
},
name: key.clone(), symbol: symbol.unwrap_or_else(|| empty_symbol(&key)), pads,
reference: Some(reference.clone()),
},
};
if let Some(reason) = unplaceable(&offer.part) {
refused.push(format!(
"'{reference}' cannot go on a sheet: part '{key}' {reason}. Fix it in the part's \
Symbol or Pads workbench, then Update components."));
return None;
}
Some(offer)
}).collect();
(offered, refused)
}
fn empty_symbol(name: &str) -> brep_ecad_core::Symbol {
brep_ecad_core::Symbol { unit_count: 0, graphic_gates: vec![], hide_pin_names: false, hide_pin_numbers: false, properties: BTreeMap::new(), power_net: None,
library_id: name.into(), reference_prefix: "P".into(), description: String::new(),
graphics: Vec::new(), pins: Vec::new() }
}
#[derive(Debug, Default)]
pub(crate) struct OccurrenceMark {
revision: u64,
step: Option<u64>,
occurrences: BTreeMap<String, Occurrence>,
pending: BTreeSet<String>,
}
#[derive(Clone, Debug, PartialEq)]
pub(crate) struct Occurrence { key: String, reference: String }
fn top_occurrences(history: &brep_render::history::History) -> BTreeMap<String, Occurrence> {
(0..history.len()).filter(|&index| history.feature_type(index)
.is_some_and(|kind| matches!(kind.as_str(), "ACOMP" | "ASSEMBLY COMPONENT")))
.filter_map(|index| {
let id = history.feature_id(index)?;
let params = history.feature_params(index)?;
let designator = params["bom"]["Reference_Designator"].as_str().unwrap_or("").trim();
let reference = if designator.is_empty() { id.clone() } else { designator.to_owned() };
Some((id, Occurrence { key: params["partName"].as_str().unwrap_or("").to_owned(), reference }))
})
.collect()
}
pub(crate) fn follow_occurrences(doc: &mut Document) {
let history = &doc.engine.history;
let revision = history.revision();
if doc.ecad_occurrences.as_ref().is_some_and(|mark| mark.revision == revision) { return; }
let now = top_occurrences(history);
let step = history.undo_top();
let forward = !history.can_redo();
let Some(mark) = doc.ecad_occurrences.take() else {
doc.ecad_occurrences = Some(OccurrenceMark { revision, step, occurrences: now, pending: BTreeSet::new() });
return;
};
let moved = forward && step.is_some() && step > mark.step;
let gone: Vec<(String, Occurrence)> = mark.occurrences.iter()
.filter(|(id, _)| !now.contains_key(*id)).map(|(id, o)| (id.clone(), o.clone())).collect();
let added: BTreeSet<String> = now.keys().filter(|id| !mark.occurrences.contains_key(*id)).cloned().collect();
let mut pending: BTreeSet<String> = if step == mark.step && forward {
mark.pending.iter().filter(|id| now.contains_key(*id)).cloned().collect()
} else {
BTreeSet::new()
};
if moved && gone.is_empty() { pending.extend(added.iter().cloned()); }
if !pending.is_empty() { pending = designate(&mut doc.engine, pending); }
if moved && !gone.is_empty() && added.is_empty() { retire(&mut doc.engine, &gone); }
doc.ecad_occurrences = Some(OccurrenceMark {
revision: doc.engine.history.revision(), step: doc.engine.history.undo_top(), occurrences: now, pending,
});
}
fn designators_in_use(history: &brep_render::history::History, features: &[Value]) -> BTreeSet<String> {
let mut used: BTreeSet<String> = devices(features, history.parts_library()).into_iter()
.flat_map(|device| {
let own = device.reference.rsplit('/').next().unwrap_or_default().to_owned();
[device.reference, own]
})
.collect();
for block in [history.pcb_block(), history.diagram_block()].into_iter().flatten() {
for component in block["components"].as_array().into_iter().flatten() {
if let Some(reference) = component["reference"].as_str() { used.insert(reference.to_owned()); }
}
}
used
}
fn designate(engine: &mut brep_render::engine_state::EngineState, occurrences: BTreeSet<String>) -> BTreeSet<String> {
let history = &engine.history;
let features: Vec<Value> = (0..history.len()).filter_map(|index| {
let kind = history.feature_type(index)?;
if !matches!(kind.as_str(), "ACOMP" | "ASSEMBLY COMPONENT") { return None; }
Some(serde_json::json!({"type": kind, "inputParams": history.feature_params(index)?}))
}).collect();
let mut used = designators_in_use(history, &features);
let mut waiting = BTreeSet::new();
let mut writes: Vec<(String, Value)> = Vec::new();
for device in devices(&features, history.parts_library()) {
if !occurrences.contains(&device.occurrence) { continue; }
let Some(index) = device.local_index else { continue; };
let params = &features[index]["inputParams"];
if params["bom"]["Reference_Designator"].as_str().is_some_and(|d| !d.trim().is_empty()) { continue; }
if !device.known { waiting.insert(device.occurrence); continue; }
let prefix = device.blocks["symbol"]["reference_prefix"].as_str().unwrap_or_default().trim().to_owned();
if prefix.is_empty() || prefix.contains(['/', ':']) { continue; }
let designator = (1u32..).map(|n| format!("{prefix}{n}")).find(|d| !used.contains(d)).unwrap_or_default();
used.insert(designator.clone());
let mut bom = params["bom"].as_object().cloned().unwrap_or_default();
bom.insert("Reference_Designator".into(), Value::String(designator));
writes.push((device.occurrence, Value::Object(bom)));
}
for (id, bom) in writes { engine.history.fold_param_no_undo(&id, "bom", bom); }
waiting
}
fn under(chain: &str, occurrence: &str) -> bool {
chain == occurrence || chain.strip_prefix(occurrence).is_some_and(|rest| rest.starts_with(':'))
}
fn retire(engine: &mut brep_render::engine_state::EngineState, gone: &[(String, Occurrence)]) {
let Ok(mut draft) = serde_json::from_str::<Value>(&engine.history.request_json()) else { return; };
let lost = |instance: Option<&str>| instance.is_some_and(|chain| gone.iter().any(|(g, _)| under(chain, g)));
let lost_component = |c: &brep_ecad_core::Component| match &c.part {
Some(source) if source.instance.is_some() => lost(source.instance.as_deref()),
Some(source) => gone.iter().any(|(_, o)| o.key == source.key && o.reference == c.reference),
None => false,
};
let mut lines: Vec<String> = Vec::new();
let mut retired: Vec<String> = Vec::new();
let mut changed = false;
for (block, sheet_name) in [("pcb", "the schematic and the board"), ("diagram", "the wiring diagram")] {
let Some(value) = draft.get(block).filter(|v| !v.is_null()).cloned() else { continue; };
let Ok(mut sheet) = brep_ecad_core::Document::from_value(value) else { continue; };
let doomed: Vec<(brep_ecad_core::Uuid, String)> = sheet.components.iter()
.filter(|c| lost_component(c))
.map(|c| (c.id, c.reference.clone())).collect();
if doomed.is_empty() { continue; }
let wired = sheet.wires.iter().filter(|w| [&w.start, &w.end].into_iter().flatten()
.any(|t| doomed.iter().any(|(id, _)| *id == t.component))).count();
let placed = doomed.iter().filter(|(id, _)| sheet.board.placement(*id).is_some()).count();
for (id, _) in &doomed { sheet.delete_component(*id); }
let Ok(stored) = sheet.to_value() else { continue; };
draft[block] = stored;
changed = true;
for (_, name) in &doomed { if !retired.contains(name) { retired.push(name.clone()); } }
let plural = |n: usize, one: &str| format!("{n} {one}{}", if n == 1 { "" } else { "s" });
let mut line = format!("off {sheet_name}");
if block == "pcb" {
let mut kept = Vec::new();
if wired > 0 { kept.push(format!("{} to it left unconnected", plural(wired, "wire"))); }
if placed > 0 { kept.push("any track to its pads kept as unconnected copper".to_owned()); }
if !kept.is_empty() { line.push_str(&format!(" ({})", kept.join("; "))); }
} else if wired > 0 {
line.push_str(&format!(" with its {}", plural(wired, "connection")));
}
lines.push(line);
}
if let Some(connections) = draft["wireHarness"]["connections"].as_array_mut() {
let before = connections.len();
connections.retain(|c| !["from", "to"].iter().any(|end| lost(c[end].as_str())));
let dropped = before - connections.len();
if dropped > 0 {
changed = true;
lines.push(format!("{dropped} harness connection{} dropped", if dropped == 1 { "" } else { "s" }));
}
}
if !changed { return; }
if let Err(error) = engine.history.adopt_document(&draft.to_string()) {
engine.push_notice(format!("The deleted part stays on the eCAD sheets: {error}"));
return;
}
engine.rebuild_current_history();
let what = if retired.is_empty() {
gone.iter().map(|(_, o)| o.reference.as_str()).collect::<Vec<_>>().join(", ")
} else {
retired.join(", ")
};
engine.push_notice_as(NoticeSeverity::Warning, format!("{what} left the assembly, so it left eCAD too: {}. Undo brings it all back.",
lines.join(", ")));
}
pub(crate) fn sync(doc: &mut Document) {
follow_occurrences(doc);
if !matches!(doc.engine.settings.workbench.as_str(), "diagram" | "pcb") { return; }
let history = &doc.engine.history;
let revision = history.revision();
if doc.ecad_parts_revision == Some(revision) { return; }
let features: Vec<_> = (0..history.len()).filter_map(|index| {
let kind = history.feature_type(index)?;
if !matches!(kind.as_str(), "ACOMP" | "ASSEMBLY COMPONENT") { return None; }
Some(serde_json::json!({"type": kind, "inputParams": history.feature_params(index)?}))
}).collect();
let reached = devices(&features, history.parts_library());
let missing: Vec<String> = reached.iter().filter(|device| !device.known)
.map(|device| format!(
"'{}' is placed from part '{}', which this assembly's library does not \
carry — it cannot be offered to Diagram or PCB.", device.reference, device.key))
.collect();
let (offered, refused) = offers_and_refusals(reached);
let missing: Vec<String> = missing.into_iter().chain(refused).collect();
doc.ecad.diagram.parts = offered.iter().filter(|offer| offer.symbol).map(|offer| offer.part.clone()).collect();
doc.ecad.pcb.parts = offered.into_iter().map(|offer| offer.part).collect();
let hints = [
(&mut doc.ecad.diagram, "A part appears here once the assembly holds it and it has a symbol. Draw one in the Symbol workbench, or import one from KiCad into the part."),
(&mut doc.ecad.pcb, "A part appears here once the assembly holds it and it has a symbol or pads. Parts with pads go onto the board as well."),
];
for (editor, hint) in hints {
editor.parts_hint = hint.into();
editor.add_part_label = Some(ADD_PART_LABEL.into());
editor.labels_from_host = Some("Named by the BOM: rename it in the BOM pane's Reference_Designator column.".into());
}
let fresh: Vec<String> = missing.iter().filter(|line| !doc.ecad_parts_reported.contains(line)).cloned().collect();
doc.ecad_parts_reported = missing;
for line in fresh { doc.engine.push_notice_as(NoticeSeverity::Warning, line); }
doc.ecad_parts_revision = Some(revision);
}
const ADD_PART_LABEL: &str = "Add part to assembly\u{2026}";
pub static HIT_KEYS: &[crate::automation::hit_keys::HitKeyDoc] = &[
crate::automation::hit_keys::HitKeyDoc { panel: "ecadparts", prefix: "parts:add", meaning: "add a part to the assembly: Assembly's Add Component (the insert-component modal), the same arm as the toolbar's assembly.add_component.", command: Some("workbench_button") },
crate::automation::hit_keys::HitKeyDoc { panel: "ecadparts", prefix: "parts:search", meaning: "the search field over the part cards (shown while the pane lists any)", command: None },
crate::automation::hit_keys::HitKeyDoc { panel: "ecadparts", prefix: "parts:card:", meaning: "a part card (parts:card:<reference>, or <name> for a part with no reference), visible rows only: a click starts placing it, a drag drops it on the sheet, a right-click opens its menu", command: None },
];
pub(crate) fn take_add_part_request(doc: &mut Document) -> bool {
let active = match doc.engine.settings.workbench.as_str() {
"diagram" => Some(&doc.ecad.diagram),
"pcb" => Some(&doc.ecad.pcb),
_ => None,
};
if crate::automation::registry::enabled() {
let hits: Vec<(String, eframe::egui::Rect)> = active.map(|e| e.library_hits.clone()).unwrap_or_default();
crate::automation::registry::publish(
"__brepEcadPartsHit",
"the eCAD parts pane's widget rects (parts:add, parts:search, parts:card:<reference>)",
&crate::automation::hit_rects::hits_json(hits.iter().map(|(k, r)| (k, r))),
);
}
let mut asked = false;
for editor in [&mut doc.ecad.diagram, &mut doc.ecad.pcb] {
editor.library_hits.clear();
asked |= std::mem::take(&mut editor.add_part_requested);
}
asked
}
pub(crate) fn take_open_requests(doc: &mut Document) -> Vec<String> {
let requests: BTreeSet<_> = doc.ecad.diagram.open_part_requests.drain(..)
.chain(doc.ecad.pcb.open_part_requests.drain(..)).collect();
if requests.is_empty() { return Vec::new(); }
let document: Value = serde_json::from_str(&doc.engine.history.request_json()).unwrap_or_default();
let mut entries: BTreeMap<String, String> = BTreeMap::new();
let mut libraries = vec![document["partsLibrary"].clone()];
while let Some(library) = libraries.pop() {
for (key, entry) in library.as_object().into_iter().flatten() {
let source = entry["sourceKey"].as_str().unwrap_or("").to_owned();
entries.entry(key.clone()).or_insert(source);
let nested = entry["document"]["partsLibrary"].clone();
if nested.is_object() { libraries.push(nested); }
}
}
requests.into_iter().filter_map(|part| {
match entries.get(&part).filter(|key| !key.is_empty()) {
Some(key) => Some(key.clone()),
None => { doc.engine.push_notice(format!("'{part}' is embedded only; use Edit Part in Assembly to edit it.")); None }
}
}).collect()
}
pub(crate) fn refresh_snapshots(
assembly: &mut Value, before: &Value, key: &str,
) -> Result<Vec<String>, String> {
let now = devices(features_of(assembly), &assembly["partsLibrary"]);
let then = devices(features_of(before), &before["partsLibrary"]);
let nothing = Value::Null;
let entry = &assembly["partsLibrary"][key];
let legacy = Device {
occurrence: String::new(), key: key.to_owned(),
signature: entry["sourceSignature"].as_str().unwrap_or_default().to_owned(),
reference: String::new(), blocks: blocks_of(&entry["document"]), known: true,
local_index: None,
};
let legacy_before = blocks_of(&before["partsLibrary"][key]["document"]);
let resolve = |source: &brep_ecad_core::PartSource| -> Option<(&Device, &Value)> {
match &source.instance {
Some(chain) => Some((
now.iter().find(|device| device.occurrence == *chain)?,
then.iter().find(|device| device.occurrence == *chain)
.map_or(¬hing, |device| &device.blocks),
)),
None if source.key == key => Some((&legacy, &legacy_before)),
None => None,
}
};
let mut notes: Vec<String> = Vec::new();
for block in ["diagram", "pcb"] {
let Some(value) = assembly.get(block).filter(|v| !v.is_null()) else { continue; };
let mut sheet = brep_ecad_core::Document::from_value(value.clone())
.map_err(|e| format!("{block}: {e}"))?;
let mut moved = false;
for component in sheet.components.clone() {
let Some(source) = &component.part else { continue; };
let Some((device, was)) = resolve(source) else { continue; };
if device.signature.is_empty() || device.signature == source.signature { continue; }
let mut refused = |error: String| {
let line = format!("'{}' kept the version it had: part '{}' {error}",
component.reference, device.key);
if !notes.contains(&line) { notes.push(line) }
};
let symbol = match device.blocks.get("symbol").filter(|v| !v.is_null())
.map(|v| serde_json::from_value(v.clone())).transpose() {
Ok(symbol) => symbol.unwrap_or_else(|| empty_symbol(&device.key)),
Err(error) => { refused(format!("symbol: {error}")); continue; }
};
let pads: Option<brep_ecad_core::board::Footprint> =
match device.blocks.get("pads").filter(|v| !v.is_null())
.map(|v| serde_json::from_value(v.clone())).transpose() {
Ok(pads) => pads.map(|pads| named_pads(pads, &device.key)),
Err(error) => { refused(format!("pads: {error}")); continue; }
};
let renames = brep_render::brep_kernel::point_renames(was, &device.blocks);
if let Err(error) = sheet.refresh_component(
component.id, &symbol, pads.as_ref(), &device.signature, &renames.all()) {
refused(error);
continue;
}
moved = true;
for (from, to) in &renames.positional {
let line = format!(
"'{}': point '{from}' was read as a rename to '{to}' by POSITION — nothing in \
either version seats it, so a deleted point and a new one would read the \
same. Check the connections on '{to}'.", device.key);
if !notes.contains(&line) { notes.push(line) }
}
}
if moved { assembly[block] = sheet.to_value()?; }
}
Ok(notes)
}
pub(crate) fn declared_endpoints(document: &Value) -> Vec<brep_render::brep_kernel::WireHarnessEndpoint> {
devices(features_of(document), &document["partsLibrary"]).iter()
.flat_map(|device| brep_render::brep_kernel::declared_point_endpoints(
&device.occurrence, &device.blocks))
.collect()
}
fn unlinked_note(component: &brep_ecad_core::Component, reason: &str) -> String {
format!("'{}' is UNLINKED: {reason}. Pick the device it belongs to.", component.reference)
}
pub(crate) fn follow_sheet(
document: &mut Value, block: &str, sheet: &brep_ecad_core::Document,
endpoints: &[brep_render::brep_kernel::WireHarnessEndpoint],
) -> Result<Vec<String>, String> {
let features = document["features"].as_array().cloned().unwrap_or_default();
let reached = devices(&features, &document["partsLibrary"]);
let mut problems = Vec::new();
let mut note = |line: String| if !problems.contains(&line) { problems.push(line) };
let device = |component: &brep_ecad_core::Component| -> Result<Option<&Device>, String> {
let Some(source) = &component.part else { return Ok(None) };
if let Some(chain) = &source.instance {
return reached.iter().find(|device| device.occurrence == *chain).map(Some).ok_or_else(||
unlinked_note(component, &format!("this assembly no longer has the occurrence '{chain}'")));
}
let matches: Vec<&Device> = reached.iter().filter(|device|
device.key == source.key && device.reference == component.reference).collect();
match matches.as_slice() {
[device] => Ok(Some(device)),
[] => Err(unlinked_note(component, "no device of this assembly carries its label")),
several => Err(unlinked_note(component,
&format!("{} devices carry its label", several.len()))),
}
};
if block == "pcb" {
for placement in &sheet.board.placements {
let Some(component) = sheet.components.iter().find(|c| c.id == placement.component) else { continue; };
let device = match device(component) {
Ok(Some(device)) => device,
Ok(None) => continue,
Err(line) => { note(line); continue; }
};
let Some(index) = device.local_index else {
note(format!("'{}' sits inside '{}' and its board placement is not written back to the assembly.",
component.reference, device.occurrence));
continue;
};
let angle = f64::from(placement.rotation % 4) * 90.;
let z = if placement.bottom {
-f64::from(brep_ecad_core::board::SUBSTRATE_THICKNESS) / 1000.
} else {
0.
};
let params = &mut document["features"][index]["inputParams"];
params["transform"] = serde_json::json!({
"translate": [f64::from(placement.at.x) / 1000., -f64::from(placement.at.y) / 1000., z],
"rotateEulerDeg": [0., if placement.bottom {180.} else {0.}, if placement.bottom {angle} else {-angle}],
});
}
} else if block == "diagram" {
let mut connections = document["wireHarness"]["connections"].as_array().cloned().unwrap_or_default();
let previous = connections.clone();
connections.retain(|c| !c["id"].as_str().is_some_and(|id| id.starts_with("diagram:")));
for wire in &sheet.wires {
let (Some(start), Some(end)) = (&wire.start, &wire.end) else { continue; };
let resolve = |terminal: &brep_ecad_core::Terminal| -> Result<Option<String>, String> {
let component = sheet.components.iter().find(|c| c.id == terminal.component)
.ok_or_else(|| "A connection names a component the sheet no longer has.".to_string())?;
let Some(device) = device(component)? else {
return Ok(None);
};
let group = component.symbol.pins.iter().find(|pin| pin.number == terminal.pin)
.and_then(|pin| brep_render::brep_kernel::port_group_for_unit(&device.blocks, pin.unit));
brep_render::brep_kernel::resolve_component_pin(
endpoints, &device.occurrence, group.as_deref(), &terminal.pin)
.map(Some)
.map_err(|reason| format!("'{}' pin '{}': {reason}", component.reference, terminal.pin))
};
let (from, to) = match (resolve(start), resolve(end)) {
(Ok(Some(from)), Ok(Some(to))) => (from, to),
(from, to) => {
for line in [from.err(), to.err()].into_iter().flatten() { note(line); }
continue;
}
};
let id = format!("diagram:{}", wire.id);
let diameter = previous.iter().find(|c| c["id"] == id)
.and_then(|c| c["diameter"].as_f64()).unwrap_or(1.0);
connections.push(serde_json::json!({"id": id, "name": wire.connection_id,
"from": from, "to": to, "diameter": diameter}));
}
if !connections.is_empty() || document.get("wireHarness").is_some() {
if !document["wireHarness"].is_object() { document["wireHarness"] = serde_json::json!({}); }
document["wireHarness"]["connections"] = Value::Array(connections);
}
}
Ok(problems)
}