Skip to main content

brep_render/engine_state/
wire_harness_ops.rs

1use super::*;
2use brep_kernel::{WireHarnessConnection, WireHarnessReport, WireHarnessState};
3
4/// A patch the panel applies to one connection: every field optional, only
5/// the present ones change.
6#[derive(Debug, Default, Clone, PartialEq)]
7pub struct ConnectionPatch {
8    pub name: Option<String>,
9    pub from: Option<String>,
10    pub to: Option<String>,
11    pub diameter: Option<f64>,
12}
13
14impl EngineState {
15    // --- read surface ------------------------------------------------------
16
17    /// The routing report of the last APPLIED run: endpoints (every port in
18    /// the model), the harness segments, one route per connection, and the
19    /// bundles. `None` before the first run.
20    pub fn wire_harness_report(&self) -> Option<&WireHarnessReport> {
21        self.wire_harness_report.as_ref()
22    }
23
24    /// The document's `wireHarness` block as typed state (the default — no
25    /// connections — when the document carries none).
26    pub fn wire_harness_state(&self) -> WireHarnessState {
27        self.history
28            .wire_harness_block()
29            .and_then(|block| serde_json::from_value(block.clone()).ok())
30            .unwrap_or_default()
31    }
32
33    /// The panel's verifier global: the block, the report, and the endpoint
34    /// choices, as one JSON object.
35    pub fn wire_harness_state_json(&self) -> String {
36        let state = self.wire_harness_state();
37        serde_json::json!({
38            "connections": state.connections,
39            "buildBundles": state.build_bundles,
40            "cutMargin": state.cut_margin,
41            "report": self.wire_harness_report,
42        })
43        .to_string()
44    }
45
46    // --- mutations (checkpointed document edits + re-run) -------------------
47
48    /// Write `state` as the document's block (checkpointed) and re-run the
49    /// history so the tail routes it. An empty block (no connections, a fresh
50    /// counter and no cut margin) is removed from the document so a part that
51    /// never had a harness saves byte-identically.
52    fn write_wire_harness_state(&mut self, state: WireHarnessState) -> String {
53        let block = if state.connections.is_empty() && state.id_counter == 0 && state.cut_margin == 0.0 {
54            None
55        } else {
56            serde_json::to_value(&state).ok()
57        };
58        self.history.set_wire_harness_block(block);
59        self.rerun_history()
60    }
61
62    /// Add a connection between two port ids (either may be empty — the panel
63    /// fills them in) and return its minted id (`wire-N`, named `Wire N`).
64    pub fn wire_harness_add_connection(&mut self, from: &str, to: &str, diameter: f64) -> String {
65        let mut state = self.wire_harness_state();
66        let id = state.next_id();
67        let name = format!("Wire {}", state.id_counter);
68        state.connections.push(WireHarnessConnection {
69            id: id.clone(),
70            name,
71            from: from.to_string(),
72            to: to.to_string(),
73            diameter: if diameter.is_finite() && diameter > 0.0 { diameter } else { 1.0 },
74        });
75        self.write_wire_harness_state(state);
76        id
77    }
78
79    /// Apply a patch to one connection. An unknown id is an error; a
80    /// non-positive diameter is refused (the connection keeps its diameter).
81    pub fn wire_harness_update_connection(
82        &mut self,
83        id: &str,
84        patch: &ConnectionPatch,
85    ) -> Result<(), String> {
86        let mut state = self.wire_harness_state();
87        let connection = state
88            .connections
89            .iter_mut()
90            .find(|connection| connection.id == id)
91            .ok_or_else(|| format!("no harness connection '{id}'"))?;
92        if let Some(name) = &patch.name {
93            connection.name = name.trim().to_string();
94        }
95        if let Some(from) = &patch.from {
96            connection.from = from.trim().to_string();
97        }
98        if let Some(to) = &patch.to {
99            connection.to = to.trim().to_string();
100        }
101        if let Some(diameter) = patch.diameter {
102            if !(diameter.is_finite() && diameter > 0.0) {
103                return Err(format!("wire diameter must be positive, got {diameter}"));
104            }
105            connection.diameter = diameter;
106        }
107        self.write_wire_harness_state(state);
108        Ok(())
109    }
110
111    /// Remove one connection. An unknown id is an error.
112    pub fn wire_harness_remove_connection(&mut self, id: &str) -> Result<(), String> {
113        let mut state = self.wire_harness_state();
114        let before = state.connections.len();
115        state.connections.retain(|connection| connection.id != id);
116        if state.connections.len() == before {
117            return Err(format!("no harness connection '{id}'"));
118        }
119        self.write_wire_harness_state(state);
120        Ok(())
121    }
122
123    /// Switch the bundle solids on / off (the routing report stays either way).
124    pub fn wire_harness_set_build_bundles(&mut self, on: bool) {
125        let mut state = self.wire_harness_state();
126        if state.build_bundles == on {
127            return;
128        }
129        state.build_bundles = on;
130        self.write_wire_harness_state(state);
131    }
132
133    /// Set the cut margin every wire's BOM cut length adds to its routed
134    /// length (see [`WireHarnessState::cut_margin`]). A negative or non-finite
135    /// margin is refused and the block is left alone; an unchanged one writes
136    /// nothing, so it takes no undo step. The re-run replays the harness from
137    /// its cache — the router does not read the margin.
138    pub fn wire_harness_set_cut_margin(&mut self, margin: f64) -> Result<(), String> {
139        if !(margin.is_finite() && margin >= 0.0) {
140            return Err(format!("cut margin must be zero or more, got {margin}"));
141        }
142        let mut state = self.wire_harness_state();
143        if state.cut_margin == margin {
144            return Ok(());
145        }
146        state.cut_margin = margin;
147        self.write_wire_harness_state(state);
148        Ok(())
149    }
150
151    // --- hover ----------------------------------------------------------------
152
153    /// Highlight a connection under the panel's pointer: its two ports (their
154    /// sheet solids are keyed by the port feature id) and the bundle solids of
155    /// every segment its route crosses. An unknown id highlights nothing.
156    pub fn wire_harness_hover_connection(&mut self, id: &str) {
157        let state = self.wire_harness_state();
158        let mut names: Vec<String> = Vec::new();
159        if let Some(connection) = state.connections.iter().find(|c| c.id == id) {
160            for port in [&connection.from, &connection.to] {
161                if !port.is_empty() && self.scene.solid(port).is_some() {
162                    names.push(port.clone());
163                }
164            }
165        }
166        if let Some(report) = &self.wire_harness_report {
167            if let Some(route) = report.routes.iter().find(|route| route.connection_id == id) {
168                for segment in &route.segment_ids {
169                    if let Some(bundle) = report.bundles.iter().find(|b| &b.segment_id == segment) {
170                        if !bundle.solid_name.is_empty() {
171                            names.push(bundle.solid_name.clone());
172                        }
173                    }
174                }
175            }
176        }
177        self.clear_hover();
178        self.emphasis.hovered_solids.extend(names);
179        self.emphasis.generation = self.emphasis.generation.wrapping_add(1);
180        self.dirty = true;
181    }
182
183    /// The pointer left the panel's rows: drop the highlight.
184    pub fn wire_harness_hover_end(&mut self) {
185        if self.clear_hover() {
186            self.dirty = true;
187        }
188    }
189}
190