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
172#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
178pub struct WireHarnessState {
179 #[serde(default)]
180 pub connections: Vec<WireHarnessConnection>,
181 #[serde(default, rename = "idCounter")]
183 pub id_counter: u64,
184 #[serde(default = "default_true", rename = "buildBundles")]
187 pub build_bundles: bool,
188 #[serde(default, rename = "cutMargin", skip_serializing_if = "is_zero")]
199 pub cut_margin: f64,
200}
201
202impl Default for WireHarnessState {
203 fn default() -> Self {
204 Self {
205 connections: Vec::new(),
206 id_counter: 0,
207 build_bundles: true,
208 cut_margin: 0.0,
209 }
210 }
211}
212
213impl WireHarnessState {
214 pub fn next_id(&mut self) -> String {
216 self.id_counter += 1;
217 format!("wire-{}", self.id_counter)
218 }
219}
220
221#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
223pub struct WireHarnessConnection {
224 pub id: String,
225 #[serde(default)]
226 pub name: String,
227 #[serde(default)]
228 pub from: String,
229 #[serde(default)]
230 pub to: String,
231 #[serde(default = "default_diameter")]
232 pub diameter: f64,
233}
234
235fn default_true() -> bool {
236 true
237}
238
239fn is_zero(value: &f64) -> bool {
240 *value == 0.0
241}
242
243fn default_diameter() -> f64 {
244 1.0
245}
246
247#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
253pub struct WireHarnessReport {
254 pub endpoints: Vec<WireHarnessEndpoint>,
256 pub segments: Vec<WireHarnessSegment>,
258 pub routes: Vec<RouteResult>,
260 pub bundles: Vec<WireHarnessBundle>,
262 pub segment_problems: Vec<String>,
265}
266
267#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
269pub struct WireHarnessEndpoint {
270 pub id: String,
273 #[serde(default)]
275 pub port: String,
276 #[serde(default)]
279 pub point: String,
280 #[serde(default)]
283 pub purpose: String,
284 pub kind: PortKind,
285 #[serde(default, skip_serializing_if = "Option::is_none")]
289 pub component: Option<String>,
290}
291
292#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
295pub struct WireHarnessSegment {
296 pub id: String,
297 pub first_port: String,
298 pub first_side: PortSide,
299 pub second_port: String,
300 pub second_side: PortSide,
301 pub length: f64,
302}
303
304#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
306#[serde(rename_all = "kebab-case")]
307pub enum RouteStatus {
308 Routed,
310 MissingEndpoint,
312 WaypointEndpoint,
314 SameEndpoint,
316 NoSegments,
318 NoRoute,
320 PortReuse,
322}
323
324impl RouteStatus {
325 pub fn as_str(self) -> &'static str {
327 match self {
328 RouteStatus::Routed => "routed",
329 RouteStatus::MissingEndpoint => "missing-endpoint",
330 RouteStatus::WaypointEndpoint => "waypoint-endpoint",
331 RouteStatus::SameEndpoint => "same-endpoint",
332 RouteStatus::NoSegments => "no-segments",
333 RouteStatus::NoRoute => "no-route",
334 RouteStatus::PortReuse => "port-reuse",
335 }
336 }
337}
338
339#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
341pub struct RouteResult {
342 pub connection_id: String,
343 pub feasible: bool,
344 pub status: RouteStatus,
345 pub message: String,
347 pub length: Option<f64>,
349 pub segment_ids: Vec<String>,
351 pub node_path: Vec<String>,
356 pub port_ids: Vec<String>,
358}
359
360#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
369#[serde(rename_all = "kebab-case")]
370pub enum BundleStatus {
371 Built,
373 BundlesOff,
376 TightBend,
380 BuildFailed,
382}
383
384impl BundleStatus {
385 pub fn as_str(self) -> &'static str {
387 match self {
388 BundleStatus::Built => "built",
389 BundleStatus::BundlesOff => "bundles-off",
390 BundleStatus::TightBend => "tight-bend",
391 BundleStatus::BuildFailed => "build-failed",
392 }
393 }
394}
395
396#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
398pub struct WireHarnessBundle {
399 pub segment_id: String,
400 pub solid_name: String,
403 pub wire_count: usize,
404 pub diameter: f64,
405 pub length: f64,
406 pub connection_ids: Vec<String>,
407 pub status: BundleStatus,
409 pub error: String,
412}
413
414pub fn bundle_diameter(diameters: &[f64]) -> f64 {
417 let sum_squares: f64 = diameters
418 .iter()
419 .map(|d| d.max(0.0))
420 .map(|d| d * d)
421 .sum();
422 if sum_squares <= 0.0 {
423 return 0.0;
424 }
425 (sum_squares / PACKING_EFFICIENCY).sqrt() * SAFETY_FACTOR
426}
427
428pub(crate) struct HarnessOutcome {
434 pub result: Option<FeatureResult>,
437 pub report: Option<WireHarnessReport>,
440}
441
442pub(crate) fn finish_history_run(
446 request: &HistoryRequest,
447 scene: &SceneMap,
448 _env: &Env,
449) -> HarnessOutcome {
450 let network = build_network(request, scene);
451 let Some(state) = request.wire_harness.as_ref() else {
452 clear_cache();
454 let report = WireHarnessReport {
455 endpoints: network.endpoints(),
456 segments: network.segment_rows(),
457 segment_problems: network.problems.clone(),
458 ..Default::default()
459 };
460 return HarnessOutcome {
461 result: None,
462 report: Some(report),
463 };
464 };
465
466 let fingerprint = harness_fingerprint(state, &network);
467 let cached = HARNESS_CACHE.with(|cache| {
468 cache.borrow().as_ref().and_then(|entry| {
469 (entry.fingerprint == fingerprint).then(|| (entry.result.clone(), entry.report.clone()))
470 })
471 });
472 if let Some((mut result, report)) = cached {
473 result.reused = true;
474 return HarnessOutcome {
475 result: Some(result),
476 report: Some(report),
477 };
478 }
479 clear_cache();
481
482 let mut report = WireHarnessReport {
483 endpoints: network.endpoints(),
484 segments: network.segment_rows(),
485 segment_problems: network.problems.clone(),
486 ..Default::default()
487 };
488 let graph = SidedGraph::build(&network);
489 for connection in &state.connections {
490 report.routes.push(route_connection(&network, &graph, connection));
491 }
492 let mut result = FeatureResult::empty(WIRE_HARNESS_FEATURE_ID, WIRE_HARNESS_FEATURE_TYPE);
493 report.bundles = build_bundles(&network, state, &report.routes, &mut result);
494
495 HARNESS_CACHE.with(|cache| {
496 *cache.borrow_mut() = Some(CachedHarness {
497 fingerprint,
498 result: result.clone(),
499 report: report.clone(),
500 });
501 });
502 HarnessOutcome {
503 result: Some(result),
504 report: Some(report),
505 }
506}
507
508struct CachedHarness {
509 fingerprint: u64,
510 result: FeatureResult,
511 report: WireHarnessReport,
512}
513
514thread_local! {
515 static HARNESS_CACHE: RefCell<Option<CachedHarness>> = const { RefCell::new(None) };
516}
517
518pub fn clear_cache() {
520 HARNESS_CACHE.with(|cache| {
521 if let Some(entry) = cache.borrow_mut().take() {
522 for added in &entry.result.added {
523 crate::free_registered_solid(added.handle);
524 }
525 }
526 });
527}
528
529fn harness_fingerprint(state: &WireHarnessState, network: &Network) -> u64 {
538 use std::hash::{Hash, Hasher};
539 let mut hasher = std::collections::hash_map::DefaultHasher::new();
540 state.build_bundles.hash(&mut hasher);
541 state.connections.len().hash(&mut hasher);
542 for connection in &state.connections {
543 connection.id.hash(&mut hasher);
544 connection.from.hash(&mut hasher);
545 connection.to.hash(&mut hasher);
546 connection.diameter.to_bits().hash(&mut hasher);
547 }
548 let bits = |v: Vec3, hasher: &mut std::collections::hash_map::DefaultHasher| {
549 v.x.to_bits().hash(hasher);
550 v.y.to_bits().hash(hasher);
551 v.z.to_bits().hash(hasher);
552 };
553 for (id, port) in &network.ports {
554 id.hash(&mut hasher);
555 port.local_address().hash(&mut hasher);
556 bits(port.point, &mut hasher);
557 bits(port.direction, &mut hasher);
558 (port.kind == PortKind::Waypoint).hash(&mut hasher);
559 port.extension.to_bits().hash(&mut hasher);
560 }
561 for segment in &network.segments {
562 segment.id.hash(&mut hasher);
563 segment.first_port.hash(&mut hasher);
564 segment.first_side.hash(&mut hasher);
565 segment.second_port.hash(&mut hasher);
566 segment.second_side.hash(&mut hasher);
567 segment.length.to_bits().hash(&mut hasher);
568 for curve in &segment.chain {
569 curve.degree.hash(&mut hasher);
570 for knot in &curve.knots {
571 knot.to_bits().hash(&mut hasher);
572 }
573 for point in &curve.control_points {
574 point.x.to_bits().hash(&mut hasher);
575 point.y.to_bits().hash(&mut hasher);
576 point.z.to_bits().hash(&mut hasher);
577 point.w.to_bits().hash(&mut hasher);
578 }
579 }
580 }
581 hasher.finish()
582}
583
584struct Segment {
590 id: String,
592 first_port: String,
593 first_side: PortSide,
595 second_port: String,
596 second_side: PortSide,
597 length: f64,
598 chain: Vec<NurbsCurve>,
599}
600
601struct Network {
602 ports: BTreeMap<String, PortRecord>,
604 owners: BTreeMap<String, String>,
606 segments: Vec<Segment>,
608 problems: Vec<String>,
609}
610
611impl Network {
612 fn endpoints(&self) -> Vec<WireHarnessEndpoint> {
613 self.ports
614 .iter()
615 .map(|(id, port)| WireHarnessEndpoint {
616 id: id.clone(),
617 port: port.port_name.clone(),
618 point: port.point_name.clone(),
619 purpose: port.purpose.clone(),
620 kind: port.kind,
621 component: self.owners.get(id).cloned(),
622 })
623 .collect()
624 }
625
626 fn segment_rows(&self) -> Vec<WireHarnessSegment> {
627 self.segments
628 .iter()
629 .map(|segment| WireHarnessSegment {
630 id: segment.id.clone(),
631 first_port: segment.first_port.clone(),
632 first_side: segment.first_side,
633 second_port: segment.second_port.clone(),
634 second_side: segment.second_side,
635 length: segment.length,
636 })
637 .collect()
638 }
639
640 fn segment(&self, id: &str) -> Option<&Segment> {
641 self.segments.iter().find(|segment| segment.id == id)
642 }
643}
644
645fn build_network(request: &HistoryRequest, scene: &SceneMap) -> Network {
649 let ports: BTreeMap<String, PortRecord> = scene
650 .ports
651 .iter()
652 .map(|(id, port)| (id.clone(), port.clone()))
653 .collect();
654 let owners: BTreeMap<String, String> = ports
655 .keys()
656 .filter_map(|id| {
657 scene
658 .owning_component(id)
659 .map(|record| (id.clone(), record.id.clone()))
660 })
661 .collect();
662 let mut segments = Vec::new();
663 let mut problems = Vec::new();
664 for descriptor in &request.features {
665 if !matches!(descriptor.feature_type.as_str(), "SP" | "SPLINE") {
666 continue;
667 }
668 let id = ["id", "featureID"]
669 .iter()
670 .find_map(|key| descriptor.input_params.get(key).and_then(|v| v.as_str()))
671 .unwrap_or("")
672 .trim()
673 .to_string();
674 if id.is_empty() {
675 continue;
676 }
677 let Some(points) = descriptor
678 .persistent_data
679 .get("spline")
680 .and_then(|spline| spline.get("points"))
681 .and_then(serde_json::Value::as_array)
682 else {
683 continue;
684 };
685 if points.len() < 2 {
686 continue;
687 }
688 let first = Attachment::parse(points.first().and_then(|p| p.get("attachment")));
689 let last = Attachment::parse(points.last().and_then(|p| p.get("attachment")));
690 let (first, last) = match (first, last) {
691 (Some(first), Some(last)) => (first, last),
692 (Some(_), None) | (None, Some(_)) => {
696 problems.push(format!("{id}: only one end is attached to a port"));
697 continue;
698 }
699 (None, None) => continue,
700 };
701 if first.port_ref == last.port_ref {
702 problems.push(format!("{id}: both ends attach to the same port '{}'", first.port_ref));
703 continue;
704 }
705 let (Some(first_record), Some(last_record)) =
706 (ports.get(&first.port_ref), ports.get(&last.port_ref))
707 else {
708 continue;
710 };
711 let Some(chain) = scene.resolve_path(&format!("{id}:SplineEdge")).cloned() else {
712 continue; };
714 if chain.is_empty() {
715 continue;
716 }
717 let length = chain_length(&chain);
718 let first_side = physical_side(first_record, end_tangent(&chain, true), first.side);
719 let second_side = physical_side(
722 last_record,
723 end_tangent(&chain, false).scale(-1.0),
724 last.side.other(),
725 );
726 segments.push(Segment {
727 id,
728 first_port: first.port_ref,
729 first_side,
730 second_port: last.port_ref,
731 second_side,
732 length,
733 chain,
734 });
735 }
736 Network {
737 ports,
738 owners,
739 segments,
740 problems,
741 }
742}
743
744fn physical_side(port: &PortRecord, outward: Vec3, fallback: PortSide) -> PortSide {
748 if outward.length() <= TANGENT_EPS {
749 return fallback;
750 }
751 if outward.dot(port.direction) >= 0.0 {
752 PortSide::A
753 } else {
754 PortSide::B
755 }
756}
757
758fn end_tangent(chain: &[NurbsCurve], at_start: bool) -> Vec3 {
762 let curve = if at_start { chain.first() } else { chain.last() };
763 let Some(curve) = curve else {
764 return Vec3::new(0.0, 0.0, 0.0);
765 };
766 let Ok([t0, t1]) = curve.domain() else {
767 return Vec3::new(0.0, 0.0, 0.0);
768 };
769 let parameter = if at_start { t0 } else { t1 };
770 if let Ok(derivatives) = curve.derivatives(parameter, 1) {
771 if let Some(tangent) = derivatives.get(1) {
772 if tangent.length() > TANGENT_EPS {
773 return *tangent;
774 }
775 }
776 }
777 let near = if at_start {
778 t0 + (t1 - t0) * 0.05
779 } else {
780 t1 - (t1 - t0) * 0.05
781 };
782 match (curve.evaluate(parameter), curve.evaluate(near)) {
783 (Ok(at), Ok(nearby)) => {
784 if at_start {
785 nearby.sub(at)
786 } else {
787 at.sub(nearby)
788 }
789 }
790 _ => Vec3::new(0.0, 0.0, 0.0),
791 }
792}
793
794fn chain_length(chain: &[NurbsCurve]) -> f64 {
799 let mut total = 0.0;
800 for curve in chain {
801 let Ok([t0, t1]) = curve.domain() else { continue };
802 total += match crate::curve_arc_length(curve, t0, t1) {
803 Ok(length) => length,
804 Err(_) => chord_length(curve, t0, t1),
805 };
806 }
807 total
808}
809
810fn chord_length(curve: &NurbsCurve, t0: f64, t1: f64) -> f64 {
812 let Ok(mut previous) = curve.evaluate(t0) else { return 0.0 };
813 let mut total = 0.0;
814 for sample in 1..=LENGTH_SAMPLES {
815 let t = t0 + (t1 - t0) * sample as f64 / LENGTH_SAMPLES as f64;
816 if let Ok(point) = curve.evaluate(t) {
817 total += point.sub(previous).length();
818 previous = point;
819 }
820 }
821 total
822}
823
824fn node_key(port: &str, side: PortSide) -> String {
829 format!("{port}/{}", side.letter())
830}
831
832struct Edge {
833 to: usize,
834 weight: f64,
835 segment: usize,
837}
838
839struct SidedGraph {
840 index: HashMap<String, usize>,
842 keys: Vec<String>,
843 port_of: Vec<usize>,
845 ports: Vec<String>,
846 edges: Vec<Vec<Edge>>,
847}
848
849impl SidedGraph {
850 fn build(network: &Network) -> Self {
851 let mut graph = SidedGraph {
852 index: HashMap::new(),
853 keys: Vec::new(),
854 port_of: Vec::new(),
855 ports: Vec::new(),
856 edges: Vec::new(),
857 };
858 for id in network.ports.keys() {
861 graph.add_port(id);
862 }
863 for (segment_index, segment) in network.segments.iter().enumerate() {
864 let weight = segment.length.max(1e-9);
865 let from = graph.node(&segment.first_port, segment.first_side);
868 let to = graph.node(&segment.second_port, segment.second_side.other());
869 graph.edges[from].push(Edge {
870 to,
871 weight,
872 segment: segment_index,
873 });
874 let from = graph.node(&segment.second_port, segment.second_side);
876 let to = graph.node(&segment.first_port, segment.first_side.other());
877 graph.edges[from].push(Edge {
878 to,
879 weight,
880 segment: segment_index,
881 });
882 }
883 graph
884 }
885
886 fn add_port(&mut self, id: &str) -> usize {
887 if let Some(position) = self.ports.iter().position(|p| p == id) {
888 return position;
889 }
890 let port_index = self.ports.len();
891 self.ports.push(id.to_string());
892 for side in [PortSide::A, PortSide::B] {
893 let key = node_key(id, side);
894 self.index.insert(key.clone(), self.keys.len());
895 self.keys.push(key);
896 self.port_of.push(port_index);
897 self.edges.push(Vec::new());
898 }
899 port_index
900 }
901
902 fn node(&mut self, port: &str, side: PortSide) -> usize {
903 let key = node_key(port, side);
904 if let Some(&index) = self.index.get(&key) {
905 return index;
906 }
907 self.add_port(port);
908 self.index[&key]
909 }
910
911 fn node_index(&self, port: &str, side: PortSide) -> Option<usize> {
912 self.index.get(&node_key(port, side)).copied()
913 }
914}
915
916#[derive(Debug, Clone, PartialEq)]
918struct SidedPath {
919 distance: f64,
920 nodes: Vec<usize>,
922 segments: Vec<usize>,
924}
925
926#[derive(PartialEq)]
928struct HeapEntry<T> {
929 cost: f64,
930 item: T,
931}
932
933impl<T: PartialEq> Eq for HeapEntry<T> {}
934
935impl<T: PartialEq> PartialOrd for HeapEntry<T> {
936 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
937 Some(self.cmp(other))
938 }
939}
940
941impl<T: PartialEq> Ord for HeapEntry<T> {
942 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
943 other
945 .cost
946 .partial_cmp(&self.cost)
947 .unwrap_or(std::cmp::Ordering::Equal)
948 }
949}
950
951fn dijkstra(graph: &SidedGraph, start: usize, end: usize) -> Option<SidedPath> {
953 let count = graph.keys.len();
954 let mut distance = vec![f64::INFINITY; count];
955 let mut parent: Vec<Option<(usize, usize)>> = vec![None; count];
956 let mut done = vec![false; count];
957 let mut heap = BinaryHeap::new();
958 distance[start] = 0.0;
959 heap.push(HeapEntry {
960 cost: 0.0,
961 item: start,
962 });
963 while let Some(HeapEntry { cost, item: node }) = heap.pop() {
964 if done[node] {
965 continue;
966 }
967 done[node] = true;
968 if node == end {
969 break;
970 }
971 for edge in &graph.edges[node] {
972 let next = cost + edge.weight;
973 if next < distance[edge.to] {
974 distance[edge.to] = next;
975 parent[edge.to] = Some((node, edge.segment));
976 heap.push(HeapEntry {
977 cost: next,
978 item: edge.to,
979 });
980 }
981 }
982 }
983 if !distance[end].is_finite() || start == end {
984 return None;
985 }
986 let mut nodes = vec![end];
987 let mut segments = Vec::new();
988 let mut cursor = end;
989 while let Some((previous, segment)) = parent[cursor] {
990 segments.push(segment);
991 nodes.push(previous);
992 cursor = previous;
993 }
994 nodes.reverse();
995 segments.reverse();
996 Some(SidedPath {
997 distance: distance[end],
998 nodes,
999 segments,
1000 })
1001}
1002
1003fn reuses_a_port(graph: &SidedGraph, path: &SidedPath) -> bool {
1005 let mut seen = BTreeSet::new();
1006 path.nodes
1007 .iter()
1008 .any(|&node| !seen.insert(graph.port_of[node]))
1009}
1010
1011fn shortest_non_reusing(graph: &SidedGraph, start: usize, end: usize) -> Option<SidedPath> {
1016 #[derive(PartialEq)]
1017 struct State {
1018 node: usize,
1019 visited: Vec<bool>,
1020 nodes: Vec<usize>,
1021 segments: Vec<usize>,
1022 }
1023 let port_count = graph.ports.len();
1024 let mut best: HashMap<(usize, Vec<bool>), f64> = HashMap::new();
1025 let mut heap = BinaryHeap::new();
1026 let mut visited = vec![false; port_count];
1027 visited[graph.port_of[start]] = true;
1028 heap.push(HeapEntry {
1029 cost: 0.0,
1030 item: State {
1031 node: start,
1032 visited,
1033 nodes: vec![start],
1034 segments: Vec::new(),
1035 },
1036 });
1037 while let Some(HeapEntry { cost, item: state }) = heap.pop() {
1038 if state.node == end && !state.segments.is_empty() {
1039 return Some(SidedPath {
1040 distance: cost,
1041 nodes: state.nodes,
1042 segments: state.segments,
1043 });
1044 }
1045 let key = (state.node, state.visited.clone());
1046 if best.get(&key).is_some_and(|&known| known < cost - 1e-12) {
1047 continue;
1048 }
1049 for edge in &graph.edges[state.node] {
1050 let port = graph.port_of[edge.to];
1051 if state.visited[port] {
1052 continue;
1053 }
1054 let mut visited = state.visited.clone();
1055 visited[port] = true;
1056 let next_cost = cost + edge.weight;
1057 let next_key = (edge.to, visited.clone());
1058 if best.get(&next_key).is_some_and(|&known| known <= next_cost + 1e-12) {
1059 continue;
1060 }
1061 best.insert(next_key, next_cost);
1062 let mut nodes = state.nodes.clone();
1063 nodes.push(edge.to);
1064 let mut segments = state.segments.clone();
1065 segments.push(edge.segment);
1066 heap.push(HeapEntry {
1067 cost: next_cost,
1068 item: State {
1069 node: edge.to,
1070 visited,
1071 nodes,
1072 segments,
1073 },
1074 });
1075 }
1076 }
1077 None
1078}
1079
1080fn route_connection(
1084 network: &Network,
1085 graph: &SidedGraph,
1086 connection: &WireHarnessConnection,
1087) -> RouteResult {
1088 let unrouted = |status: RouteStatus, message: String| RouteResult {
1089 connection_id: connection.id.clone(),
1090 feasible: false,
1091 status,
1092 message,
1093 length: None,
1094 segment_ids: Vec::new(),
1095 node_path: Vec::new(),
1096 port_ids: Vec::new(),
1097 };
1098 let from = connection.from.trim();
1099 let to = connection.to.trim();
1100 for (label, id) in [("from", from), ("to", to)] {
1101 if id.is_empty() {
1102 return unrouted(RouteStatus::MissingEndpoint, format!("no {label} port"));
1103 }
1104 let Some(port) = network.ports.get(id) else {
1105 return unrouted(
1106 RouteStatus::MissingEndpoint,
1107 format!("{label} port '{id}' is not in the model"),
1108 );
1109 };
1110 if port.kind == PortKind::Waypoint {
1111 return unrouted(
1112 RouteStatus::WaypointEndpoint,
1113 format!("{label} port '{}' is a waypoint, not a termination", port.local_address()),
1114 );
1115 }
1116 }
1117 if from == to {
1118 return unrouted(RouteStatus::SameEndpoint, "from and to are the same port".into());
1119 }
1120 if network.segments.is_empty() {
1121 return unrouted(
1122 RouteStatus::NoSegments,
1123 "no harness splines: attach a spline's end anchors to two ports".into(),
1124 );
1125 }
1126
1127 let mut best: Option<SidedPath> = None;
1128 for start_side in [PortSide::A, PortSide::B] {
1129 for end_side in [PortSide::A, PortSide::B] {
1130 let (Some(start), Some(end)) = (
1131 graph.node_index(from, start_side),
1132 graph.node_index(to, end_side),
1133 ) else {
1134 continue;
1135 };
1136 if let Some(path) = dijkstra(graph, start, end) {
1137 if best.as_ref().is_none_or(|b| path.distance < b.distance) {
1138 best = Some(path);
1139 }
1140 }
1141 }
1142 }
1143 let Some(mut path) = best else {
1144 return unrouted(
1145 RouteStatus::NoRoute,
1146 "no sided path joins the two ports (check which side each spline leaves its ports on)".into(),
1147 );
1148 };
1149 if reuses_a_port(graph, &path) {
1150 let mut alternative: Option<SidedPath> = None;
1151 for start_side in [PortSide::A, PortSide::B] {
1152 for end_side in [PortSide::A, PortSide::B] {
1153 let (Some(start), Some(end)) = (
1154 graph.node_index(from, start_side),
1155 graph.node_index(to, end_side),
1156 ) else {
1157 continue;
1158 };
1159 if let Some(found) = shortest_non_reusing(graph, start, end) {
1160 if alternative.as_ref().is_none_or(|b| found.distance < b.distance) {
1161 alternative = Some(found);
1162 }
1163 }
1164 }
1165 }
1166 match alternative {
1167 Some(found) => path = found,
1168 None => {
1169 return unrouted(
1170 RouteStatus::PortReuse,
1171 "every path passes through the same port twice".into(),
1172 )
1173 }
1174 }
1175 }
1176 RouteResult {
1177 connection_id: connection.id.clone(),
1178 feasible: true,
1179 status: RouteStatus::Routed,
1180 message: String::new(),
1181 length: Some(path.distance),
1182 segment_ids: path
1183 .segments
1184 .iter()
1185 .map(|&index| network.segments[index].id.clone())
1186 .collect(),
1187 node_path: path.nodes.iter().map(|&node| graph.keys[node].clone()).collect(),
1188 port_ids: path
1189 .nodes
1190 .iter()
1191 .map(|&node| graph.ports[graph.port_of[node]].clone())
1192 .collect(),
1193 }
1194}
1195
1196fn build_bundles(
1204 network: &Network,
1205 state: &WireHarnessState,
1206 routes: &[RouteResult],
1207 result: &mut FeatureResult,
1208) -> Vec<WireHarnessBundle> {
1209 let mut usage: Vec<(String, Vec<f64>, Vec<String>)> = Vec::new();
1211 for route in routes.iter().filter(|route| route.feasible) {
1212 let Some(connection) = state
1213 .connections
1214 .iter()
1215 .find(|connection| connection.id == route.connection_id)
1216 else {
1217 continue;
1218 };
1219 let diameter = connection.diameter.max(MIN_DIAMETER);
1220 for segment_id in &route.segment_ids {
1221 let entry = match usage.iter_mut().find(|(id, _, _)| id == segment_id) {
1222 Some(entry) => entry,
1223 None => {
1224 usage.push((segment_id.clone(), Vec::new(), Vec::new()));
1225 usage.last_mut().expect("just pushed")
1226 }
1227 };
1228 entry.1.push(diameter);
1229 if !entry.2.contains(&connection.id) {
1230 entry.2.push(connection.id.clone());
1231 }
1232 }
1233 }
1234
1235 let mut bundles = Vec::with_capacity(usage.len());
1236 for (segment_id, diameters, connection_ids) in usage {
1237 let Some(segment) = network.segment(&segment_id) else {
1238 continue;
1239 };
1240 let diameter = bundle_diameter(&diameters).max(MIN_DIAMETER);
1241 let mut bundle = WireHarnessBundle {
1242 segment_id: segment_id.clone(),
1243 solid_name: String::new(),
1244 wire_count: diameters.len(),
1245 diameter,
1246 length: segment.length,
1247 connection_ids,
1248 status: BundleStatus::BundlesOff,
1249 error: String::new(),
1250 };
1251 if state.build_bundles {
1252 let name = format!("{BUNDLE_SOLID_PREFIX}{segment_id}");
1253 match sweep_bundle(segment, diameter * 0.5, &name) {
1254 Ok(solid) => {
1255 result.added.push(common::register_added(solid, &name));
1256 bundle.solid_name = name;
1257 bundle.status = BundleStatus::Built;
1258 }
1259 Err(error) => {
1260 bundle.status = if error.starts_with(crate::SWEEP_TIGHT_BEND_REFUSAL) {
1264 BundleStatus::TightBend
1265 } else {
1266 BundleStatus::BuildFailed
1267 };
1268 bundle.error = error;
1269 }
1270 }
1271 }
1272 bundles.push(bundle);
1273 }
1274 bundles
1275}
1276
1277fn sweep_bundle(segment: &Segment, radius: f64, name: &str) -> Result<crate::BrepSolid, String> {
1281 let start = segment.chain[0]
1282 .domain()
1283 .and_then(|[t0, _]| segment.chain[0].evaluate(t0))?;
1284 let tangent = end_tangent(&segment.chain, true)
1285 .normalized()
1286 .map_err(|_| "the chain starts with a zero tangent".to_string())?;
1287 let x_axis = tangent.perpendicular()?;
1288 let y_axis = tangent.cross(x_axis).normalized()?;
1289 let profile = vec![
1290 make_arc(start, x_axis, y_axis, radius, 0.0, std::f64::consts::PI)?,
1291 make_arc(start, x_axis, y_axis, radius, std::f64::consts::PI, std::f64::consts::TAU)?,
1292 ];
1293 let names: Vec<String> = (0..segment.chain.len())
1294 .map(|index| format!("{}:piece{index}", segment.id))
1295 .collect();
1296 let stations = (segment.chain.len() * STATIONS_PER_PIECE).clamp(32, 1024);
1297 let path = crate::SweepPath::new(segment.chain.clone(), names)?;
1302 let mut solid = crate::sweep_profile_along_chain_with_stations(
1303 &profile,
1304 &path,
1305 stations,
1306 "a harness segment must be tangent-continuous (a spline always is; a zero extension at an anchor can leave a corner)",
1307 )?;
1308 let faces = &mut solid
1310 .shells
1311 .get_mut(0)
1312 .ok_or("the sweep produced no shell")?
1313 .faces;
1314 let expected = ["Wall0", "Wall1", "Start", "End"];
1315 if faces.len() != expected.len() {
1316 return Err(format!(
1317 "the sweep produced {} faces, expected {}",
1318 faces.len(),
1319 expected.len()
1320 ));
1321 }
1322 for (face, suffix) in faces.iter_mut().zip(expected) {
1323 face.name = Some(format!("{name}:{suffix}"));
1324 }
1325 Ok(solid)
1326}
1327