brep_render/engine_state/wire_bom.rs
1//! Wire BOM lines — every harness wire as its own BOM line, carrying the cut
2//! length an automatic wire-cutting machine reads (MF QTY).
3//!
4//! # The number is the router's
5//!
6//! A line's length is [`brep_kernel::RouteResult::length`] from the routing
7//! report of the last APPLIED history run: the sum of the arc lengths of every
8//! spline the wire crosses, measured port base point to port base point (the
9//! ports' straight `extension` runs are pieces of each spline's chain). This
10//! module computes no second length. MF QTY is that length plus the block's
11//! [`cutMargin`](brep_kernel::WireHarnessState::cut_margin), added once to each
12//! wire — never a percentage, never once per line.
13//!
14//! # No number unless the run is the model's
15//!
16//! Routing results are not saved; the report is replaced by every applied run.
17//! So a length can be wrong in only these ways, and each is a state the line
18//! states instead of a number ([`WireLengthState`]):
19//!
20//! * a run was submitted and has not landed ([`EngineState::run_pending`]) —
21//! the report on hand routed the model as it was BEFORE the edit;
22//! * that run was CANCELLED ([`EngineState::cancelled_run`]): nothing is
23//! pending any more, but the model shows the last COMPLETED result, from
24//! before the cancelled edit, until the next edit submits a fresh run;
25//! * no run has landed at all (a fresh engine, or a history that failed to
26//! parse, which clears the report);
27//! * the history is ROLLED BACK. The tail routes the prefix it ran, so a spline
28//! past the rollback is not in the network and a wire can route over a
29//! different path and still read `routed` — a current run, a wrong length.
30//! Measured: see `a_rolled_back_history_routes_a_different_path`;
31//! * the router could not route the wire: the line carries its status.
32//!
33//! A silently stale cut length costs a reel of wire; a blank one with a reason
34//! costs a rebuild.
35
36use super::*;
37use brep_kernel::RouteStatus;
38
39/// Why a wire line has, or has not, a cut length.
40#[derive(Debug, Clone, PartialEq)]
41pub enum WireLengthState {
42 /// The last applied run routed the whole model, and this wire through it.
43 Current,
44 /// A run is in flight; the report on hand is from before the last edit.
45 RunPending,
46 /// The last run was cancelled: the report on hand is the last COMPLETED
47 /// run's, from before the edit that was cancelled.
48 Cancelled,
49 /// No run has been applied (a fresh engine, or a history that failed to
50 /// parse), or the run on hand has no route for this wire.
51 NoRun,
52 /// The history is rolled back: the run routed a prefix of the model, which
53 /// can be a different path through fewer splines.
54 RolledBack,
55 /// The router could not route the wire; the status and its sentence are
56 /// the router's own.
57 Unrouted { status: RouteStatus, message: String },
58}
59
60impl WireLengthState {
61 /// The kebab-case word the BOM keys on (`current`, `routing`, `no-run`,
62 /// `rolled-back`, or the router's status word).
63 pub fn as_str(&self) -> &'static str {
64 match self {
65 WireLengthState::Current => "current",
66 WireLengthState::RunPending => "routing",
67 WireLengthState::Cancelled => "cancelled",
68 WireLengthState::NoRun => "no-run",
69 WireLengthState::RolledBack => "rolled-back",
70 WireLengthState::Unrouted { status, .. } => status.as_str(),
71 }
72 }
73
74 /// What the MF QTY cell says in place of a number: short, and never
75 /// something that reads as a length.
76 pub fn describe(&self) -> String {
77 match self {
78 WireLengthState::Current => String::new(),
79 WireLengthState::RunPending => "routing…".into(),
80 WireLengthState::Cancelled => "rebuild cancelled".into(),
81 WireLengthState::NoRun => "needs a rebuild".into(),
82 WireLengthState::RolledBack => "rolled back".into(),
83 WireLengthState::Unrouted { status, .. } => format!("not routed ({})", status.as_str()),
84 }
85 }
86
87 /// The full sentence for a tooltip or an export's status column.
88 pub fn explain(&self) -> String {
89 match self {
90 WireLengthState::Current => "routed by the current run".into(),
91 WireLengthState::RunPending => {
92 "a rebuild is running; the cut length appears when it lands".into()
93 }
94 WireLengthState::Cancelled => {
95 "the last rebuild was cancelled, so the route on hand is from before your last edit; rebuild for cut lengths"
96 .into()
97 }
98 WireLengthState::NoRun => "no routing run has been applied; rebuild the model".into(),
99 WireLengthState::RolledBack => {
100 "the history is rolled back, so the route may miss later splines; roll to the end for cut lengths"
101 .into()
102 }
103 WireLengthState::Unrouted { message, .. } => message.clone(),
104 }
105 }
106}
107
108/// One wire as a BOM line.
109#[derive(Debug, Clone, PartialEq)]
110pub struct WireBomLine {
111 /// The harness connection id (`wire-N`) — the line's stable key.
112 pub id: String,
113 /// The connection ID the line shows: the connection's name, else its id.
114 pub connection_id: String,
115 /// The stock part number of the wire. Blank for a hand-added harness wire;
116 /// a wire a Diagram generates carries eCAD's.
117 pub stock_part_number: String,
118 /// The routed length, only when `state` is [`WireLengthState::Current`].
119 pub length: Option<f64>,
120 /// The block's cut margin, added once to this wire.
121 pub margin: f64,
122 /// The cut length: `length + margin`, only when `length` is.
123 pub mf_qty: Option<f64>,
124 pub state: WireLengthState,
125}
126
127impl EngineState {
128 /// Every harness wire as a BOM line, in block order, each with the cut
129 /// length of the last applied run — or, when that run does not speak for
130 /// the model as it stands, the reason there is none (see the module doc).
131 pub fn wire_bom_lines(&self) -> Vec<WireBomLine> {
132 let state = self.wire_harness_state();
133 let margin = state.cut_margin;
134 let rolled_back = !self.history.is_empty() && self.history.rollback() + 1 < self.history.len();
135 state
136 .connections
137 .iter()
138 .map(|connection| {
139 let route = self
140 .wire_harness_report
141 .as_ref()
142 .and_then(|report| report.routes.iter().find(|route| route.connection_id == connection.id));
143 let (state, length) = if self.run_pending() {
144 (WireLengthState::RunPending, None)
145 } else if self.cancelled_run.is_some() {
146 (WireLengthState::Cancelled, None)
147 } else if self.wire_harness_report.is_none() {
148 (WireLengthState::NoRun, None)
149 } else if rolled_back {
150 (WireLengthState::RolledBack, None)
151 } else {
152 match route {
153 None => (WireLengthState::NoRun, None),
154 Some(route) if !route.feasible => (
155 WireLengthState::Unrouted {
156 status: route.status,
157 message: route.message.clone(),
158 },
159 None,
160 ),
161 Some(route) => match route.length {
162 Some(length) => (WireLengthState::Current, Some(length)),
163 None => (WireLengthState::NoRun, None),
164 },
165 }
166 };
167 let connection_id = if connection.name.trim().is_empty() {
168 connection.id.clone()
169 } else {
170 connection.name.clone()
171 };
172 WireBomLine {
173 id: connection.id.clone(),
174 connection_id,
175 stock_part_number: connection.id.strip_prefix("diagram:")
176 .and_then(|id| self.history.diagram_block()?.get("wires")?.as_array()?.iter()
177 .find(|wire| wire["id"].as_str() == Some(id))?
178 .get("stock_part_number")?.as_str()).unwrap_or("").to_owned(),
179 length,
180 margin,
181 mf_qty: length.map(|length| length + margin),
182 state,
183 }
184 })
185 .collect()
186 }
187}
188