use std::collections::{BTreeMap, BTreeSet};
use serde::{Deserialize, Serialize};
use crate::feature_pipeline::features::common;
use crate::feature_pipeline::{
Axis, Env, FeatureResult, Frame, HistoryRequest, PortKind, PortRecord, SceneMap, ScenePoint,
};
use crate::{make_line, SelectionGeometry, Vec3};
pub const PORTS_FEATURE_ID: &str = "Ports";
pub const PORTS_FEATURE_TYPE: &str = "PORTS";
pub const PORTS_BLOCK: &str = "ports";
pub const BOUNDARY_KEY: &str = "portBoundary";
pub const ADDRESS_SEPARATOR: char = '.';
pub const RESERVED: [char; 3] = [':', '.', '/'];
pub const DEFAULT_EXTENSION: f64 = 2.0;
pub const DEFAULT_DISPLAY_LENGTH: f64 = 6.0;
const MIN_DISPLAY_LENGTH: f64 = 0.1;
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct PortDeclaration {
#[serde(default)]
pub name: String,
#[serde(default)]
pub purpose: String,
#[serde(default, rename = "symbolUnit", skip_serializing_if = "Option::is_none")]
pub symbol_unit: Option<u32>,
#[serde(default)]
pub points: Vec<PortPoint>,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct PortPoint {
#[serde(default)]
pub name: String,
#[serde(default, skip_serializing_if = "serde_json::Value::is_null")]
pub transform: serde_json::Value,
#[serde(default, rename = "pointRef", skip_serializing_if = "Option::is_none")]
pub point_ref: Option<String>,
#[serde(default, rename = "directionRef", skip_serializing_if = "Option::is_none")]
pub direction_ref: Option<String>,
#[serde(default, rename = "reverseDirection", skip_serializing_if = "is_false")]
pub reverse_direction: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub extension: Option<f64>,
#[serde(default, rename = "displayLength", skip_serializing_if = "Option::is_none")]
pub display_length: Option<f64>,
#[serde(default, rename = "mapsTo", skip_serializing_if = "Option::is_none")]
pub maps_to: Option<String>,
}
fn is_false(value: &bool) -> bool {
!*value
}
pub fn address(port: &str, point: &str) -> String {
format!("{port}{ADDRESS_SEPARATOR}{point}")
}
pub fn split_address(address: &str) -> Option<(&str, &str)> {
address.split_once(ADDRESS_SEPARATOR)
}
fn name_problem(kind: &str, name: &str) -> Option<String> {
if name.trim().is_empty() {
return Some(format!("a {kind} has no name"));
}
if name.contains(RESERVED) {
return Some(format!(
"{kind} '{name}' contains a reserved character (':', '.' or '/')"
));
}
None
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "problem", rename_all = "kebab-case")]
pub enum UnitProblem {
UnitWithoutGroup { unit: u32 },
UnitOutOfRange { port: String, unit: u32, units: u32 },
UnitContested { unit: u32, ports: Vec<String> },
}
impl UnitProblem {
pub fn message(&self) -> String {
match self {
UnitProblem::UnitWithoutGroup { unit } => {
format!("symbol unit {unit} has no port group, so its pins bind to nothing")
}
UnitProblem::UnitOutOfRange { port, unit, units } => format!(
"port '{port}' names symbol unit {unit}, and the symbol has {units}"
),
UnitProblem::UnitContested { unit, ports } => {
format!("ports {} all name symbol unit {unit}", ports.join(", "))
}
}
}
}
pub fn unit_map(
declarations: &[PortDeclaration],
unit_count: u32,
has_symbol: bool,
) -> (BTreeMap<u32, usize>, Vec<UnitProblem>) {
let units = unit_count.max(1);
let mut map: BTreeMap<u32, usize> = BTreeMap::new();
let mut problems = Vec::new();
let mut contested: BTreeMap<u32, Vec<String>> = BTreeMap::new();
let explicit: Vec<(usize, u32)> = declarations
.iter()
.enumerate()
.filter_map(|(index, group)| group.symbol_unit.map(|unit| (index, unit)))
.collect();
for (index, unit) in &explicit {
let port = declarations[*index].name.trim().to_string();
if *unit == 0 || *unit > units {
problems.push(UnitProblem::UnitOutOfRange { port, unit: *unit, units });
continue;
}
match map.get(unit) {
Some(first) => contested
.entry(*unit)
.or_insert_with(|| vec![declarations[*first].name.trim().to_string()])
.push(port),
None => {
map.insert(*unit, *index);
}
}
}
for (unit, ports) in contested {
problems.push(UnitProblem::UnitContested { unit, ports });
}
if explicit.is_empty() && declarations.len() == 1 {
map.insert(1, 0);
}
if has_symbol {
for unit in 1..=units {
if !map.contains_key(&unit) {
problems.push(UnitProblem::UnitWithoutGroup { unit });
}
}
}
(map, problems)
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct PortSeat {
pub origin: [f64; 3],
pub x: [f64; 3],
pub y: [f64; 3],
pub z: [f64; 3],
}
impl PortSeat {
pub fn to_frame(self) -> Frame {
let v = |a: [f64; 3]| Vec3::new(a[0], a[1], a[2]);
Frame {
origin: v(self.origin),
x_axis: v(self.x),
y_axis: v(self.y),
z_axis: v(self.z),
}
}
}
impl From<Frame> for PortSeat {
fn from(frame: Frame) -> Self {
let a = |v: Vec3| [v.x, v.y, v.z];
PortSeat {
origin: a(frame.origin),
x: a(frame.x_axis),
y: a(frame.y_axis),
z: a(frame.z_axis),
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct PortPointRow {
pub address: String,
pub port: String,
pub point: String,
pub purpose: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub maps_to: Option<String>,
pub seat: PortSeat,
pub position: [f64; 3],
pub direction: [f64; 3],
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct PortsReport {
#[serde(default)]
pub boundary: bool,
pub points: Vec<PortPointRow>,
pub problems: Vec<String>,
}
pub fn document_is_boundary(document: &serde_json::Value) -> bool {
match document.get(BOUNDARY_KEY).and_then(serde_json::Value::as_bool) {
Some(explicit) => explicit,
None => document.get("pcb").is_some_and(|block| !block.is_null()),
}
}
pub fn request_is_boundary(request: &HistoryRequest) -> bool {
request.port_boundary.unwrap_or(request.pcb_present)
}
pub fn export_ports(
scene_ports: &BTreeMap<String, PortRecord>,
declared: &[String],
boundary: bool,
) -> BTreeMap<String, PortRecord> {
if !boundary {
return scene_ports.clone();
}
let declared: BTreeSet<&str> = declared.iter().map(String::as_str).collect();
scene_ports
.iter()
.filter(|(address, _)| declared.contains(address.as_str()))
.map(|(address, record)| (address.clone(), record.clone()))
.collect()
}
pub fn port_line(record: &PortRecord) -> (Vec3, Vec3) {
let point = record.point;
let direction = record.direction;
let length = record.display_length;
match record.kind {
PortKind::Termination => (point, point.add(direction.scale(length))),
PortKind::Waypoint => (
point.sub(direction.scale(length * 0.5)),
point.add(direction.scale(length * 0.5)),
),
}
}
pub fn publish_port(
result: &mut crate::feature_pipeline::FeatureResult,
address: &str,
record: PortRecord,
) -> Result<(), String> {
let (start, end) = port_line(&record);
result
.paths
.push((format!("{address}:PortLine"), vec![make_line(start, end)?]));
result
.points
.push((format!("{address}:Base"), ScenePoint::model(record.point)));
result.axes.push((
address.to_string(),
Axis { point: record.point, direction: record.direction },
));
result.frames.push((
address.to_string(),
Frame::from_origin_normal(record.point, record.direction)?,
));
result.ports.push((address.to_string(), record));
Ok(())
}
pub fn place_scene_port(
scene: &mut SceneMap,
address: &str,
record: PortRecord,
) -> Result<(), String> {
let (start, end) = port_line(&record);
scene
.paths
.insert(format!("{address}:PortLine"), vec![make_line(start, end)?]);
scene
.points
.insert(format!("{address}:Base"), ScenePoint::model(record.point));
scene.axes.insert(
address.to_string(),
Axis { point: record.point, direction: record.direction },
);
scene.frames.insert(
address.to_string(),
Frame::from_origin_normal(record.point, record.direction)?,
);
scene.ports.insert(address.to_string(), record);
Ok(())
}
fn resolve_seat_reference(scene: &SceneMap, name: &str) -> Result<(Vec3, Option<Vec3>), String> {
if let Some(position) = scene.resolve_point(name) {
return Ok((position, None));
}
if let Some(frame) = scene.resolve_frame(name) {
return Ok((frame.origin, Some(frame.z_axis)));
}
if let Some(axis) = scene.resolve_axis(name) {
return Ok((axis.point, Some(axis.direction)));
}
match analytic_reference(scene, name) {
Some(Ok(geometry)) => Ok(seat_of_geometry(&geometry)),
Some(Err(problem)) => Err(problem),
None => Err(format!("'{name}' resolves to no geometry")),
}
}
fn resolve_seat_direction(scene: &SceneMap, name: &str) -> Result<Vec3, String> {
if let Some(frame) = scene.resolve_frame(name) {
return Ok(frame.z_axis);
}
if let Some(axis) = scene.resolve_axis(name) {
return Ok(axis.direction);
}
if scene.resolve_point(name).is_some() {
return Err(format!("'{name}' is a point, which implies no direction"));
}
match analytic_reference(scene, name) {
Some(Ok(geometry)) => seat_of_geometry(&geometry)
.1
.ok_or_else(|| format!("'{name}' implies no direction")),
Some(Err(problem)) => Err(problem),
None => Err(format!("'{name}' resolves to no geometry")),
}
}
fn seat_of_geometry(geometry: &SelectionGeometry) -> (Vec3, Option<Vec3>) {
match geometry {
SelectionGeometry::Plane { origin, normal } => (*origin, Some(*normal)),
SelectionGeometry::Axis { origin, direction, .. } => (*origin, Some(*direction)),
SelectionGeometry::Sphere { center, .. } => (*center, None),
SelectionGeometry::Circle { center, axis, .. } => (*center, Some(*axis)),
SelectionGeometry::Line { origin, direction } => (*origin, Some(*direction)),
SelectionGeometry::Point { position } => (*position, None),
}
}
fn analytic_reference(
scene: &SceneMap,
name: &str,
) -> Option<Result<SelectionGeometry, String>> {
if name.contains('@') {
return Some(
crate::feature_pipeline::pmi::resolve::resolve_reference(scene, name)
.map_err(|error| error.to_string()),
);
}
if let Some(face) = scene.resolve_face(name) {
let analytic = crate::with_registered_solid_str(face.handle, |solid| {
Ok(crate::resolve_face_selection(solid, face.face_id))
});
return Some(match analytic {
Ok(Ok(geometry)) => Ok(geometry),
_ => common::face_point_normal(face).map(|(point, normal)| {
SelectionGeometry::Plane { origin: point, normal }
}),
});
}
if let Some(edge) = scene.resolve_edge(name) {
let analytic = crate::with_registered_solid_str(edge.handle, |solid| {
Ok(crate::resolve_edge_selection(solid, edge.edge_id))
});
return Some(match analytic {
Ok(Ok(geometry)) => Ok(geometry),
_ => common::edge_axis(edge).map(|axis| SelectionGeometry::Line {
origin: axis.point,
direction: axis.direction,
}),
});
}
None
}
pub fn seat_of(
point: &PortPoint,
scene: &SceneMap,
) -> (Frame, Vec<String>) {
let mut problems = Vec::new();
let mut origin = Vec3::new(0.0, 0.0, 0.0);
let mut direction: Option<Vec3> = None;
if let Some(reference) = trimmed(point.point_ref.as_deref()) {
match resolve_seat_reference(scene, reference) {
Ok((located, implied)) => {
origin = located;
if let Some(implied) = implied {
direction = Some(implied);
}
}
Err(problem) => problems.push(problem),
}
}
if let Some(reference) = trimmed(point.direction_ref.as_deref()) {
match resolve_seat_direction(scene, reference) {
Ok(resolved) => direction = Some(resolved),
Err(problem) => problems.push(problem),
}
}
let seat = match direction {
Some(direction) => Frame::from_origin_direction(origin, direction)
.unwrap_or_else(|_| Frame { origin, ..world_seat() }),
None => Frame { origin, ..world_seat() },
};
(seat, problems)
}
pub fn seat_point(seat: &Frame, local: Vec3) -> Vec3 {
seat.origin
.add(seat.x_axis.scale(local.x))
.add(seat.y_axis.scale(local.y))
.add(seat.z_axis.scale(local.z))
}
pub fn unseat_point(seat: &Frame, world: Vec3) -> Vec3 {
unseat_vector(seat, world.sub(seat.origin))
}
pub fn seat_vector(seat: &Frame, local: Vec3) -> Vec3 {
seat.x_axis
.scale(local.x)
.add(seat.y_axis.scale(local.y))
.add(seat.z_axis.scale(local.z))
}
pub fn unseat_vector(seat: &Frame, world: Vec3) -> Vec3 {
Vec3::new(
world.dot(seat.x_axis),
world.dot(seat.y_axis),
world.dot(seat.z_axis),
)
}
pub fn seated_direction(seat: &Frame, rotation_deg: [f64; 3]) -> Vec3 {
let local = crate::feature_pipeline::features::datum::rotate_euler_xyz(
Vec3::new(1.0, 0.0, 0.0),
[
rotation_deg[0].to_radians(),
rotation_deg[1].to_radians(),
rotation_deg[2].to_radians(),
],
);
seat_vector(seat, local)
}
fn trimmed(value: Option<&str>) -> Option<&str> {
value.map(str::trim).filter(|text| !text.is_empty())
}
pub fn resolve_direction(scene: &SceneMap, name: &str) -> Result<Option<Vec3>, String> {
Ok(resolve_seat_direction(scene, name).ok())
}
pub fn seed_history_run(
request: &HistoryRequest,
scene: &mut SceneMap,
env: &Env,
) -> Vec<(String, u64)> {
let mut seeded = Vec::new();
let mut ports_seen: BTreeSet<&str> = BTreeSet::new();
for declaration in &request.ports {
let port = declaration.name.trim();
if name_problem("port", port).is_some() || !ports_seen.insert(port) {
continue;
}
let mut points_seen: BTreeSet<&str> = BTreeSet::new();
for point in &declaration.points {
let name = point.name.trim();
if name_problem("point", name).is_some() || !points_seen.insert(name) {
continue;
}
if point.point_ref.is_some() || point.direction_ref.is_some() || point.maps_to.is_some()
{
continue;
}
let Ok((record, _, _)) =
resolve_point(point, port, name, &declaration.purpose, scene, env)
else {
continue;
};
let address = address(port, name);
if place_scene_port(scene, &address, record).is_err() {
continue;
}
let version = declaration_version(port, &declaration.purpose, point);
for name in [
address.clone(),
format!("{address}:Base"),
format!("{address}:PortLine"),
] {
seeded.push((name, version));
}
}
}
seeded
}
fn declaration_version(port: &str, purpose: &str, point: &PortPoint) -> u64 {
use std::hash::{Hash, Hasher};
let mut hasher = std::collections::hash_map::DefaultHasher::new();
port.hash(&mut hasher);
purpose.hash(&mut hasher);
serde_json::to_string(point).unwrap_or_default().hash(&mut hasher);
hasher.finish()
}
#[derive(Debug, Default)]
pub struct PortsOutcome {
pub report: Option<PortsReport>,
pub result: Option<FeatureResult>,
}
pub fn finish_history_run(
request: &HistoryRequest,
scene: &mut SceneMap,
env: &Env,
) -> PortsOutcome {
if request.ports.is_empty() {
return PortsOutcome::default();
}
let mut result = FeatureResult::pass_through(
PORTS_FEATURE_ID.to_string(),
PORTS_FEATURE_TYPE.to_string(),
);
let mut report = PortsReport {
boundary: request_is_boundary(request),
..PortsReport::default()
};
let mut ports_seen: BTreeSet<&str> = BTreeSet::new();
for declaration in &request.ports {
let port = declaration.name.trim();
if let Some(problem) = name_problem("port", port) {
report.problems.push(problem);
continue;
}
if !ports_seen.insert(port) {
report.problems.push(format!("two ports are named '{port}'"));
continue;
}
let mut points_seen: BTreeSet<&str> = BTreeSet::new();
for point in &declaration.points {
let name = point.name.trim();
if let Some(problem) = name_problem(&format!("point of port '{port}'"), name) {
report.problems.push(problem);
continue;
}
if !points_seen.insert(name) {
report
.problems
.push(format!("port '{port}' has two points named '{name}'"));
continue;
}
let address = address(port, name);
match resolve_point(point, port, name, &declaration.purpose, scene, env) {
Ok((record, seat, problems)) => {
report.problems.extend(problems);
let (position, direction) = (record.point, record.direction);
if let Err(error) = publish_port(&mut result, &address, record) {
report.problems.push(format!("{address}: {error}"));
continue;
}
report.points.push(PortPointRow {
address,
port: port.to_string(),
point: name.to_string(),
purpose: declaration.purpose.clone(),
maps_to: point.maps_to.clone(),
seat: seat.into(),
position: [position.x, position.y, position.z],
direction: [direction.x, direction.y, direction.z],
});
}
Err(error) => report.problems.push(format!("{address}: {error}")),
}
}
}
scene.apply(&result);
PortsOutcome { report: Some(report), result: Some(result) }
}
fn resolve_point(
point: &PortPoint,
port: &str,
name: &str,
purpose: &str,
scene: &SceneMap,
env: &Env,
) -> Result<(PortRecord, Frame, Vec<String>), String> {
let address = address(port, name);
let mut problems = Vec::new();
if let Some(target) = point.maps_to.as_deref().map(str::trim).filter(|t| !t.is_empty()) {
match scene.resolve_port(target) {
Some(mapped) => {
let seat = Frame::from_origin_direction(mapped.point, mapped.direction)
.unwrap_or_else(|_| world_seat());
return Ok((
PortRecord {
point: mapped.point,
direction: mapped.direction,
kind: PortKind::Termination,
extension: point.extension.unwrap_or(mapped.extension).max(0.0),
display_length: point
.display_length
.unwrap_or(mapped.display_length)
.max(MIN_DISPLAY_LENGTH),
port_name: port.to_string(),
point_name: name.to_string(),
purpose: purpose.to_string(),
},
seat,
problems,
));
}
None => problems.push(format!(
"{address} maps to '{target}', which is not a connection point of this document"
)),
}
}
let position = common::vec3_from_value(
env,
point.transform.get("position"),
&format!("{address}.transform.position"),
[0.0, 0.0, 0.0],
)?;
let rotation = common::vec3_from_value(
env,
point.transform.get("rotationEuler"),
&format!("{address}.transform.rotationEuler"),
[0.0, 0.0, 0.0],
)?;
let (seat, seat_problems) = seat_of(point, scene);
problems.extend(
seat_problems
.into_iter()
.map(|problem| format!("{address}: {problem}")),
);
let base = seat_point(&seat, Vec3::new(position[0], position[1], position[2]));
let mut direction = seated_direction(&seat, rotation)
.normalized()
.map_err(|_| "direction is a zero vector".to_string())?;
if point.reverse_direction {
direction = direction.scale(-1.0);
}
Ok((
PortRecord {
point: base,
direction,
kind: PortKind::Termination,
extension: point.extension.unwrap_or(DEFAULT_EXTENSION).max(0.0),
display_length: point
.display_length
.unwrap_or(DEFAULT_DISPLAY_LENGTH)
.max(MIN_DISPLAY_LENGTH),
port_name: port.to_string(),
point_name: name.to_string(),
purpose: purpose.to_string(),
},
seat,
problems,
))
}
pub fn world_seat() -> Frame {
Frame {
origin: Vec3::new(0.0, 0.0, 0.0),
x_axis: Vec3::new(1.0, 0.0, 0.0),
y_axis: Vec3::new(0.0, 1.0, 0.0),
z_axis: Vec3::new(0.0, 0.0, 1.0),
}
}