1use std::cell::RefCell;
73use std::collections::{BTreeMap, BTreeSet, BinaryHeap, HashMap};
74
75use serde::{Deserialize, Serialize};
76
77use crate::feature_pipeline::features::common;
78use crate::feature_pipeline::{Env, FeatureResult, HistoryRequest, PortKind, PortRecord, SceneMap};
79use crate::{make_arc, NurbsCurve, Vec3};
80
81pub const WIRE_HARNESS_FEATURE_ID: &str = "WireHarness";
84pub const WIRE_HARNESS_FEATURE_TYPE: &str = "WH";
86pub const BUNDLE_SOLID_PREFIX: &str = "WireHarness:";
88
89const PACKING_EFFICIENCY: f64 = 0.75;
92const SAFETY_FACTOR: f64 = 1.1;
94const MIN_DIAMETER: f64 = 0.01;
97const LENGTH_SAMPLES: usize = 32;
101const STATIONS_PER_PIECE: usize = 12;
103const TANGENT_EPS: f64 = 1e-9;
105
106#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
112pub enum PortSide {
113 A,
114 B,
115}
116
117impl PortSide {
118 pub fn other(self) -> Self {
120 match self {
121 PortSide::A => PortSide::B,
122 PortSide::B => PortSide::A,
123 }
124 }
125
126 pub fn parse(text: &str) -> Option<Self> {
128 match text.trim() {
129 "A" | "a" => Some(PortSide::A),
130 "B" | "b" => Some(PortSide::B),
131 _ => None,
132 }
133 }
134
135 pub fn letter(self) -> &'static str {
136 match self {
137 PortSide::A => "A",
138 PortSide::B => "B",
139 }
140 }
141}
142
143#[derive(Debug, Clone, PartialEq, Eq)]
146pub struct Attachment {
147 pub port_ref: String,
148 pub side: PortSide,
149}
150
151impl Attachment {
152 pub fn parse(value: Option<&serde_json::Value>) -> Option<Self> {
155 let object = value?.as_object()?;
156 let port_ref = object.get("portRef")?.as_str()?.trim();
157 if port_ref.is_empty() {
158 return None;
159 }
160 let side = object
161 .get("side")
162 .and_then(serde_json::Value::as_str)
163 .and_then(PortSide::parse)
164 .unwrap_or(PortSide::A);
165 Some(Attachment {
166 port_ref: port_ref.to_string(),
167 side,
168 })
169 }
170}
171
172fn connections_or_null<'de, D>(deserializer: D) -> Result<Vec<WireHarnessConnection>, D::Error>
177where
178 D: serde::Deserializer<'de>,
179{
180 Ok(Option::<Vec<WireHarnessConnection>>::deserialize(deserializer)?.unwrap_or_default())
181}
182
183#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
185pub struct WireHarnessState {
186 #[serde(default, deserialize_with = "connections_or_null")]
190 pub connections: Vec<WireHarnessConnection>,
191 #[serde(default, rename = "idCounter")]
193 pub id_counter: u64,
194 #[serde(default = "default_true", rename = "buildBundles")]
197 pub build_bundles: bool,
198 #[serde(default, rename = "cutMargin", skip_serializing_if = "is_zero")]
209 pub cut_margin: f64,
210}
211
212impl Default for WireHarnessState {
213 fn default() -> Self {
214 Self {
215 connections: Vec::new(),
216 id_counter: 0,
217 build_bundles: true,
218 cut_margin: 0.0,
219 }
220 }
221}
222
223impl WireHarnessState {
224 pub fn next_id(&mut self) -> String {
226 self.id_counter += 1;
227 format!("wire-{}", self.id_counter)
228 }
229}
230
231#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
233pub struct WireHarnessConnection {
234 pub id: String,
235 #[serde(default)]
236 pub name: String,
237 #[serde(default)]
238 pub from: String,
239 #[serde(default)]
240 pub to: String,
241 #[serde(default = "default_diameter")]
242 pub diameter: f64,
243}
244
245fn default_true() -> bool {
246 true
247}
248
249fn is_zero(value: &f64) -> bool {
250 *value == 0.0
251}
252
253fn default_diameter() -> f64 {
254 1.0
255}
256
257#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
263pub struct WireHarnessReport {
264 pub endpoints: Vec<WireHarnessEndpoint>,
266 pub segments: Vec<WireHarnessSegment>,
268 pub routes: Vec<RouteResult>,
270 pub bundles: Vec<WireHarnessBundle>,
272 pub segment_problems: Vec<String>,
275}
276
277#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
279pub struct WireHarnessEndpoint {
280 pub id: String,
283 #[serde(default)]
285 pub port: String,
286 #[serde(default)]
289 pub point: String,
290 #[serde(default)]
293 pub purpose: String,
294 pub kind: PortKind,
295 #[serde(default, skip_serializing_if = "Option::is_none")]
299 pub component: Option<String>,
300}
301
302#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
305pub struct WireHarnessSegment {
306 pub id: String,
307 pub first_port: String,
308 pub first_side: PortSide,
309 pub second_port: String,
310 pub second_side: PortSide,
311 pub length: f64,
312}
313
314#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
316#[serde(rename_all = "kebab-case")]
317pub enum RouteStatus {
318 Routed,
320 MissingEndpoint,
322 WaypointEndpoint,
324 SameEndpoint,
326 NoSegments,
328 NoRoute,
330 PortReuse,
332}
333
334impl RouteStatus {
335 pub fn as_str(self) -> &'static str {
337 match self {
338 RouteStatus::Routed => "routed",
339 RouteStatus::MissingEndpoint => "missing-endpoint",
340 RouteStatus::WaypointEndpoint => "waypoint-endpoint",
341 RouteStatus::SameEndpoint => "same-endpoint",
342 RouteStatus::NoSegments => "no-segments",
343 RouteStatus::NoRoute => "no-route",
344 RouteStatus::PortReuse => "port-reuse",
345 }
346 }
347}
348
349#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
351pub struct RouteResult {
352 pub connection_id: String,
353 pub feasible: bool,
354 pub status: RouteStatus,
355 pub message: String,
357 pub length: Option<f64>,
359 pub segment_ids: Vec<String>,
361 pub node_path: Vec<String>,
366 pub port_ids: Vec<String>,
368}
369
370#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
379#[serde(rename_all = "kebab-case")]
380pub enum BundleStatus {
381 Built,
383 BundlesOff,
386 TightBend,
390 BuildFailed,
392}
393
394impl BundleStatus {
395 pub fn as_str(self) -> &'static str {
397 match self {
398 BundleStatus::Built => "built",
399 BundleStatus::BundlesOff => "bundles-off",
400 BundleStatus::TightBend => "tight-bend",
401 BundleStatus::BuildFailed => "build-failed",
402 }
403 }
404}
405
406#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
408pub struct WireHarnessBundle {
409 pub segment_id: String,
410 pub solid_name: String,
413 pub wire_count: usize,
414 pub diameter: f64,
415 pub length: f64,
416 pub connection_ids: Vec<String>,
417 pub status: BundleStatus,
419 pub error: String,
422}
423
424pub fn bundle_diameter(diameters: &[f64]) -> f64 {
427 let sum_squares: f64 = diameters
428 .iter()
429 .map(|d| d.max(0.0))
430 .map(|d| d * d)
431 .sum();
432 if sum_squares <= 0.0 {
433 return 0.0;
434 }
435 (sum_squares / PACKING_EFFICIENCY).sqrt() * SAFETY_FACTOR
436}
437
438pub(crate) struct HarnessOutcome {
444 pub result: Option<FeatureResult>,
447 pub report: Option<WireHarnessReport>,
450}
451
452pub(crate) fn finish_history_run(
456 request: &HistoryRequest,
457 scene: &SceneMap,
458 _env: &Env,
459) -> HarnessOutcome {
460 let network = build_network(request, scene);
461 let Some(state) = request.wire_harness.as_ref() else {
462 clear_cache();
464 let report = WireHarnessReport {
465 endpoints: network.endpoints(),
466 segments: network.segment_rows(),
467 segment_problems: network.problems.clone(),
468 ..Default::default()
469 };
470 return HarnessOutcome {
471 result: None,
472 report: Some(report),
473 };
474 };
475
476 let fingerprint = harness_fingerprint(state, &network);
477 let cached = HARNESS_CACHE.with(|cache| {
478 cache.borrow().as_ref().and_then(|entry| {
479 (entry.fingerprint == fingerprint).then(|| (entry.result.clone(), entry.report.clone()))
480 })
481 });
482 if let Some((mut result, report)) = cached {
483 result.reused = true;
484 return HarnessOutcome {
485 result: Some(result),
486 report: Some(report),
487 };
488 }
489 clear_cache();
491
492 let mut report = WireHarnessReport {
493 endpoints: network.endpoints(),
494 segments: network.segment_rows(),
495 segment_problems: network.problems.clone(),
496 ..Default::default()
497 };
498 let graph = SidedGraph::build(&network);
499 for connection in &state.connections {
500 report.routes.push(route_connection(&network, &graph, connection));
501 }
502 let mut result = FeatureResult::empty(WIRE_HARNESS_FEATURE_ID, WIRE_HARNESS_FEATURE_TYPE);
503 report.bundles = build_bundles(&network, state, &report.routes, &mut result);
504
505 HARNESS_CACHE.with(|cache| {
506 *cache.borrow_mut() = Some(CachedHarness {
507 fingerprint,
508 result: result.clone(),
509 report: report.clone(),
510 });
511 });
512 HarnessOutcome {
513 result: Some(result),
514 report: Some(report),
515 }
516}
517
518struct CachedHarness {
519 fingerprint: u64,
520 result: FeatureResult,
521 report: WireHarnessReport,
522}
523
524thread_local! {
525 static HARNESS_CACHE: RefCell<Option<CachedHarness>> = const { RefCell::new(None) };
526}
527
528pub fn clear_cache() {
530 HARNESS_CACHE.with(|cache| {
531 if let Some(entry) = cache.borrow_mut().take() {
532 for added in &entry.result.added {
533 crate::free_registered_solid(added.handle);
534 }
535 }
536 });
537}
538
539fn harness_fingerprint(state: &WireHarnessState, network: &Network) -> u64 {
548 use std::hash::{Hash, Hasher};
549 let mut hasher = std::collections::hash_map::DefaultHasher::new();
550 state.build_bundles.hash(&mut hasher);
551 state.connections.len().hash(&mut hasher);
552 for connection in &state.connections {
553 connection.id.hash(&mut hasher);
554 connection.from.hash(&mut hasher);
555 connection.to.hash(&mut hasher);
556 connection.diameter.to_bits().hash(&mut hasher);
557 }
558 let bits = |v: Vec3, hasher: &mut std::collections::hash_map::DefaultHasher| {
559 v.x.to_bits().hash(hasher);
560 v.y.to_bits().hash(hasher);
561 v.z.to_bits().hash(hasher);
562 };
563 for (id, port) in &network.ports {
564 id.hash(&mut hasher);
565 port.local_address().hash(&mut hasher);
566 bits(port.point, &mut hasher);
567 bits(port.direction, &mut hasher);
568 (port.kind == PortKind::Waypoint).hash(&mut hasher);
569 port.extension.to_bits().hash(&mut hasher);
570 }
571 for segment in &network.segments {
572 segment.id.hash(&mut hasher);
573 segment.first_port.hash(&mut hasher);
574 segment.first_side.hash(&mut hasher);
575 segment.second_port.hash(&mut hasher);
576 segment.second_side.hash(&mut hasher);
577 segment.length.to_bits().hash(&mut hasher);
578 for curve in &segment.chain {
579 curve.degree.hash(&mut hasher);
580 for knot in &curve.knots {
581 knot.to_bits().hash(&mut hasher);
582 }
583 for point in &curve.control_points {
584 point.x.to_bits().hash(&mut hasher);
585 point.y.to_bits().hash(&mut hasher);
586 point.z.to_bits().hash(&mut hasher);
587 point.w.to_bits().hash(&mut hasher);
588 }
589 }
590 }
591 hasher.finish()
592}
593
594struct Segment {
600 id: String,
602 first_port: String,
603 first_side: PortSide,
605 second_port: String,
606 second_side: PortSide,
607 length: f64,
608 chain: Vec<NurbsCurve>,
609}
610
611struct Network {
612 ports: BTreeMap<String, PortRecord>,
614 owners: BTreeMap<String, String>,
616 segments: Vec<Segment>,
618 problems: Vec<String>,
619}
620
621impl Network {
622 fn endpoints(&self) -> Vec<WireHarnessEndpoint> {
623 self.ports
624 .iter()
625 .map(|(id, port)| WireHarnessEndpoint {
626 id: id.clone(),
627 port: port.port_name.clone(),
628 point: port.point_name.clone(),
629 purpose: port.purpose.clone(),
630 kind: port.kind,
631 component: self.owners.get(id).cloned(),
632 })
633 .collect()
634 }
635
636 fn segment_rows(&self) -> Vec<WireHarnessSegment> {
637 self.segments
638 .iter()
639 .map(|segment| WireHarnessSegment {
640 id: segment.id.clone(),
641 first_port: segment.first_port.clone(),
642 first_side: segment.first_side,
643 second_port: segment.second_port.clone(),
644 second_side: segment.second_side,
645 length: segment.length,
646 })
647 .collect()
648 }
649
650 fn segment(&self, id: &str) -> Option<&Segment> {
651 self.segments.iter().find(|segment| segment.id == id)
652 }
653}
654
655fn build_network(request: &HistoryRequest, scene: &SceneMap) -> Network {
659 let ports: BTreeMap<String, PortRecord> = scene
660 .ports
661 .iter()
662 .map(|(id, port)| (id.clone(), port.clone()))
663 .collect();
664 let owners: BTreeMap<String, String> = ports
665 .keys()
666 .filter_map(|id| {
667 scene
668 .owning_component(id)
669 .map(|record| (id.clone(), record.id.clone()))
670 })
671 .collect();
672 let mut segments = Vec::new();
673 let mut problems = Vec::new();
674 for descriptor in &request.features {
675 if !matches!(descriptor.feature_type.as_str(), "SP" | "SPLINE") {
676 continue;
677 }
678 let id = ["id", "featureID"]
679 .iter()
680 .find_map(|key| descriptor.input_params.get(key).and_then(|v| v.as_str()))
681 .unwrap_or("")
682 .trim()
683 .to_string();
684 if id.is_empty() {
685 continue;
686 }
687 let Some(points) = descriptor
688 .persistent_data
689 .get("spline")
690 .and_then(|spline| spline.get("points"))
691 .and_then(serde_json::Value::as_array)
692 else {
693 continue;
694 };
695 if points.len() < 2 {
696 continue;
697 }
698 let first = Attachment::parse(points.first().and_then(|p| p.get("attachment")));
699 let last = Attachment::parse(points.last().and_then(|p| p.get("attachment")));
700 let (first, last) = match (first, last) {
701 (Some(first), Some(last)) => (first, last),
702 (Some(_), None) | (None, Some(_)) => {
706 problems.push(format!("{id}: only one end is attached to a port"));
707 continue;
708 }
709 (None, None) => continue,
710 };
711 if first.port_ref == last.port_ref {
712 problems.push(format!("{id}: both ends attach to the same port '{}'", first.port_ref));
713 continue;
714 }
715 let (Some(first_record), Some(last_record)) =
716 (ports.get(&first.port_ref), ports.get(&last.port_ref))
717 else {
718 continue;
720 };
721 let Some(chain) = scene.resolve_path(&format!("{id}:SplineEdge")).cloned() else {
722 continue; };
724 if chain.is_empty() {
725 continue;
726 }
727 let length = chain_length(&chain);
728 let first_side = physical_side(first_record, end_tangent(&chain, true), first.side);
729 let second_side = physical_side(
732 last_record,
733 end_tangent(&chain, false).scale(-1.0),
734 last.side.other(),
735 );
736 segments.push(Segment {
737 id,
738 first_port: first.port_ref,
739 first_side,
740 second_port: last.port_ref,
741 second_side,
742 length,
743 chain,
744 });
745 }
746 Network {
747 ports,
748 owners,
749 segments,
750 problems,
751 }
752}
753
754fn physical_side(port: &PortRecord, outward: Vec3, fallback: PortSide) -> PortSide {
758 if outward.length() <= TANGENT_EPS {
759 return fallback;
760 }
761 if outward.dot(port.direction) >= 0.0 {
762 PortSide::A
763 } else {
764 PortSide::B
765 }
766}
767
768fn end_tangent(chain: &[NurbsCurve], at_start: bool) -> Vec3 {
772 let curve = if at_start { chain.first() } else { chain.last() };
773 let Some(curve) = curve else {
774 return Vec3::new(0.0, 0.0, 0.0);
775 };
776 let Ok([t0, t1]) = curve.domain() else {
777 return Vec3::new(0.0, 0.0, 0.0);
778 };
779 let parameter = if at_start { t0 } else { t1 };
780 if let Ok(derivatives) = curve.derivatives(parameter, 1) {
781 if let Some(tangent) = derivatives.get(1) {
782 if tangent.length() > TANGENT_EPS {
783 return *tangent;
784 }
785 }
786 }
787 let near = if at_start {
788 t0 + (t1 - t0) * 0.05
789 } else {
790 t1 - (t1 - t0) * 0.05
791 };
792 match (curve.evaluate(parameter), curve.evaluate(near)) {
793 (Ok(at), Ok(nearby)) => {
794 if at_start {
795 nearby.sub(at)
796 } else {
797 at.sub(nearby)
798 }
799 }
800 _ => Vec3::new(0.0, 0.0, 0.0),
801 }
802}
803
804fn chain_length(chain: &[NurbsCurve]) -> f64 {
809 let mut total = 0.0;
810 for curve in chain {
811 let Ok([t0, t1]) = curve.domain() else { continue };
812 total += match crate::curve_arc_length(curve, t0, t1) {
813 Ok(length) => length,
814 Err(_) => chord_length(curve, t0, t1),
815 };
816 }
817 total
818}
819
820fn chord_length(curve: &NurbsCurve, t0: f64, t1: f64) -> f64 {
822 let Ok(mut previous) = curve.evaluate(t0) else { return 0.0 };
823 let mut total = 0.0;
824 for sample in 1..=LENGTH_SAMPLES {
825 let t = t0 + (t1 - t0) * sample as f64 / LENGTH_SAMPLES as f64;
826 if let Ok(point) = curve.evaluate(t) {
827 total += point.sub(previous).length();
828 previous = point;
829 }
830 }
831 total
832}
833
834fn node_key(port: &str, side: PortSide) -> String {
839 format!("{port}/{}", side.letter())
840}
841
842struct Edge {
843 to: usize,
844 weight: f64,
845 segment: usize,
847}
848
849struct SidedGraph {
850 index: HashMap<String, usize>,
852 keys: Vec<String>,
853 port_of: Vec<usize>,
855 ports: Vec<String>,
856 edges: Vec<Vec<Edge>>,
857}
858
859impl SidedGraph {
860 fn build(network: &Network) -> Self {
861 let mut graph = SidedGraph {
862 index: HashMap::new(),
863 keys: Vec::new(),
864 port_of: Vec::new(),
865 ports: Vec::new(),
866 edges: Vec::new(),
867 };
868 for id in network.ports.keys() {
871 graph.add_port(id);
872 }
873 for (segment_index, segment) in network.segments.iter().enumerate() {
874 let weight = segment.length.max(1e-9);
875 let from = graph.node(&segment.first_port, segment.first_side);
878 let to = graph.node(&segment.second_port, segment.second_side.other());
879 graph.edges[from].push(Edge {
880 to,
881 weight,
882 segment: segment_index,
883 });
884 let from = graph.node(&segment.second_port, segment.second_side);
886 let to = graph.node(&segment.first_port, segment.first_side.other());
887 graph.edges[from].push(Edge {
888 to,
889 weight,
890 segment: segment_index,
891 });
892 }
893 graph
894 }
895
896 fn add_port(&mut self, id: &str) -> usize {
897 if let Some(position) = self.ports.iter().position(|p| p == id) {
898 return position;
899 }
900 let port_index = self.ports.len();
901 self.ports.push(id.to_string());
902 for side in [PortSide::A, PortSide::B] {
903 let key = node_key(id, side);
904 self.index.insert(key.clone(), self.keys.len());
905 self.keys.push(key);
906 self.port_of.push(port_index);
907 self.edges.push(Vec::new());
908 }
909 port_index
910 }
911
912 fn node(&mut self, port: &str, side: PortSide) -> usize {
913 let key = node_key(port, side);
914 if let Some(&index) = self.index.get(&key) {
915 return index;
916 }
917 self.add_port(port);
918 self.index[&key]
919 }
920
921 fn node_index(&self, port: &str, side: PortSide) -> Option<usize> {
922 self.index.get(&node_key(port, side)).copied()
923 }
924}
925
926#[derive(Debug, Clone, PartialEq)]
928struct SidedPath {
929 distance: f64,
930 nodes: Vec<usize>,
932 segments: Vec<usize>,
934}
935
936#[derive(PartialEq)]
938struct HeapEntry<T> {
939 cost: f64,
940 item: T,
941}
942
943impl<T: PartialEq> Eq for HeapEntry<T> {}
944
945impl<T: PartialEq> PartialOrd for HeapEntry<T> {
946 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
947 Some(self.cmp(other))
948 }
949}
950
951impl<T: PartialEq> Ord for HeapEntry<T> {
952 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
953 other
955 .cost
956 .partial_cmp(&self.cost)
957 .unwrap_or(std::cmp::Ordering::Equal)
958 }
959}
960
961fn dijkstra(graph: &SidedGraph, start: usize, end: usize) -> Option<SidedPath> {
963 let count = graph.keys.len();
964 let mut distance = vec![f64::INFINITY; count];
965 let mut parent: Vec<Option<(usize, usize)>> = vec![None; count];
966 let mut done = vec![false; count];
967 let mut heap = BinaryHeap::new();
968 distance[start] = 0.0;
969 heap.push(HeapEntry {
970 cost: 0.0,
971 item: start,
972 });
973 while let Some(HeapEntry { cost, item: node }) = heap.pop() {
974 if done[node] {
975 continue;
976 }
977 done[node] = true;
978 if node == end {
979 break;
980 }
981 for edge in &graph.edges[node] {
982 let next = cost + edge.weight;
983 if next < distance[edge.to] {
984 distance[edge.to] = next;
985 parent[edge.to] = Some((node, edge.segment));
986 heap.push(HeapEntry {
987 cost: next,
988 item: edge.to,
989 });
990 }
991 }
992 }
993 if !distance[end].is_finite() || start == end {
994 return None;
995 }
996 let mut nodes = vec![end];
997 let mut segments = Vec::new();
998 let mut cursor = end;
999 while let Some((previous, segment)) = parent[cursor] {
1000 segments.push(segment);
1001 nodes.push(previous);
1002 cursor = previous;
1003 }
1004 nodes.reverse();
1005 segments.reverse();
1006 Some(SidedPath {
1007 distance: distance[end],
1008 nodes,
1009 segments,
1010 })
1011}
1012
1013fn reuses_a_port(graph: &SidedGraph, path: &SidedPath) -> bool {
1015 let mut seen = BTreeSet::new();
1016 path.nodes
1017 .iter()
1018 .any(|&node| !seen.insert(graph.port_of[node]))
1019}
1020
1021fn shortest_non_reusing(graph: &SidedGraph, start: usize, end: usize) -> Option<SidedPath> {
1026 #[derive(PartialEq)]
1027 struct State {
1028 node: usize,
1029 visited: Vec<bool>,
1030 nodes: Vec<usize>,
1031 segments: Vec<usize>,
1032 }
1033 let port_count = graph.ports.len();
1034 let mut best: HashMap<(usize, Vec<bool>), f64> = HashMap::new();
1035 let mut heap = BinaryHeap::new();
1036 let mut visited = vec![false; port_count];
1037 visited[graph.port_of[start]] = true;
1038 heap.push(HeapEntry {
1039 cost: 0.0,
1040 item: State {
1041 node: start,
1042 visited,
1043 nodes: vec![start],
1044 segments: Vec::new(),
1045 },
1046 });
1047 while let Some(HeapEntry { cost, item: state }) = heap.pop() {
1048 if state.node == end && !state.segments.is_empty() {
1049 return Some(SidedPath {
1050 distance: cost,
1051 nodes: state.nodes,
1052 segments: state.segments,
1053 });
1054 }
1055 let key = (state.node, state.visited.clone());
1056 if best.get(&key).is_some_and(|&known| known < cost - 1e-12) {
1057 continue;
1058 }
1059 for edge in &graph.edges[state.node] {
1060 let port = graph.port_of[edge.to];
1061 if state.visited[port] {
1062 continue;
1063 }
1064 let mut visited = state.visited.clone();
1065 visited[port] = true;
1066 let next_cost = cost + edge.weight;
1067 let next_key = (edge.to, visited.clone());
1068 if best.get(&next_key).is_some_and(|&known| known <= next_cost + 1e-12) {
1069 continue;
1070 }
1071 best.insert(next_key, next_cost);
1072 let mut nodes = state.nodes.clone();
1073 nodes.push(edge.to);
1074 let mut segments = state.segments.clone();
1075 segments.push(edge.segment);
1076 heap.push(HeapEntry {
1077 cost: next_cost,
1078 item: State {
1079 node: edge.to,
1080 visited,
1081 nodes,
1082 segments,
1083 },
1084 });
1085 }
1086 }
1087 None
1088}
1089
1090fn route_connection(
1094 network: &Network,
1095 graph: &SidedGraph,
1096 connection: &WireHarnessConnection,
1097) -> RouteResult {
1098 let unrouted = |status: RouteStatus, message: String| RouteResult {
1099 connection_id: connection.id.clone(),
1100 feasible: false,
1101 status,
1102 message,
1103 length: None,
1104 segment_ids: Vec::new(),
1105 node_path: Vec::new(),
1106 port_ids: Vec::new(),
1107 };
1108 let from = connection.from.trim();
1109 let to = connection.to.trim();
1110 for (label, id) in [("from", from), ("to", to)] {
1111 if id.is_empty() {
1112 return unrouted(RouteStatus::MissingEndpoint, format!("no {label} port"));
1113 }
1114 let Some(port) = network.ports.get(id) else {
1115 return unrouted(
1116 RouteStatus::MissingEndpoint,
1117 format!("{label} port '{id}' is not in the model"),
1118 );
1119 };
1120 if port.kind == PortKind::Waypoint {
1121 return unrouted(
1122 RouteStatus::WaypointEndpoint,
1123 format!("{label} port '{}' is a waypoint, not a termination", port.local_address()),
1124 );
1125 }
1126 }
1127 if from == to {
1128 return unrouted(RouteStatus::SameEndpoint, "from and to are the same port".into());
1129 }
1130 if network.segments.is_empty() {
1131 return unrouted(
1132 RouteStatus::NoSegments,
1133 "no harness splines: attach a spline's end anchors to two ports".into(),
1134 );
1135 }
1136
1137 let mut best: Option<SidedPath> = None;
1138 for start_side in [PortSide::A, PortSide::B] {
1139 for end_side in [PortSide::A, PortSide::B] {
1140 let (Some(start), Some(end)) = (
1141 graph.node_index(from, start_side),
1142 graph.node_index(to, end_side),
1143 ) else {
1144 continue;
1145 };
1146 if let Some(path) = dijkstra(graph, start, end) {
1147 if best.as_ref().is_none_or(|b| path.distance < b.distance) {
1148 best = Some(path);
1149 }
1150 }
1151 }
1152 }
1153 let Some(mut path) = best else {
1154 return unrouted(
1155 RouteStatus::NoRoute,
1156 "no sided path joins the two ports (check which side each spline leaves its ports on)".into(),
1157 );
1158 };
1159 if reuses_a_port(graph, &path) {
1160 let mut alternative: Option<SidedPath> = None;
1161 for start_side in [PortSide::A, PortSide::B] {
1162 for end_side in [PortSide::A, PortSide::B] {
1163 let (Some(start), Some(end)) = (
1164 graph.node_index(from, start_side),
1165 graph.node_index(to, end_side),
1166 ) else {
1167 continue;
1168 };
1169 if let Some(found) = shortest_non_reusing(graph, start, end) {
1170 if alternative.as_ref().is_none_or(|b| found.distance < b.distance) {
1171 alternative = Some(found);
1172 }
1173 }
1174 }
1175 }
1176 match alternative {
1177 Some(found) => path = found,
1178 None => {
1179 return unrouted(
1180 RouteStatus::PortReuse,
1181 "every path passes through the same port twice".into(),
1182 )
1183 }
1184 }
1185 }
1186 RouteResult {
1187 connection_id: connection.id.clone(),
1188 feasible: true,
1189 status: RouteStatus::Routed,
1190 message: String::new(),
1191 length: Some(path.distance),
1192 segment_ids: path
1193 .segments
1194 .iter()
1195 .map(|&index| network.segments[index].id.clone())
1196 .collect(),
1197 node_path: path.nodes.iter().map(|&node| graph.keys[node].clone()).collect(),
1198 port_ids: path
1199 .nodes
1200 .iter()
1201 .map(|&node| graph.ports[graph.port_of[node]].clone())
1202 .collect(),
1203 }
1204}
1205
1206fn build_bundles(
1214 network: &Network,
1215 state: &WireHarnessState,
1216 routes: &[RouteResult],
1217 result: &mut FeatureResult,
1218) -> Vec<WireHarnessBundle> {
1219 let mut usage: Vec<(String, Vec<f64>, Vec<String>)> = Vec::new();
1221 for route in routes.iter().filter(|route| route.feasible) {
1222 let Some(connection) = state
1223 .connections
1224 .iter()
1225 .find(|connection| connection.id == route.connection_id)
1226 else {
1227 continue;
1228 };
1229 let diameter = connection.diameter.max(MIN_DIAMETER);
1230 for segment_id in &route.segment_ids {
1231 let entry = match usage.iter_mut().find(|(id, _, _)| id == segment_id) {
1232 Some(entry) => entry,
1233 None => {
1234 usage.push((segment_id.clone(), Vec::new(), Vec::new()));
1235 usage.last_mut().expect("just pushed")
1236 }
1237 };
1238 entry.1.push(diameter);
1239 if !entry.2.contains(&connection.id) {
1240 entry.2.push(connection.id.clone());
1241 }
1242 }
1243 }
1244
1245 let mut bundles = Vec::with_capacity(usage.len());
1246 for (segment_id, diameters, connection_ids) in usage {
1247 let Some(segment) = network.segment(&segment_id) else {
1248 continue;
1249 };
1250 let diameter = bundle_diameter(&diameters).max(MIN_DIAMETER);
1251 let mut bundle = WireHarnessBundle {
1252 segment_id: segment_id.clone(),
1253 solid_name: String::new(),
1254 wire_count: diameters.len(),
1255 diameter,
1256 length: segment.length,
1257 connection_ids,
1258 status: BundleStatus::BundlesOff,
1259 error: String::new(),
1260 };
1261 if state.build_bundles {
1262 let name = format!("{BUNDLE_SOLID_PREFIX}{segment_id}");
1263 match sweep_bundle(segment, diameter * 0.5, &name) {
1264 Ok(swept) => {
1265 if let Some(report) = swept.report {
1267 result.approximations.push(report.approximation(name.clone()));
1268 }
1269 result.added.push(common::register_added(swept.solid, &name));
1270 bundle.solid_name = name;
1271 bundle.status = BundleStatus::Built;
1272 }
1273 Err(error) => {
1274 bundle.status = if crate::is_sweep_tight_bend(&error) {
1279 BundleStatus::TightBend
1280 } else {
1281 BundleStatus::BuildFailed
1282 };
1283 bundle.error = error.message;
1284 }
1285 }
1286 }
1287 bundles.push(bundle);
1288 }
1289 bundles
1290}
1291
1292fn sweep_bundle(
1296 segment: &Segment,
1297 radius: f64,
1298 name: &str,
1299) -> Result<crate::SweptChain, crate::KernelRefusal> {
1300 use crate::{KernelRefusal, KernelStage, OrRefuse};
1301 let start = segment.chain[0]
1302 .domain()
1303 .and_then(|[t0, _]| segment.chain[0].evaluate(t0))
1304 .or_refuse(KernelStage::Collect, "evaluate")?;
1305 let tangent = end_tangent(&segment.chain, true).normalized().map_err(|_| {
1306 KernelRefusal::input(KernelStage::Collect, "zero_tangent", "the chain starts with a zero tangent")
1307 })?;
1308 let x_axis = tangent.perpendicular().or_refuse(KernelStage::Collect, "perpendicular")?;
1309 let y_axis = tangent.cross(x_axis).normalized().or_refuse(KernelStage::Collect, "normalized")?;
1310 let profile = vec![
1311 make_arc(start, x_axis, y_axis, radius, 0.0, std::f64::consts::PI)
1312 .or_refuse(KernelStage::Fragment, "make_arc")?,
1313 make_arc(start, x_axis, y_axis, radius, std::f64::consts::PI, std::f64::consts::TAU)
1314 .or_refuse(KernelStage::Fragment, "make_arc")?,
1315 ];
1316 let names: Vec<String> = (0..segment.chain.len())
1317 .map(|index| format!("{}:piece{index}", segment.id))
1318 .collect();
1319 let stations = (segment.chain.len() * STATIONS_PER_PIECE).clamp(32, 1024);
1320 let path = crate::SweepPath::new(segment.chain.clone(), names)?;
1325 let crate::SweptChain { mut solid, report } = crate::sweep_profile_along_chain_with_stations_reported(
1326 &profile,
1327 &path,
1328 stations,
1329 "a harness segment must be tangent-continuous (a spline always is; a zero extension at an anchor can leave a corner)",
1330 None,
1331 )?;
1332 let faces = &mut solid
1334 .shells
1335 .get_mut(0)
1336 .ok_or_else(|| KernelRefusal::internal(KernelStage::Sew, "bundle_shell", "the sweep produced no shell"))?
1337 .faces;
1338 let expected = ["Wall0", "Wall1", "Start", "End"];
1339 if faces.len() != expected.len() {
1340 return Err(KernelRefusal::internal(
1341 KernelStage::Sew,
1342 "bundle_faces",
1343 format!("the sweep produced {} faces, expected {}", faces.len(), expected.len()),
1344 ));
1345 }
1346 for (face, suffix) in faces.iter_mut().zip(expected) {
1347 face.name = Some(format!("{name}:{suffix}"));
1348 }
1349 Ok(crate::SweptChain { solid, report })
1350}
1351