use std::cell::RefCell;
use std::collections::{BTreeMap, BTreeSet, BinaryHeap, HashMap};
use serde::{Deserialize, Serialize};
use crate::feature_pipeline::features::common;
use crate::feature_pipeline::{Env, FeatureResult, HistoryRequest, PortKind, PortRecord, SceneMap};
use crate::{make_arc, NurbsCurve, Vec3};
pub const WIRE_HARNESS_FEATURE_ID: &str = "WireHarness";
pub const WIRE_HARNESS_FEATURE_TYPE: &str = "WH";
pub const BUNDLE_SOLID_PREFIX: &str = "WireHarness:";
const PACKING_EFFICIENCY: f64 = 0.75;
const SAFETY_FACTOR: f64 = 1.1;
const MIN_DIAMETER: f64 = 0.01;
const LENGTH_SAMPLES: usize = 32;
const STATIONS_PER_PIECE: usize = 12;
const TANGENT_EPS: f64 = 1e-9;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
pub enum PortSide {
A,
B,
}
impl PortSide {
pub fn other(self) -> Self {
match self {
PortSide::A => PortSide::B,
PortSide::B => PortSide::A,
}
}
pub fn parse(text: &str) -> Option<Self> {
match text.trim() {
"A" | "a" => Some(PortSide::A),
"B" | "b" => Some(PortSide::B),
_ => None,
}
}
pub fn letter(self) -> &'static str {
match self {
PortSide::A => "A",
PortSide::B => "B",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Attachment {
pub port_ref: String,
pub side: PortSide,
}
impl Attachment {
pub fn parse(value: Option<&serde_json::Value>) -> Option<Self> {
let object = value?.as_object()?;
let port_ref = object.get("portRef")?.as_str()?.trim();
if port_ref.is_empty() {
return None;
}
let side = object
.get("side")
.and_then(serde_json::Value::as_str)
.and_then(PortSide::parse)
.unwrap_or(PortSide::A);
Some(Attachment {
port_ref: port_ref.to_string(),
side,
})
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct WireHarnessState {
#[serde(default)]
pub connections: Vec<WireHarnessConnection>,
#[serde(default, rename = "idCounter")]
pub id_counter: u64,
#[serde(default = "default_true", rename = "buildBundles")]
pub build_bundles: bool,
}
impl Default for WireHarnessState {
fn default() -> Self {
Self {
connections: Vec::new(),
id_counter: 0,
build_bundles: true,
}
}
}
impl WireHarnessState {
pub fn next_id(&mut self) -> String {
self.id_counter += 1;
format!("wire-{}", self.id_counter)
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct WireHarnessConnection {
pub id: String,
#[serde(default)]
pub name: String,
#[serde(default)]
pub from: String,
#[serde(default)]
pub to: String,
#[serde(default = "default_diameter")]
pub diameter: f64,
}
fn default_true() -> bool {
true
}
fn default_diameter() -> f64 {
1.0
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct WireHarnessReport {
pub endpoints: Vec<WireHarnessEndpoint>,
pub segments: Vec<WireHarnessSegment>,
pub routes: Vec<RouteResult>,
pub bundles: Vec<WireHarnessBundle>,
pub segment_problems: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct WireHarnessEndpoint {
pub id: String,
pub label: String,
pub kind: PortKind,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub component: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct WireHarnessSegment {
pub id: String,
pub first_port: String,
pub first_side: PortSide,
pub second_port: String,
pub second_side: PortSide,
pub length: f64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum RouteStatus {
Routed,
MissingEndpoint,
WaypointEndpoint,
SameEndpoint,
NoSegments,
NoRoute,
PortReuse,
}
impl RouteStatus {
pub fn as_str(self) -> &'static str {
match self {
RouteStatus::Routed => "routed",
RouteStatus::MissingEndpoint => "missing-endpoint",
RouteStatus::WaypointEndpoint => "waypoint-endpoint",
RouteStatus::SameEndpoint => "same-endpoint",
RouteStatus::NoSegments => "no-segments",
RouteStatus::NoRoute => "no-route",
RouteStatus::PortReuse => "port-reuse",
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct RouteResult {
pub connection_id: String,
pub feasible: bool,
pub status: RouteStatus,
pub message: String,
pub length: Option<f64>,
pub segment_ids: Vec<String>,
pub node_path: Vec<String>,
pub port_ids: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct WireHarnessBundle {
pub segment_id: String,
pub solid_name: String,
pub wire_count: usize,
pub diameter: f64,
pub length: f64,
pub connection_ids: Vec<String>,
pub error: String,
}
pub fn bundle_diameter(diameters: &[f64]) -> f64 {
let sum_squares: f64 = diameters
.iter()
.map(|d| d.max(0.0))
.map(|d| d * d)
.sum();
if sum_squares <= 0.0 {
return 0.0;
}
(sum_squares / PACKING_EFFICIENCY).sqrt() * SAFETY_FACTOR
}
pub(crate) struct HarnessOutcome {
pub result: Option<FeatureResult>,
pub report: Option<WireHarnessReport>,
}
pub(crate) fn finish_history_run(
request: &HistoryRequest,
scene: &SceneMap,
_env: &Env,
) -> HarnessOutcome {
let network = build_network(request, scene);
let Some(state) = request.wire_harness.as_ref() else {
clear_cache();
let report = WireHarnessReport {
endpoints: network.endpoints(),
segments: network.segment_rows(),
segment_problems: network.problems.clone(),
..Default::default()
};
return HarnessOutcome {
result: None,
report: Some(report),
};
};
let fingerprint = harness_fingerprint(state, &network);
let cached = HARNESS_CACHE.with(|cache| {
cache.borrow().as_ref().and_then(|entry| {
(entry.fingerprint == fingerprint).then(|| (entry.result.clone(), entry.report.clone()))
})
});
if let Some((mut result, report)) = cached {
result.reused = true;
return HarnessOutcome {
result: Some(result),
report: Some(report),
};
}
clear_cache();
let mut report = WireHarnessReport {
endpoints: network.endpoints(),
segments: network.segment_rows(),
segment_problems: network.problems.clone(),
..Default::default()
};
let graph = SidedGraph::build(&network);
for connection in &state.connections {
report.routes.push(route_connection(&network, &graph, connection));
}
let mut result = FeatureResult::empty(WIRE_HARNESS_FEATURE_ID, WIRE_HARNESS_FEATURE_TYPE);
report.bundles = build_bundles(&network, state, &report.routes, &mut result);
HARNESS_CACHE.with(|cache| {
*cache.borrow_mut() = Some(CachedHarness {
fingerprint,
result: result.clone(),
report: report.clone(),
});
});
HarnessOutcome {
result: Some(result),
report: Some(report),
}
}
struct CachedHarness {
fingerprint: u64,
result: FeatureResult,
report: WireHarnessReport,
}
thread_local! {
static HARNESS_CACHE: RefCell<Option<CachedHarness>> = const { RefCell::new(None) };
}
pub fn clear_cache() {
HARNESS_CACHE.with(|cache| {
if let Some(entry) = cache.borrow_mut().take() {
for added in &entry.result.added {
crate::free_registered_solid(added.handle);
}
}
});
}
fn harness_fingerprint(state: &WireHarnessState, network: &Network) -> u64 {
use std::hash::{Hash, Hasher};
let mut hasher = std::collections::hash_map::DefaultHasher::new();
state.build_bundles.hash(&mut hasher);
state.connections.len().hash(&mut hasher);
for connection in &state.connections {
connection.id.hash(&mut hasher);
connection.from.hash(&mut hasher);
connection.to.hash(&mut hasher);
connection.diameter.to_bits().hash(&mut hasher);
}
let bits = |v: Vec3, hasher: &mut std::collections::hash_map::DefaultHasher| {
v.x.to_bits().hash(hasher);
v.y.to_bits().hash(hasher);
v.z.to_bits().hash(hasher);
};
for (id, port) in &network.ports {
id.hash(&mut hasher);
bits(port.point, &mut hasher);
bits(port.direction, &mut hasher);
(port.kind == PortKind::Waypoint).hash(&mut hasher);
port.extension.to_bits().hash(&mut hasher);
}
for segment in &network.segments {
segment.id.hash(&mut hasher);
segment.first_port.hash(&mut hasher);
segment.first_side.hash(&mut hasher);
segment.second_port.hash(&mut hasher);
segment.second_side.hash(&mut hasher);
segment.length.to_bits().hash(&mut hasher);
for curve in &segment.chain {
curve.degree.hash(&mut hasher);
for knot in &curve.knots {
knot.to_bits().hash(&mut hasher);
}
for point in &curve.control_points {
point.x.to_bits().hash(&mut hasher);
point.y.to_bits().hash(&mut hasher);
point.z.to_bits().hash(&mut hasher);
point.w.to_bits().hash(&mut hasher);
}
}
}
hasher.finish()
}
struct Segment {
id: String,
first_port: String,
first_side: PortSide,
second_port: String,
second_side: PortSide,
length: f64,
chain: Vec<NurbsCurve>,
}
struct Network {
ports: BTreeMap<String, PortRecord>,
owners: BTreeMap<String, String>,
segments: Vec<Segment>,
problems: Vec<String>,
}
impl Network {
fn endpoints(&self) -> Vec<WireHarnessEndpoint> {
self.ports
.iter()
.map(|(id, port)| WireHarnessEndpoint {
id: id.clone(),
label: port.label.clone(),
kind: port.kind,
component: self.owners.get(id).cloned(),
})
.collect()
}
fn segment_rows(&self) -> Vec<WireHarnessSegment> {
self.segments
.iter()
.map(|segment| WireHarnessSegment {
id: segment.id.clone(),
first_port: segment.first_port.clone(),
first_side: segment.first_side,
second_port: segment.second_port.clone(),
second_side: segment.second_side,
length: segment.length,
})
.collect()
}
fn segment(&self, id: &str) -> Option<&Segment> {
self.segments.iter().find(|segment| segment.id == id)
}
}
fn build_network(request: &HistoryRequest, scene: &SceneMap) -> Network {
let ports: BTreeMap<String, PortRecord> = scene
.ports
.iter()
.map(|(id, port)| (id.clone(), port.clone()))
.collect();
let owners: BTreeMap<String, String> = ports
.keys()
.filter_map(|id| {
scene
.owning_component(id)
.map(|record| (id.clone(), record.id.clone()))
})
.collect();
let mut segments = Vec::new();
let mut problems = Vec::new();
for descriptor in &request.features {
if !matches!(descriptor.feature_type.as_str(), "SP" | "SPLINE") {
continue;
}
let id = ["id", "featureID"]
.iter()
.find_map(|key| descriptor.input_params.get(key).and_then(|v| v.as_str()))
.unwrap_or("")
.trim()
.to_string();
if id.is_empty() {
continue;
}
let Some(points) = descriptor
.persistent_data
.get("spline")
.and_then(|spline| spline.get("points"))
.and_then(serde_json::Value::as_array)
else {
continue;
};
if points.len() < 2 {
continue;
}
let first = Attachment::parse(points.first().and_then(|p| p.get("attachment")));
let last = Attachment::parse(points.last().and_then(|p| p.get("attachment")));
let (first, last) = match (first, last) {
(Some(first), Some(last)) => (first, last),
(Some(_), None) | (None, Some(_)) => {
problems.push(format!("{id}: only one end is attached to a port"));
continue;
}
(None, None) => continue,
};
if first.port_ref == last.port_ref {
problems.push(format!("{id}: both ends attach to the same port '{}'", first.port_ref));
continue;
}
let (Some(first_record), Some(last_record)) =
(ports.get(&first.port_ref), ports.get(&last.port_ref))
else {
continue;
};
let Some(chain) = scene.resolve_path(&format!("{id}:SplineEdge")).cloned() else {
continue; };
if chain.is_empty() {
continue;
}
let length = chain_length(&chain);
let first_side = physical_side(first_record, end_tangent(&chain, true), first.side);
let second_side = physical_side(
last_record,
end_tangent(&chain, false).scale(-1.0),
last.side.other(),
);
segments.push(Segment {
id,
first_port: first.port_ref,
first_side,
second_port: last.port_ref,
second_side,
length,
chain,
});
}
Network {
ports,
owners,
segments,
problems,
}
}
fn physical_side(port: &PortRecord, outward: Vec3, fallback: PortSide) -> PortSide {
if outward.length() <= TANGENT_EPS {
return fallback;
}
if outward.dot(port.direction) >= 0.0 {
PortSide::A
} else {
PortSide::B
}
}
fn end_tangent(chain: &[NurbsCurve], at_start: bool) -> Vec3 {
let curve = if at_start { chain.first() } else { chain.last() };
let Some(curve) = curve else {
return Vec3::new(0.0, 0.0, 0.0);
};
let Ok([t0, t1]) = curve.domain() else {
return Vec3::new(0.0, 0.0, 0.0);
};
let parameter = if at_start { t0 } else { t1 };
if let Ok(derivatives) = curve.derivatives(parameter, 1) {
if let Some(tangent) = derivatives.get(1) {
if tangent.length() > TANGENT_EPS {
return *tangent;
}
}
}
let near = if at_start {
t0 + (t1 - t0) * 0.05
} else {
t1 - (t1 - t0) * 0.05
};
match (curve.evaluate(parameter), curve.evaluate(near)) {
(Ok(at), Ok(nearby)) => {
if at_start {
nearby.sub(at)
} else {
at.sub(nearby)
}
}
_ => Vec3::new(0.0, 0.0, 0.0),
}
}
fn chain_length(chain: &[NurbsCurve]) -> f64 {
let mut total = 0.0;
for curve in chain {
let Ok([t0, t1]) = curve.domain() else { continue };
let Ok(mut previous) = curve.evaluate(t0) else { continue };
for sample in 1..=LENGTH_SAMPLES {
let t = t0 + (t1 - t0) * sample as f64 / LENGTH_SAMPLES as f64;
if let Ok(point) = curve.evaluate(t) {
total += point.sub(previous).length();
previous = point;
}
}
}
total
}
fn node_key(port: &str, side: PortSide) -> String {
format!("{port}/{}", side.letter())
}
struct Edge {
to: usize,
weight: f64,
segment: usize,
}
struct SidedGraph {
index: HashMap<String, usize>,
keys: Vec<String>,
port_of: Vec<usize>,
ports: Vec<String>,
edges: Vec<Vec<Edge>>,
}
impl SidedGraph {
fn build(network: &Network) -> Self {
let mut graph = SidedGraph {
index: HashMap::new(),
keys: Vec::new(),
port_of: Vec::new(),
ports: Vec::new(),
edges: Vec::new(),
};
for id in network.ports.keys() {
graph.add_port(id);
}
for (segment_index, segment) in network.segments.iter().enumerate() {
let weight = segment.length.max(1e-9);
let from = graph.node(&segment.first_port, segment.first_side);
let to = graph.node(&segment.second_port, segment.second_side.other());
graph.edges[from].push(Edge {
to,
weight,
segment: segment_index,
});
let from = graph.node(&segment.second_port, segment.second_side);
let to = graph.node(&segment.first_port, segment.first_side.other());
graph.edges[from].push(Edge {
to,
weight,
segment: segment_index,
});
}
graph
}
fn add_port(&mut self, id: &str) -> usize {
if let Some(position) = self.ports.iter().position(|p| p == id) {
return position;
}
let port_index = self.ports.len();
self.ports.push(id.to_string());
for side in [PortSide::A, PortSide::B] {
let key = node_key(id, side);
self.index.insert(key.clone(), self.keys.len());
self.keys.push(key);
self.port_of.push(port_index);
self.edges.push(Vec::new());
}
port_index
}
fn node(&mut self, port: &str, side: PortSide) -> usize {
let key = node_key(port, side);
if let Some(&index) = self.index.get(&key) {
return index;
}
self.add_port(port);
self.index[&key]
}
fn node_index(&self, port: &str, side: PortSide) -> Option<usize> {
self.index.get(&node_key(port, side)).copied()
}
}
#[derive(Debug, Clone, PartialEq)]
struct SidedPath {
distance: f64,
nodes: Vec<usize>,
segments: Vec<usize>,
}
#[derive(PartialEq)]
struct HeapEntry<T> {
cost: f64,
item: T,
}
impl<T: PartialEq> Eq for HeapEntry<T> {}
impl<T: PartialEq> PartialOrd for HeapEntry<T> {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl<T: PartialEq> Ord for HeapEntry<T> {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
other
.cost
.partial_cmp(&self.cost)
.unwrap_or(std::cmp::Ordering::Equal)
}
}
fn dijkstra(graph: &SidedGraph, start: usize, end: usize) -> Option<SidedPath> {
let count = graph.keys.len();
let mut distance = vec![f64::INFINITY; count];
let mut parent: Vec<Option<(usize, usize)>> = vec![None; count];
let mut done = vec![false; count];
let mut heap = BinaryHeap::new();
distance[start] = 0.0;
heap.push(HeapEntry {
cost: 0.0,
item: start,
});
while let Some(HeapEntry { cost, item: node }) = heap.pop() {
if done[node] {
continue;
}
done[node] = true;
if node == end {
break;
}
for edge in &graph.edges[node] {
let next = cost + edge.weight;
if next < distance[edge.to] {
distance[edge.to] = next;
parent[edge.to] = Some((node, edge.segment));
heap.push(HeapEntry {
cost: next,
item: edge.to,
});
}
}
}
if !distance[end].is_finite() || start == end {
return None;
}
let mut nodes = vec![end];
let mut segments = Vec::new();
let mut cursor = end;
while let Some((previous, segment)) = parent[cursor] {
segments.push(segment);
nodes.push(previous);
cursor = previous;
}
nodes.reverse();
segments.reverse();
Some(SidedPath {
distance: distance[end],
nodes,
segments,
})
}
fn reuses_a_port(graph: &SidedGraph, path: &SidedPath) -> bool {
let mut seen = BTreeSet::new();
path.nodes
.iter()
.any(|&node| !seen.insert(graph.port_of[node]))
}
fn shortest_non_reusing(graph: &SidedGraph, start: usize, end: usize) -> Option<SidedPath> {
#[derive(PartialEq)]
struct State {
node: usize,
visited: Vec<bool>,
nodes: Vec<usize>,
segments: Vec<usize>,
}
let port_count = graph.ports.len();
let mut best: HashMap<(usize, Vec<bool>), f64> = HashMap::new();
let mut heap = BinaryHeap::new();
let mut visited = vec![false; port_count];
visited[graph.port_of[start]] = true;
heap.push(HeapEntry {
cost: 0.0,
item: State {
node: start,
visited,
nodes: vec![start],
segments: Vec::new(),
},
});
while let Some(HeapEntry { cost, item: state }) = heap.pop() {
if state.node == end && !state.segments.is_empty() {
return Some(SidedPath {
distance: cost,
nodes: state.nodes,
segments: state.segments,
});
}
let key = (state.node, state.visited.clone());
if best.get(&key).is_some_and(|&known| known < cost - 1e-12) {
continue;
}
for edge in &graph.edges[state.node] {
let port = graph.port_of[edge.to];
if state.visited[port] {
continue;
}
let mut visited = state.visited.clone();
visited[port] = true;
let next_cost = cost + edge.weight;
let next_key = (edge.to, visited.clone());
if best.get(&next_key).is_some_and(|&known| known <= next_cost + 1e-12) {
continue;
}
best.insert(next_key, next_cost);
let mut nodes = state.nodes.clone();
nodes.push(edge.to);
let mut segments = state.segments.clone();
segments.push(edge.segment);
heap.push(HeapEntry {
cost: next_cost,
item: State {
node: edge.to,
visited,
nodes,
segments,
},
});
}
}
None
}
fn route_connection(
network: &Network,
graph: &SidedGraph,
connection: &WireHarnessConnection,
) -> RouteResult {
let unrouted = |status: RouteStatus, message: String| RouteResult {
connection_id: connection.id.clone(),
feasible: false,
status,
message,
length: None,
segment_ids: Vec::new(),
node_path: Vec::new(),
port_ids: Vec::new(),
};
let from = connection.from.trim();
let to = connection.to.trim();
for (label, id) in [("from", from), ("to", to)] {
if id.is_empty() {
return unrouted(RouteStatus::MissingEndpoint, format!("no {label} port"));
}
let Some(port) = network.ports.get(id) else {
return unrouted(
RouteStatus::MissingEndpoint,
format!("{label} port '{id}' is not in the model"),
);
};
if port.kind == PortKind::Waypoint {
return unrouted(
RouteStatus::WaypointEndpoint,
format!("{label} port '{}' is a waypoint, not a termination", port.label),
);
}
}
if from == to {
return unrouted(RouteStatus::SameEndpoint, "from and to are the same port".into());
}
if network.segments.is_empty() {
return unrouted(
RouteStatus::NoSegments,
"no harness splines: attach a spline's end anchors to two ports".into(),
);
}
let mut best: Option<SidedPath> = None;
for start_side in [PortSide::A, PortSide::B] {
for end_side in [PortSide::A, PortSide::B] {
let (Some(start), Some(end)) = (
graph.node_index(from, start_side),
graph.node_index(to, end_side),
) else {
continue;
};
if let Some(path) = dijkstra(graph, start, end) {
if best.as_ref().is_none_or(|b| path.distance < b.distance) {
best = Some(path);
}
}
}
}
let Some(mut path) = best else {
return unrouted(
RouteStatus::NoRoute,
"no sided path joins the two ports (check which side each spline leaves its ports on)".into(),
);
};
if reuses_a_port(graph, &path) {
let mut alternative: Option<SidedPath> = None;
for start_side in [PortSide::A, PortSide::B] {
for end_side in [PortSide::A, PortSide::B] {
let (Some(start), Some(end)) = (
graph.node_index(from, start_side),
graph.node_index(to, end_side),
) else {
continue;
};
if let Some(found) = shortest_non_reusing(graph, start, end) {
if alternative.as_ref().is_none_or(|b| found.distance < b.distance) {
alternative = Some(found);
}
}
}
}
match alternative {
Some(found) => path = found,
None => {
return unrouted(
RouteStatus::PortReuse,
"every path passes through the same port twice".into(),
)
}
}
}
RouteResult {
connection_id: connection.id.clone(),
feasible: true,
status: RouteStatus::Routed,
message: String::new(),
length: Some(path.distance),
segment_ids: path
.segments
.iter()
.map(|&index| network.segments[index].id.clone())
.collect(),
node_path: path.nodes.iter().map(|&node| graph.keys[node].clone()).collect(),
port_ids: path
.nodes
.iter()
.map(|&node| graph.ports[graph.port_of[node]].clone())
.collect(),
}
}
fn build_bundles(
network: &Network,
state: &WireHarnessState,
routes: &[RouteResult],
result: &mut FeatureResult,
) -> Vec<WireHarnessBundle> {
let mut usage: Vec<(String, Vec<f64>, Vec<String>)> = Vec::new();
for route in routes.iter().filter(|route| route.feasible) {
let Some(connection) = state
.connections
.iter()
.find(|connection| connection.id == route.connection_id)
else {
continue;
};
let diameter = connection.diameter.max(MIN_DIAMETER);
for segment_id in &route.segment_ids {
let entry = match usage.iter_mut().find(|(id, _, _)| id == segment_id) {
Some(entry) => entry,
None => {
usage.push((segment_id.clone(), Vec::new(), Vec::new()));
usage.last_mut().expect("just pushed")
}
};
entry.1.push(diameter);
if !entry.2.contains(&connection.id) {
entry.2.push(connection.id.clone());
}
}
}
let mut bundles = Vec::with_capacity(usage.len());
for (segment_id, diameters, connection_ids) in usage {
let Some(segment) = network.segment(&segment_id) else {
continue;
};
let diameter = bundle_diameter(&diameters).max(MIN_DIAMETER);
let mut bundle = WireHarnessBundle {
segment_id: segment_id.clone(),
solid_name: String::new(),
wire_count: diameters.len(),
diameter,
length: segment.length,
connection_ids,
error: String::new(),
};
if state.build_bundles {
let name = format!("{BUNDLE_SOLID_PREFIX}{segment_id}");
match sweep_bundle(segment, diameter * 0.5, &name) {
Ok(solid) => {
result.added.push(common::register_added(solid, &name));
bundle.solid_name = name;
}
Err(error) => bundle.error = error,
}
}
bundles.push(bundle);
}
bundles
}
fn sweep_bundle(segment: &Segment, radius: f64, name: &str) -> Result<crate::BrepSolid, String> {
let start = segment.chain[0]
.domain()
.and_then(|[t0, _]| segment.chain[0].evaluate(t0))?;
let tangent = end_tangent(&segment.chain, true)
.normalized()
.map_err(|_| "the chain starts with a zero tangent".to_string())?;
let x_axis = tangent.perpendicular()?;
let y_axis = tangent.cross(x_axis).normalized()?;
let profile = vec![
make_arc(start, x_axis, y_axis, radius, 0.0, std::f64::consts::PI)?,
make_arc(start, x_axis, y_axis, radius, std::f64::consts::PI, std::f64::consts::TAU)?,
];
let names: Vec<String> = (0..segment.chain.len())
.map(|index| format!("{}:piece{index}", segment.id))
.collect();
let stations = (segment.chain.len() * STATIONS_PER_PIECE).clamp(32, 1024);
let mut solid = crate::sweep_profile_along_chain_with_stations(
&profile,
&segment.chain,
&names,
stations,
"a harness segment must be tangent-continuous (a spline always is; a zero extension at an anchor can leave a corner)",
)?;
let faces = &mut solid
.shells
.get_mut(0)
.ok_or("the sweep produced no shell")?
.faces;
let expected = ["Wall0", "Wall1", "Start", "End"];
if faces.len() != expected.len() {
return Err(format!(
"the sweep produced {} faces, expected {}",
faces.len(),
expected.len()
));
}
for (face, suffix) in faces.iter_mut().zip(expected) {
face.name = Some(format!("{name}:{suffix}"));
}
Ok(solid)
}