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