Skip to main content

brep_render/engine_state/
ports_ops.rs

1//! The PORTS surface: what the ports tail resolved on the last applied run.
2//!
3//! A part's declared connection points are DATA, not features
4//! (`brep_kernel`'s `ports` module), and they are resolved at the TAIL of
5//! every history run. The tail is not a feature, so its result reaches no
6//! feature row: this is the only place a caller can read what it published,
7//! what name it refused and which reference it could not resolve.
8//!
9//! The block itself is read and written through `History` (`ports_block`,
10//! `set_ports_block`), which carries the pin/point follow inside the write —
11//! so a caller that edits a point here edits its pin too, in one undo step.
12
13use super::*;
14use brep_kernel::PortsReport;
15use serde_json::Value;
16
17
18/// Which of a connection point's two reference fields a pick is filling.
19///
20/// They are two different questions about the same seat — WHERE it sits and
21/// WHICH WAY it faces — so they admit different picks: a vertex locates a
22/// point and implies no direction, which is why it is offered to one and not
23/// the other.
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub enum PortRefField {
26    /// `pointRef` — the geometry the point sits on.
27    Point,
28    /// `directionRef` — the geometry it faces along.
29    Direction,
30}
31
32impl PortRefField {
33    /// The block key this field writes.
34    pub fn key(self) -> &'static str {
35        match self {
36            PortRefField::Point => "pointRef",
37            PortRefField::Direction => "directionRef",
38        }
39    }
40
41    /// The picker's heading.
42    pub fn label(self) -> &'static str {
43        match self {
44            PortRefField::Point => "Seat this connection point on",
45            PortRefField::Direction => "Point it along",
46        }
47    }
48
49    /// Which pick kinds this field admits. A VERTEX seats a position and
50    /// implies no direction, so it is offered to `pointRef` alone; SOLID is
51    /// offered to neither, because a click meant for a face that landed on the
52    /// body instead would seat the point at the body's centre, which is never
53    /// what the click meant.
54    pub fn filter(self) -> Vec<String> {
55        let kinds: &[&str] = match self {
56            PortRefField::Point => &["VERTEX", "EDGE", "FACE", "PLANE"],
57            PortRefField::Direction => &["EDGE", "FACE", "PLANE"],
58        };
59        kinds.iter().map(|kind| (*kind).to_string()).collect()
60    }
61}
62
63impl EngineState {
64    /// The tail's row for one point of THIS document, by part-local address —
65    /// its seat, where it resolved and which way it faces. `None` for a point
66    /// the last run did not resolve (one just typed in, or one whose group
67    /// name is refused).
68    pub fn port_point_row(&self, address: &str) -> Option<&brep_kernel::PortPointRow> {
69        self.ports_report()?
70            .points
71            .iter()
72            .find(|row| row.address == address)
73    }
74
75    /// The connection point of THIS DOCUMENT that the viewport selection
76    /// names, if any — the 3D half of "select it there, see it here".
77    ///
78    /// A declared point draws as a synthesized sheet keyed by its ADDRESS, and
79    /// its base vertex and line hang off that address, so a click anywhere on
80    /// it selects one of three names that all peel back to the same point.
81    ///
82    /// A NAMESPACED address (`ACOMP1:J1.VCC`) is deliberately not adopted: it
83    /// belongs to a component this assembly places, not to the document being
84    /// qualified, and selecting its row here would offer to edit a point this
85    /// document does not own.
86    pub fn selected_port_point(&self) -> Option<String> {
87        let declared: Vec<String> = self
88            .history
89            .declared_points()
90            .into_iter()
91            .map(|point| point.address())
92            .collect();
93        self.emphasis.selected_solids.iter().find_map(|name| {
94            let address = name
95                .strip_suffix(":Base")
96                .or_else(|| name.strip_suffix(":PortLine"))
97                .unwrap_or(name);
98            declared
99                .iter()
100                .find(|candidate| candidate.as_str() == address)
101                .cloned()
102        })
103    }
104
105    /// Edit ONE declared point's JSON object in place, by address, and write
106    /// the block back through `History::set_ports_block` (which carries the
107    /// pin/point follow and re-runs). Returns whether the point was found.
108    ///
109    /// The point is handed to `edit` as its RAW object, one key at a time, for
110    /// the reason the Qualify panel edits it that way: a round trip through
111    /// `PortDeclaration` drops any field the struct does not model, and no
112    /// lane may be the reason a later slice's field disappears when the field
113    /// beside it is touched.
114    pub fn edit_port_point(
115        &mut self,
116        address: &str,
117        coalesce: Option<&str>,
118        edit: impl FnOnce(&mut serde_json::Map<String, Value>),
119    ) -> bool {
120        let Some((port, point_name)) = brep_kernel::split_address(address) else {
121            return false;
122        };
123        let Some(mut block) = self
124            .history
125            .ports_block()
126            .and_then(Value::as_array)
127            .cloned()
128        else {
129            return false;
130        };
131        let mut found = false;
132        for group in block.iter_mut() {
133            if group.get("name").and_then(Value::as_str).map(str::trim) != Some(port) {
134                continue;
135            }
136            let Some(points) = group.get_mut("points").and_then(Value::as_array_mut) else {
137                continue;
138            };
139            for candidate in points.iter_mut() {
140                if candidate.get("name").and_then(Value::as_str).map(str::trim)
141                    != Some(point_name)
142                {
143                    continue;
144                }
145                let Some(object) = candidate.as_object_mut() else {
146                    continue;
147                };
148                edit(object);
149                found = true;
150                break;
151            }
152            break;
153        }
154        if found {
155            self.history.set_ports_block(Some(Value::Array(block)), coalesce);
156        }
157        found
158    }
159
160    // --- the reference picker's connection-point flavour ---------------------
161
162    /// Enter the reference picker to seat `address` on geometry.
163    ///
164    /// Unlike every feature field, this does NOT roll the history back: a
165    /// connection point resolves at the TAIL of the run, against the finished
166    /// part, so the geometry it may be seated on is exactly what is on screen.
167    /// Rolling would hide from the user the very faces the point can name.
168    pub fn begin_ref_select_for_port_point(&mut self, address: &str, field: PortRefField) {
169        // The handles come OFF first. The picker takes over the shell and the
170        // Qualify panel is not drawn while it is up, so nothing else would
171        // take them off — and `route_drag_start` puts the transform gizmo
172        // AHEAD of the camera with no picker guard, so a user dragging to
173        // orbit round to the face they want to pick would drag the point
174        // instead.
175        if self.transform_armed_port_point() == Some(address) {
176            self.disarm_transform();
177        }
178        let filter = field.filter();
179        self.selection_filter = SelectionFilter::from_ref_filter(&filter);
180        let seed = self
181            .declared_point_value(address)
182            .and_then(|point| {
183                point
184                    .get(field.key())
185                    .and_then(Value::as_str)
186                    .map(str::to_string)
187            })
188            .filter(|name| !name.trim().is_empty())
189            .into_iter()
190            .collect();
191        self.ref_select = Some(RefSelectState {
192            feature_id: address.to_string(),
193            path: vec![field.key().to_string()],
194            label: format!("{} \u{2014} {address}", field.label()),
195            filter,
196            multiple: false,
197            names: seed,
198            restore_index: self.history.rollback(),
199            target: RefSelectTarget::PortPoint { field },
200            picks: 0,
201            claimed: false,
202            finish_requested: false,
203        });
204        self.sync_ref_select_emphasis();
205    }
206
207    /// Unseat a connection point: clear one of its reference fields and
208    /// re-run. The OFFSET is deliberately left as it is — it is on screen, and
209    /// silently rewriting the numbers a user can see is worse than a point
210    /// that visibly moved to where those numbers say.
211    pub fn clear_port_point_ref(&mut self, address: &str, field: PortRefField) {
212        self.ports_commit_ref(address, field, "");
213        self.rerun_history();
214    }
215
216    /// One declared point's raw object, by address.
217    pub(super) fn declared_point_value(&self, address: &str) -> Option<Value> {
218        let (port, name) = brep_kernel::split_address(address)?;
219        self.history
220            .ports_block()?
221            .as_array()?
222            .iter()
223            .find(|group| group.get("name").and_then(Value::as_str).map(str::trim) == Some(port))?
224            .get("points")?
225            .as_array()?
226            .iter()
227            .find(|point| point.get("name").and_then(Value::as_str).map(str::trim) == Some(name))
228            .cloned()
229    }
230
231    /// Commit the picker's name into the point's reference field, WITHOUT
232    /// re-running — the picker's shared tail re-runs.
233    ///
234    /// Installing the FIRST reference on a point also zeroes its transform.
235    /// The numbers that were there described an absolute placement; the same
236    /// numbers in a seat mean somewhere else entirely, so keeping them would
237    /// fling the point off the face the user just picked. Clearing a reference
238    /// does NOT restore them: the offset stays as it is, now read in the world
239    /// frame, and it is on screen in the panel where the user can see it.
240    pub(super) fn ports_commit_ref(&mut self, address: &str, field: PortRefField, name: &str) {
241        let name = name.trim().to_string();
242        let was_seated = self
243            .declared_point_value(address)
244            .map(|point| {
245                ["pointRef", "directionRef"].iter().any(|key| {
246                    point
247                        .get(*key)
248                        .and_then(Value::as_str)
249                        .is_some_and(|value| !value.trim().is_empty())
250                })
251            })
252            .unwrap_or(false);
253        self.edit_port_point(address, None, |point| {
254            match name.is_empty() {
255                true => {
256                    point.remove(field.key());
257                }
258                false => {
259                    point.insert(field.key().into(), Value::String(name.clone()));
260                }
261            }
262            if !was_seated && !name.is_empty() {
263                point.insert(
264                    "transform".into(),
265                    serde_json::json!({
266                        "position": [0.0, 0.0, 0.0],
267                        "rotationEuler": [0.0, 0.0, 0.0],
268                    }),
269                );
270            }
271        });
272    }
273
274    /// The ports tail's report of the last APPLIED run: whether this document
275    /// is an encapsulation boundary, every point it resolved, and everything
276    /// about the block that does not hold. `None` before the first run, and
277    /// for a document that declares no `ports` block.
278    pub fn ports_report(&self) -> Option<&PortsReport> {
279        self.ports_report.as_ref()
280    }
281
282    /// The Qualify panel's verifier global: the declared block and the tail's
283    /// report, as one JSON object. `report` is null before the first run.
284    pub fn ports_state_json(&self) -> String {
285        serde_json::json!({
286            "block": self.history.ports_block(),
287            "report": self.ports_report,
288        })
289        .to_string()
290    }
291}
292