1use std::cell::RefCell;
70use std::collections::{BTreeMap, BTreeSet, BinaryHeap, HashMap};
71
72use serde::{Deserialize, Serialize};
73
74use crate::feature_pipeline::features::common;
75use crate::feature_pipeline::{Env, FeatureResult, HistoryRequest, PortKind, PortRecord, SceneMap};
76use crate::{make_arc, NurbsCurve, Vec3};
77
78pub const WIRE_HARNESS_FEATURE_ID: &str = "WireHarness";
81pub const WIRE_HARNESS_FEATURE_TYPE: &str = "WH";
83pub const BUNDLE_SOLID_PREFIX: &str = "WireHarness:";
85
86const PACKING_EFFICIENCY: f64 = 0.75;
89const SAFETY_FACTOR: f64 = 1.1;
91const MIN_DIAMETER: f64 = 0.01;
94const LENGTH_SAMPLES: usize = 32;
96const STATIONS_PER_PIECE: usize = 12;
98const TANGENT_EPS: f64 = 1e-9;
100
101#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
107pub enum PortSide {
108 A,
109 B,
110}
111
112impl PortSide {
113 pub fn other(self) -> Self {
115 match self {
116 PortSide::A => PortSide::B,
117 PortSide::B => PortSide::A,
118 }
119 }
120
121 pub fn parse(text: &str) -> Option<Self> {
123 match text.trim() {
124 "A" | "a" => Some(PortSide::A),
125 "B" | "b" => Some(PortSide::B),
126 _ => None,
127 }
128 }
129
130 pub fn letter(self) -> &'static str {
131 match self {
132 PortSide::A => "A",
133 PortSide::B => "B",
134 }
135 }
136}
137
138#[derive(Debug, Clone, PartialEq, Eq)]
141pub struct Attachment {
142 pub port_ref: String,
143 pub side: PortSide,
144}
145
146impl Attachment {
147 pub fn parse(value: Option<&serde_json::Value>) -> Option<Self> {
150 let object = value?.as_object()?;
151 let port_ref = object.get("portRef")?.as_str()?.trim();
152 if port_ref.is_empty() {
153 return None;
154 }
155 let side = object
156 .get("side")
157 .and_then(serde_json::Value::as_str)
158 .and_then(PortSide::parse)
159 .unwrap_or(PortSide::A);
160 Some(Attachment {
161 port_ref: port_ref.to_string(),
162 side,
163 })
164 }
165}
166
167#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
173pub struct WireHarnessState {
174 #[serde(default)]
175 pub connections: Vec<WireHarnessConnection>,
176 #[serde(default, rename = "idCounter")]
178 pub id_counter: u64,
179 #[serde(default = "default_true", rename = "buildBundles")]
182 pub build_bundles: bool,
183}
184
185impl Default for WireHarnessState {
186 fn default() -> Self {
187 Self {
188 connections: Vec::new(),
189 id_counter: 0,
190 build_bundles: true,
191 }
192 }
193}
194
195impl WireHarnessState {
196 pub fn next_id(&mut self) -> String {
198 self.id_counter += 1;
199 format!("wire-{}", self.id_counter)
200 }
201}
202
203#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
205pub struct WireHarnessConnection {
206 pub id: String,
207 #[serde(default)]
208 pub name: String,
209 #[serde(default)]
210 pub from: String,
211 #[serde(default)]
212 pub to: String,
213 #[serde(default = "default_diameter")]
214 pub diameter: f64,
215}
216
217fn default_true() -> bool {
218 true
219}
220
221fn default_diameter() -> f64 {
222 1.0
223}
224
225#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
231pub struct WireHarnessReport {
232 pub endpoints: Vec<WireHarnessEndpoint>,
234 pub segments: Vec<WireHarnessSegment>,
236 pub routes: Vec<RouteResult>,
238 pub bundles: Vec<WireHarnessBundle>,
240 pub segment_problems: Vec<String>,
243}
244
245#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
247pub struct WireHarnessEndpoint {
248 pub id: String,
249 pub label: String,
250 pub kind: PortKind,
251 #[serde(default, skip_serializing_if = "Option::is_none")]
255 pub component: Option<String>,
256}
257
258#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
261pub struct WireHarnessSegment {
262 pub id: String,
263 pub first_port: String,
264 pub first_side: PortSide,
265 pub second_port: String,
266 pub second_side: PortSide,
267 pub length: f64,
268}
269
270#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
272#[serde(rename_all = "kebab-case")]
273pub enum RouteStatus {
274 Routed,
276 MissingEndpoint,
278 WaypointEndpoint,
280 SameEndpoint,
282 NoSegments,
284 NoRoute,
286 PortReuse,
288}
289
290impl RouteStatus {
291 pub fn as_str(self) -> &'static str {
293 match self {
294 RouteStatus::Routed => "routed",
295 RouteStatus::MissingEndpoint => "missing-endpoint",
296 RouteStatus::WaypointEndpoint => "waypoint-endpoint",
297 RouteStatus::SameEndpoint => "same-endpoint",
298 RouteStatus::NoSegments => "no-segments",
299 RouteStatus::NoRoute => "no-route",
300 RouteStatus::PortReuse => "port-reuse",
301 }
302 }
303}
304
305#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
307pub struct RouteResult {
308 pub connection_id: String,
309 pub feasible: bool,
310 pub status: RouteStatus,
311 pub message: String,
313 pub length: Option<f64>,
315 pub segment_ids: Vec<String>,
317 pub node_path: Vec<String>,
322 pub port_ids: Vec<String>,
324}
325
326#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
328pub struct WireHarnessBundle {
329 pub segment_id: String,
330 pub solid_name: String,
333 pub wire_count: usize,
334 pub diameter: f64,
335 pub length: f64,
336 pub connection_ids: Vec<String>,
337 pub error: String,
339}
340
341pub fn bundle_diameter(diameters: &[f64]) -> f64 {
344 let sum_squares: f64 = diameters
345 .iter()
346 .map(|d| d.max(0.0))
347 .map(|d| d * d)
348 .sum();
349 if sum_squares <= 0.0 {
350 return 0.0;
351 }
352 (sum_squares / PACKING_EFFICIENCY).sqrt() * SAFETY_FACTOR
353}
354
355pub(crate) struct HarnessOutcome {
361 pub result: Option<FeatureResult>,
364 pub report: Option<WireHarnessReport>,
367}
368
369pub(crate) fn finish_history_run(
373 request: &HistoryRequest,
374 scene: &SceneMap,
375 _env: &Env,
376) -> HarnessOutcome {
377 let network = build_network(request, scene);
378 let Some(state) = request.wire_harness.as_ref() else {
379 clear_cache();
381 let report = WireHarnessReport {
382 endpoints: network.endpoints(),
383 segments: network.segment_rows(),
384 segment_problems: network.problems.clone(),
385 ..Default::default()
386 };
387 return HarnessOutcome {
388 result: None,
389 report: Some(report),
390 };
391 };
392
393 let fingerprint = harness_fingerprint(state, &network);
394 let cached = HARNESS_CACHE.with(|cache| {
395 cache.borrow().as_ref().and_then(|entry| {
396 (entry.fingerprint == fingerprint).then(|| (entry.result.clone(), entry.report.clone()))
397 })
398 });
399 if let Some((mut result, report)) = cached {
400 result.reused = true;
401 return HarnessOutcome {
402 result: Some(result),
403 report: Some(report),
404 };
405 }
406 clear_cache();
408
409 let mut report = WireHarnessReport {
410 endpoints: network.endpoints(),
411 segments: network.segment_rows(),
412 segment_problems: network.problems.clone(),
413 ..Default::default()
414 };
415 let graph = SidedGraph::build(&network);
416 for connection in &state.connections {
417 report.routes.push(route_connection(&network, &graph, connection));
418 }
419 let mut result = FeatureResult::empty(WIRE_HARNESS_FEATURE_ID, WIRE_HARNESS_FEATURE_TYPE);
420 report.bundles = build_bundles(&network, state, &report.routes, &mut result);
421
422 HARNESS_CACHE.with(|cache| {
423 *cache.borrow_mut() = Some(CachedHarness {
424 fingerprint,
425 result: result.clone(),
426 report: report.clone(),
427 });
428 });
429 HarnessOutcome {
430 result: Some(result),
431 report: Some(report),
432 }
433}
434
435struct CachedHarness {
436 fingerprint: u64,
437 result: FeatureResult,
438 report: WireHarnessReport,
439}
440
441thread_local! {
442 static HARNESS_CACHE: RefCell<Option<CachedHarness>> = const { RefCell::new(None) };
443}
444
445pub fn clear_cache() {
447 HARNESS_CACHE.with(|cache| {
448 if let Some(entry) = cache.borrow_mut().take() {
449 for added in &entry.result.added {
450 crate::free_registered_solid(added.handle);
451 }
452 }
453 });
454}
455
456fn harness_fingerprint(state: &WireHarnessState, network: &Network) -> u64 {
462 use std::hash::{Hash, Hasher};
463 let mut hasher = std::collections::hash_map::DefaultHasher::new();
464 state.build_bundles.hash(&mut hasher);
465 state.connections.len().hash(&mut hasher);
466 for connection in &state.connections {
467 connection.id.hash(&mut hasher);
468 connection.from.hash(&mut hasher);
469 connection.to.hash(&mut hasher);
470 connection.diameter.to_bits().hash(&mut hasher);
471 }
472 let bits = |v: Vec3, hasher: &mut std::collections::hash_map::DefaultHasher| {
473 v.x.to_bits().hash(hasher);
474 v.y.to_bits().hash(hasher);
475 v.z.to_bits().hash(hasher);
476 };
477 for (id, port) in &network.ports {
478 id.hash(&mut hasher);
479 bits(port.point, &mut hasher);
480 bits(port.direction, &mut hasher);
481 (port.kind == PortKind::Waypoint).hash(&mut hasher);
482 port.extension.to_bits().hash(&mut hasher);
483 }
484 for segment in &network.segments {
485 segment.id.hash(&mut hasher);
486 segment.first_port.hash(&mut hasher);
487 segment.first_side.hash(&mut hasher);
488 segment.second_port.hash(&mut hasher);
489 segment.second_side.hash(&mut hasher);
490 segment.length.to_bits().hash(&mut hasher);
491 for curve in &segment.chain {
492 curve.degree.hash(&mut hasher);
493 for knot in &curve.knots {
494 knot.to_bits().hash(&mut hasher);
495 }
496 for point in &curve.control_points {
497 point.x.to_bits().hash(&mut hasher);
498 point.y.to_bits().hash(&mut hasher);
499 point.z.to_bits().hash(&mut hasher);
500 point.w.to_bits().hash(&mut hasher);
501 }
502 }
503 }
504 hasher.finish()
505}
506
507struct Segment {
513 id: String,
515 first_port: String,
516 first_side: PortSide,
518 second_port: String,
519 second_side: PortSide,
520 length: f64,
521 chain: Vec<NurbsCurve>,
522}
523
524struct Network {
525 ports: BTreeMap<String, PortRecord>,
527 owners: BTreeMap<String, String>,
529 segments: Vec<Segment>,
531 problems: Vec<String>,
532}
533
534impl Network {
535 fn endpoints(&self) -> Vec<WireHarnessEndpoint> {
536 self.ports
537 .iter()
538 .map(|(id, port)| WireHarnessEndpoint {
539 id: id.clone(),
540 label: port.label.clone(),
541 kind: port.kind,
542 component: self.owners.get(id).cloned(),
543 })
544 .collect()
545 }
546
547 fn segment_rows(&self) -> Vec<WireHarnessSegment> {
548 self.segments
549 .iter()
550 .map(|segment| WireHarnessSegment {
551 id: segment.id.clone(),
552 first_port: segment.first_port.clone(),
553 first_side: segment.first_side,
554 second_port: segment.second_port.clone(),
555 second_side: segment.second_side,
556 length: segment.length,
557 })
558 .collect()
559 }
560
561 fn segment(&self, id: &str) -> Option<&Segment> {
562 self.segments.iter().find(|segment| segment.id == id)
563 }
564}
565
566fn build_network(request: &HistoryRequest, scene: &SceneMap) -> Network {
570 let ports: BTreeMap<String, PortRecord> = scene
571 .ports
572 .iter()
573 .map(|(id, port)| (id.clone(), port.clone()))
574 .collect();
575 let owners: BTreeMap<String, String> = ports
576 .keys()
577 .filter_map(|id| {
578 scene
579 .owning_component(id)
580 .map(|record| (id.clone(), record.id.clone()))
581 })
582 .collect();
583 let mut segments = Vec::new();
584 let mut problems = Vec::new();
585 for descriptor in &request.features {
586 if !matches!(descriptor.feature_type.as_str(), "SP" | "SPLINE") {
587 continue;
588 }
589 let id = ["id", "featureID"]
590 .iter()
591 .find_map(|key| descriptor.input_params.get(key).and_then(|v| v.as_str()))
592 .unwrap_or("")
593 .trim()
594 .to_string();
595 if id.is_empty() {
596 continue;
597 }
598 let Some(points) = descriptor
599 .persistent_data
600 .get("spline")
601 .and_then(|spline| spline.get("points"))
602 .and_then(serde_json::Value::as_array)
603 else {
604 continue;
605 };
606 if points.len() < 2 {
607 continue;
608 }
609 let first = Attachment::parse(points.first().and_then(|p| p.get("attachment")));
610 let last = Attachment::parse(points.last().and_then(|p| p.get("attachment")));
611 let (first, last) = match (first, last) {
612 (Some(first), Some(last)) => (first, last),
613 (Some(_), None) | (None, Some(_)) => {
617 problems.push(format!("{id}: only one end is attached to a port"));
618 continue;
619 }
620 (None, None) => continue,
621 };
622 if first.port_ref == last.port_ref {
623 problems.push(format!("{id}: both ends attach to the same port '{}'", first.port_ref));
624 continue;
625 }
626 let (Some(first_record), Some(last_record)) =
627 (ports.get(&first.port_ref), ports.get(&last.port_ref))
628 else {
629 continue;
631 };
632 let Some(chain) = scene.resolve_path(&format!("{id}:SplineEdge")).cloned() else {
633 continue; };
635 if chain.is_empty() {
636 continue;
637 }
638 let length = chain_length(&chain);
639 let first_side = physical_side(first_record, end_tangent(&chain, true), first.side);
640 let second_side = physical_side(
643 last_record,
644 end_tangent(&chain, false).scale(-1.0),
645 last.side.other(),
646 );
647 segments.push(Segment {
648 id,
649 first_port: first.port_ref,
650 first_side,
651 second_port: last.port_ref,
652 second_side,
653 length,
654 chain,
655 });
656 }
657 Network {
658 ports,
659 owners,
660 segments,
661 problems,
662 }
663}
664
665fn physical_side(port: &PortRecord, outward: Vec3, fallback: PortSide) -> PortSide {
669 if outward.length() <= TANGENT_EPS {
670 return fallback;
671 }
672 if outward.dot(port.direction) >= 0.0 {
673 PortSide::A
674 } else {
675 PortSide::B
676 }
677}
678
679fn end_tangent(chain: &[NurbsCurve], at_start: bool) -> Vec3 {
683 let curve = if at_start { chain.first() } else { chain.last() };
684 let Some(curve) = curve else {
685 return Vec3::new(0.0, 0.0, 0.0);
686 };
687 let Ok([t0, t1]) = curve.domain() else {
688 return Vec3::new(0.0, 0.0, 0.0);
689 };
690 let parameter = if at_start { t0 } else { t1 };
691 if let Ok(derivatives) = curve.derivatives(parameter, 1) {
692 if let Some(tangent) = derivatives.get(1) {
693 if tangent.length() > TANGENT_EPS {
694 return *tangent;
695 }
696 }
697 }
698 let near = if at_start {
699 t0 + (t1 - t0) * 0.05
700 } else {
701 t1 - (t1 - t0) * 0.05
702 };
703 match (curve.evaluate(parameter), curve.evaluate(near)) {
704 (Ok(at), Ok(nearby)) => {
705 if at_start {
706 nearby.sub(at)
707 } else {
708 at.sub(nearby)
709 }
710 }
711 _ => Vec3::new(0.0, 0.0, 0.0),
712 }
713}
714
715fn chain_length(chain: &[NurbsCurve]) -> f64 {
717 let mut total = 0.0;
718 for curve in chain {
719 let Ok([t0, t1]) = curve.domain() else { continue };
720 let Ok(mut previous) = curve.evaluate(t0) else { continue };
721 for sample in 1..=LENGTH_SAMPLES {
722 let t = t0 + (t1 - t0) * sample as f64 / LENGTH_SAMPLES as f64;
723 if let Ok(point) = curve.evaluate(t) {
724 total += point.sub(previous).length();
725 previous = point;
726 }
727 }
728 }
729 total
730}
731
732fn node_key(port: &str, side: PortSide) -> String {
737 format!("{port}/{}", side.letter())
738}
739
740struct Edge {
741 to: usize,
742 weight: f64,
743 segment: usize,
745}
746
747struct SidedGraph {
748 index: HashMap<String, usize>,
750 keys: Vec<String>,
751 port_of: Vec<usize>,
753 ports: Vec<String>,
754 edges: Vec<Vec<Edge>>,
755}
756
757impl SidedGraph {
758 fn build(network: &Network) -> Self {
759 let mut graph = SidedGraph {
760 index: HashMap::new(),
761 keys: Vec::new(),
762 port_of: Vec::new(),
763 ports: Vec::new(),
764 edges: Vec::new(),
765 };
766 for id in network.ports.keys() {
769 graph.add_port(id);
770 }
771 for (segment_index, segment) in network.segments.iter().enumerate() {
772 let weight = segment.length.max(1e-9);
773 let from = graph.node(&segment.first_port, segment.first_side);
776 let to = graph.node(&segment.second_port, segment.second_side.other());
777 graph.edges[from].push(Edge {
778 to,
779 weight,
780 segment: segment_index,
781 });
782 let from = graph.node(&segment.second_port, segment.second_side);
784 let to = graph.node(&segment.first_port, segment.first_side.other());
785 graph.edges[from].push(Edge {
786 to,
787 weight,
788 segment: segment_index,
789 });
790 }
791 graph
792 }
793
794 fn add_port(&mut self, id: &str) -> usize {
795 if let Some(position) = self.ports.iter().position(|p| p == id) {
796 return position;
797 }
798 let port_index = self.ports.len();
799 self.ports.push(id.to_string());
800 for side in [PortSide::A, PortSide::B] {
801 let key = node_key(id, side);
802 self.index.insert(key.clone(), self.keys.len());
803 self.keys.push(key);
804 self.port_of.push(port_index);
805 self.edges.push(Vec::new());
806 }
807 port_index
808 }
809
810 fn node(&mut self, port: &str, side: PortSide) -> usize {
811 let key = node_key(port, side);
812 if let Some(&index) = self.index.get(&key) {
813 return index;
814 }
815 self.add_port(port);
816 self.index[&key]
817 }
818
819 fn node_index(&self, port: &str, side: PortSide) -> Option<usize> {
820 self.index.get(&node_key(port, side)).copied()
821 }
822}
823
824#[derive(Debug, Clone, PartialEq)]
826struct SidedPath {
827 distance: f64,
828 nodes: Vec<usize>,
830 segments: Vec<usize>,
832}
833
834#[derive(PartialEq)]
836struct HeapEntry<T> {
837 cost: f64,
838 item: T,
839}
840
841impl<T: PartialEq> Eq for HeapEntry<T> {}
842
843impl<T: PartialEq> PartialOrd for HeapEntry<T> {
844 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
845 Some(self.cmp(other))
846 }
847}
848
849impl<T: PartialEq> Ord for HeapEntry<T> {
850 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
851 other
853 .cost
854 .partial_cmp(&self.cost)
855 .unwrap_or(std::cmp::Ordering::Equal)
856 }
857}
858
859fn dijkstra(graph: &SidedGraph, start: usize, end: usize) -> Option<SidedPath> {
861 let count = graph.keys.len();
862 let mut distance = vec![f64::INFINITY; count];
863 let mut parent: Vec<Option<(usize, usize)>> = vec![None; count];
864 let mut done = vec![false; count];
865 let mut heap = BinaryHeap::new();
866 distance[start] = 0.0;
867 heap.push(HeapEntry {
868 cost: 0.0,
869 item: start,
870 });
871 while let Some(HeapEntry { cost, item: node }) = heap.pop() {
872 if done[node] {
873 continue;
874 }
875 done[node] = true;
876 if node == end {
877 break;
878 }
879 for edge in &graph.edges[node] {
880 let next = cost + edge.weight;
881 if next < distance[edge.to] {
882 distance[edge.to] = next;
883 parent[edge.to] = Some((node, edge.segment));
884 heap.push(HeapEntry {
885 cost: next,
886 item: edge.to,
887 });
888 }
889 }
890 }
891 if !distance[end].is_finite() || start == end {
892 return None;
893 }
894 let mut nodes = vec![end];
895 let mut segments = Vec::new();
896 let mut cursor = end;
897 while let Some((previous, segment)) = parent[cursor] {
898 segments.push(segment);
899 nodes.push(previous);
900 cursor = previous;
901 }
902 nodes.reverse();
903 segments.reverse();
904 Some(SidedPath {
905 distance: distance[end],
906 nodes,
907 segments,
908 })
909}
910
911fn reuses_a_port(graph: &SidedGraph, path: &SidedPath) -> bool {
913 let mut seen = BTreeSet::new();
914 path.nodes
915 .iter()
916 .any(|&node| !seen.insert(graph.port_of[node]))
917}
918
919fn shortest_non_reusing(graph: &SidedGraph, start: usize, end: usize) -> Option<SidedPath> {
924 #[derive(PartialEq)]
925 struct State {
926 node: usize,
927 visited: Vec<bool>,
928 nodes: Vec<usize>,
929 segments: Vec<usize>,
930 }
931 let port_count = graph.ports.len();
932 let mut best: HashMap<(usize, Vec<bool>), f64> = HashMap::new();
933 let mut heap = BinaryHeap::new();
934 let mut visited = vec![false; port_count];
935 visited[graph.port_of[start]] = true;
936 heap.push(HeapEntry {
937 cost: 0.0,
938 item: State {
939 node: start,
940 visited,
941 nodes: vec![start],
942 segments: Vec::new(),
943 },
944 });
945 while let Some(HeapEntry { cost, item: state }) = heap.pop() {
946 if state.node == end && !state.segments.is_empty() {
947 return Some(SidedPath {
948 distance: cost,
949 nodes: state.nodes,
950 segments: state.segments,
951 });
952 }
953 let key = (state.node, state.visited.clone());
954 if best.get(&key).is_some_and(|&known| known < cost - 1e-12) {
955 continue;
956 }
957 for edge in &graph.edges[state.node] {
958 let port = graph.port_of[edge.to];
959 if state.visited[port] {
960 continue;
961 }
962 let mut visited = state.visited.clone();
963 visited[port] = true;
964 let next_cost = cost + edge.weight;
965 let next_key = (edge.to, visited.clone());
966 if best.get(&next_key).is_some_and(|&known| known <= next_cost + 1e-12) {
967 continue;
968 }
969 best.insert(next_key, next_cost);
970 let mut nodes = state.nodes.clone();
971 nodes.push(edge.to);
972 let mut segments = state.segments.clone();
973 segments.push(edge.segment);
974 heap.push(HeapEntry {
975 cost: next_cost,
976 item: State {
977 node: edge.to,
978 visited,
979 nodes,
980 segments,
981 },
982 });
983 }
984 }
985 None
986}
987
988fn route_connection(
992 network: &Network,
993 graph: &SidedGraph,
994 connection: &WireHarnessConnection,
995) -> RouteResult {
996 let unrouted = |status: RouteStatus, message: String| RouteResult {
997 connection_id: connection.id.clone(),
998 feasible: false,
999 status,
1000 message,
1001 length: None,
1002 segment_ids: Vec::new(),
1003 node_path: Vec::new(),
1004 port_ids: Vec::new(),
1005 };
1006 let from = connection.from.trim();
1007 let to = connection.to.trim();
1008 for (label, id) in [("from", from), ("to", to)] {
1009 if id.is_empty() {
1010 return unrouted(RouteStatus::MissingEndpoint, format!("no {label} port"));
1011 }
1012 let Some(port) = network.ports.get(id) else {
1013 return unrouted(
1014 RouteStatus::MissingEndpoint,
1015 format!("{label} port '{id}' is not in the model"),
1016 );
1017 };
1018 if port.kind == PortKind::Waypoint {
1019 return unrouted(
1020 RouteStatus::WaypointEndpoint,
1021 format!("{label} port '{}' is a waypoint, not a termination", port.label),
1022 );
1023 }
1024 }
1025 if from == to {
1026 return unrouted(RouteStatus::SameEndpoint, "from and to are the same port".into());
1027 }
1028 if network.segments.is_empty() {
1029 return unrouted(
1030 RouteStatus::NoSegments,
1031 "no harness splines: attach a spline's end anchors to two ports".into(),
1032 );
1033 }
1034
1035 let mut best: Option<SidedPath> = None;
1036 for start_side in [PortSide::A, PortSide::B] {
1037 for end_side in [PortSide::A, PortSide::B] {
1038 let (Some(start), Some(end)) = (
1039 graph.node_index(from, start_side),
1040 graph.node_index(to, end_side),
1041 ) else {
1042 continue;
1043 };
1044 if let Some(path) = dijkstra(graph, start, end) {
1045 if best.as_ref().is_none_or(|b| path.distance < b.distance) {
1046 best = Some(path);
1047 }
1048 }
1049 }
1050 }
1051 let Some(mut path) = best else {
1052 return unrouted(
1053 RouteStatus::NoRoute,
1054 "no sided path joins the two ports (check which side each spline leaves its ports on)".into(),
1055 );
1056 };
1057 if reuses_a_port(graph, &path) {
1058 let mut alternative: Option<SidedPath> = None;
1059 for start_side in [PortSide::A, PortSide::B] {
1060 for end_side in [PortSide::A, PortSide::B] {
1061 let (Some(start), Some(end)) = (
1062 graph.node_index(from, start_side),
1063 graph.node_index(to, end_side),
1064 ) else {
1065 continue;
1066 };
1067 if let Some(found) = shortest_non_reusing(graph, start, end) {
1068 if alternative.as_ref().is_none_or(|b| found.distance < b.distance) {
1069 alternative = Some(found);
1070 }
1071 }
1072 }
1073 }
1074 match alternative {
1075 Some(found) => path = found,
1076 None => {
1077 return unrouted(
1078 RouteStatus::PortReuse,
1079 "every path passes through the same port twice".into(),
1080 )
1081 }
1082 }
1083 }
1084 RouteResult {
1085 connection_id: connection.id.clone(),
1086 feasible: true,
1087 status: RouteStatus::Routed,
1088 message: String::new(),
1089 length: Some(path.distance),
1090 segment_ids: path
1091 .segments
1092 .iter()
1093 .map(|&index| network.segments[index].id.clone())
1094 .collect(),
1095 node_path: path.nodes.iter().map(|&node| graph.keys[node].clone()).collect(),
1096 port_ids: path
1097 .nodes
1098 .iter()
1099 .map(|&node| graph.ports[graph.port_of[node]].clone())
1100 .collect(),
1101 }
1102}
1103
1104fn build_bundles(
1112 network: &Network,
1113 state: &WireHarnessState,
1114 routes: &[RouteResult],
1115 result: &mut FeatureResult,
1116) -> Vec<WireHarnessBundle> {
1117 let mut usage: Vec<(String, Vec<f64>, Vec<String>)> = Vec::new();
1119 for route in routes.iter().filter(|route| route.feasible) {
1120 let Some(connection) = state
1121 .connections
1122 .iter()
1123 .find(|connection| connection.id == route.connection_id)
1124 else {
1125 continue;
1126 };
1127 let diameter = connection.diameter.max(MIN_DIAMETER);
1128 for segment_id in &route.segment_ids {
1129 let entry = match usage.iter_mut().find(|(id, _, _)| id == segment_id) {
1130 Some(entry) => entry,
1131 None => {
1132 usage.push((segment_id.clone(), Vec::new(), Vec::new()));
1133 usage.last_mut().expect("just pushed")
1134 }
1135 };
1136 entry.1.push(diameter);
1137 if !entry.2.contains(&connection.id) {
1138 entry.2.push(connection.id.clone());
1139 }
1140 }
1141 }
1142
1143 let mut bundles = Vec::with_capacity(usage.len());
1144 for (segment_id, diameters, connection_ids) in usage {
1145 let Some(segment) = network.segment(&segment_id) else {
1146 continue;
1147 };
1148 let diameter = bundle_diameter(&diameters).max(MIN_DIAMETER);
1149 let mut bundle = WireHarnessBundle {
1150 segment_id: segment_id.clone(),
1151 solid_name: String::new(),
1152 wire_count: diameters.len(),
1153 diameter,
1154 length: segment.length,
1155 connection_ids,
1156 error: String::new(),
1157 };
1158 if state.build_bundles {
1159 let name = format!("{BUNDLE_SOLID_PREFIX}{segment_id}");
1160 match sweep_bundle(segment, diameter * 0.5, &name) {
1161 Ok(solid) => {
1162 result.added.push(common::register_added(solid, &name));
1163 bundle.solid_name = name;
1164 }
1165 Err(error) => bundle.error = error,
1166 }
1167 }
1168 bundles.push(bundle);
1169 }
1170 bundles
1171}
1172
1173fn sweep_bundle(segment: &Segment, radius: f64, name: &str) -> Result<crate::BrepSolid, String> {
1177 let start = segment.chain[0]
1178 .domain()
1179 .and_then(|[t0, _]| segment.chain[0].evaluate(t0))?;
1180 let tangent = end_tangent(&segment.chain, true)
1181 .normalized()
1182 .map_err(|_| "the chain starts with a zero tangent".to_string())?;
1183 let x_axis = tangent.perpendicular()?;
1184 let y_axis = tangent.cross(x_axis).normalized()?;
1185 let profile = vec![
1186 make_arc(start, x_axis, y_axis, radius, 0.0, std::f64::consts::PI)?,
1187 make_arc(start, x_axis, y_axis, radius, std::f64::consts::PI, std::f64::consts::TAU)?,
1188 ];
1189 let names: Vec<String> = (0..segment.chain.len())
1190 .map(|index| format!("{}:piece{index}", segment.id))
1191 .collect();
1192 let stations = (segment.chain.len() * STATIONS_PER_PIECE).clamp(32, 1024);
1193 let mut solid = crate::sweep_profile_along_chain_with_stations(
1194 &profile,
1195 &segment.chain,
1196 &names,
1197 stations,
1198 "a harness segment must be tangent-continuous (a spline always is; a zero extension at an anchor can leave a corner)",
1199 )?;
1200 let faces = &mut solid
1202 .shells
1203 .get_mut(0)
1204 .ok_or("the sweep produced no shell")?
1205 .faces;
1206 let expected = ["Wall0", "Wall1", "Start", "End"];
1207 if faces.len() != expected.len() {
1208 return Err(format!(
1209 "the sweep produced {} faces, expected {}",
1210 faces.len(),
1211 expected.len()
1212 ));
1213 }
1214 for (face, suffix) in faces.iter_mut().zip(expected) {
1215 face.name = Some(format!("{name}:{suffix}"));
1216 }
1217 Ok(solid)
1218}
1219
1220