Skip to main content

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