Skip to main content

brep_render/engine_state/
committed_sketches.rs

1use super::*;
2
3/// The committed-sketch SHEET base color (dim cyan, matching the retired overlay
4/// `COMMITTED_COLOR` 0x67c7d4) — a sketch sheet reads distinctly from a real solid.
5const SKETCH_SHEET_COLOR: [f32; 3] = [
6    0x67 as f32 / 255.0,
7    0xc7 as f32 / 255.0,
8    0xd4 as f32 / 255.0,
9];
10
11/// The wire-harness PORT sheet colour: red, so a port reads apart from every
12/// sketch (cyan) and solid at a glance.
13const PORT_TERMINATION_COLOR: [f32; 3] = [
14    0xe5 as f32 / 255.0,
15    0x32 as f32 / 255.0,
16    0x2d as f32 / 255.0,
17];
18/// A WAYPOINT port: a lighter red, so a pass-through reads apart from an end.
19const PORT_WAYPOINT_COLOR: [f32; 3] = [
20    0xff as f32 / 255.0,
21    0x8a as f32 / 255.0,
22    0x80 as f32 / 255.0,
23];
24
25/// The feature types whose committed display is a synthesized SKETCH-LIKE
26/// sheet (an `is_sketch` scene solid drawn from the paths the feature
27/// published), and the prefix of the path names that are that feature's own
28/// drawable segments:
29///   * `S` — a sketch draws one segment per model geometry, `{id}:G{gid}`;
30///   * `HX` — a helix draws its single fitted edge, `{id}:HelixEdge`;
31///   * `SP` — a spline draws its exact chain, `{id}:SplineEdge`;
32///   * `PORT` — a harness port draws its port line, `{id}:PortLine`.
33/// Anything else (a solid feature) has no sheet here. Only `S` is ever
34/// ENTERABLE — the sketch-mode guards check the type separately.
35fn curve_feature_segment_prefix(feature_type: &str, id: &str) -> Option<String> {
36    match feature_type {
37        "S" => Some(format!("{id}:G")),
38        "HX" => Some(format!("{id}:HelixEdge")),
39        "SP" => Some(format!("{id}:SplineEdge")),
40        "PORT" => Some(format!("{id}:PortLine")),
41        _ => None,
42    }
43}
44
45/// The prefix of the published POINT names a sketch-like feature draws as
46/// standalone vertices: a sketch publishes every solved point as `{id}:P{pid}`,
47/// and the ones no segment covers (a points-only hole-placement sketch) are
48/// otherwise invisible; a spline publishes its anchors as `{id}:P{index}`, drawn
49/// so the anchors are visible and pickable; a port publishes its base point
50/// as `{id}:Base`. A helix publishes only its two ends, which its edge already
51/// draws, so it has no point prefix.
52fn curve_feature_point_prefix(feature_type: &str, id: &str) -> Option<String> {
53    match feature_type {
54        "S" | "SP" => Some(format!("{id}:P")),
55        "PORT" => Some(format!("{id}:Base")),
56        _ => None,
57    }
58}
59
60impl EngineState {
61
62    /// The committed sketches present at the CURRENT rollback: every sketch-like
63    /// feature (see [`curve_feature_segment_prefix`]) at index `0..=rollback`,
64    /// EXCEPT the one being edited (it displays via the live editing overlay) and
65    /// EXCEPT any sketch a downstream feature within the current rollback CONSUMED
66    /// (see [`consumed_feature_names`](Self::consumed_feature_names)). Hidden ids
67    /// are still returned (the Scene tree lists them); visibility is filtered by
68    /// the caller against [`hidden_sketches`].
69    fn committed_sketch_ids(&self) -> Vec<String> {
70        self.committed_curve_features()
71            .into_iter()
72            .map(|(id, _, _)| id)
73            .collect()
74    }
75
76    /// [`committed_sketch_ids`](Self::committed_sketch_ids) with each id's
77    /// segment-name prefix and (for a sketch) its point-name prefix — the
78    /// `(id, segment prefix, point prefix)` triples the sheet refresh draws.
79    fn committed_curve_features(&self) -> Vec<(String, String, Option<String>)> {
80        self.committed_curve_features_typed()
81            .into_iter()
82            .map(|(id, _, prefix, point_prefix)| (id, prefix, point_prefix))
83            .collect()
84    }
85
86    /// [`committed_curve_features`](Self::committed_curve_features) with each
87    /// feature's TYPE beside its id — the sheet refresh colours a port sheet
88    /// by type.
89    fn committed_curve_features_typed(&self) -> Vec<(String, String, String, Option<String>)> {
90        let editing = self.sketch_edit.as_ref().map(|edit| edit.feature_id.as_str());
91        let rollback = self.history.rollback();
92        let mut ids = Vec::new();
93        for index in 0..=rollback {
94            let Some(feature_type) = self.history.feature_type(index) else {
95                continue;
96            };
97            let Some(id) = self.history.feature_id(index) else {
98                continue;
99            };
100            let Some(prefix) = curve_feature_segment_prefix(&feature_type, &id) else {
101                continue;
102            };
103            if Some(id.as_str()) == editing {
104                continue;
105            }
106            let point_prefix = curve_feature_point_prefix(&feature_type, &id);
107            ids.push((id, feature_type, prefix, point_prefix));
108        }
109        // Drop any sketch a downstream feature CONSUMED: extrude/revolve (gated by
110        // `consumeProfileSketch`) and the sheet-metal consumers push the consumed
111        // sketch id into their `result.removed`, so a consumed sketch must vanish
112        // from the scene — no sheet, no tree row — exactly like a consumed solid.
113        // Only replay when there is a candidate to test, so a sketch-less scene pays
114        // nothing for the check.
115        if !ids.is_empty() {
116            let consumed = self.consumed_feature_names();
117            ids.retain(|(id, _, _, _)| !consumed.contains(id));
118        }
119        // The ports placed components carry: not features of this document,
120        // but the harness report lists them (with their owning component) and
121        // the ACOMP published their lines under the same `{id}:PortLine` /
122        // `{id}:Base` names a PORT feature uses. Shown per workbench.
123        if self.component_ports_visible {
124            if let Some(report) = &self.wire_harness_report {
125                for endpoint in report.endpoints.iter().filter(|e| e.component.is_some()) {
126                    if ids.iter().any(|(id, _, _, _)| *id == endpoint.id) {
127                        continue;
128                    }
129                    let Some(prefix) = curve_feature_segment_prefix("PORT", &endpoint.id) else {
130                        continue;
131                    };
132                    let point_prefix = curve_feature_point_prefix("PORT", &endpoint.id);
133                    ids.push((endpoint.id.clone(), "PORT".to_string(), prefix, point_prefix));
134                }
135            }
136        }
137        ids
138    }
139
140    /// Whether the ports placed components carry draw their sheets.
141    pub fn component_ports_visible(&self) -> bool {
142        self.component_ports_visible
143    }
144
145    /// Show or hide the sheets of the ports placed components carry (the app
146    /// drives this from the active workbench). A change refreshes the sheets.
147    pub fn set_component_ports_visible(&mut self, visible: bool) {
148        if self.component_ports_visible == visible {
149            return;
150        }
151        self.component_ports_visible = visible;
152        self.refresh_committed_sketches();
153    }
154
155    /// The sheet colour for a committed curve feature: ports are red
156    /// (waypoints lighter), everything else the sketch cyan.
157    fn curve_feature_color(&self, id: &str, feature_type: &str) -> [f32; 3] {
158        if feature_type != "PORT" {
159            return SKETCH_SHEET_COLOR;
160        }
161        let waypoint = self
162            .wire_harness_report
163            .as_ref()
164            .and_then(|report| report.endpoints.iter().find(|endpoint| endpoint.id == id))
165            .is_some_and(|endpoint| endpoint.kind == brep_kernel::PortKind::Waypoint);
166        if waypoint {
167            PORT_WAYPOINT_COLOR
168        } else {
169            PORT_TERMINATION_COLOR
170        }
171    }
172
173    /// The set of output names CONSUMED (removed) by some feature within the CURRENT
174    /// rollback prefix — a solid absorbed by a boolean, or a sketch absorbed by a
175    /// downstream extrude / revolve / sheet-metal consumer. Sourced from the SAME
176    /// per-feature `removed` signal the scene build honors
177    /// (`scene_query::resident_solid_handles`): replay the rolled-to prefix (a clean
178    /// cache hit after a build — re-tessellates nothing) and union every feature's
179    /// `removed`. Because [`prefix_request`](crate::history::History::prefix_request)
180    /// stops at the rolled-to feature, a consumer ABOVE the rollback never runs, so
181    /// its removed names are (correctly) absent — a sketch consumed above the
182    /// rollback point still shows. A malformed request yields an empty set (same as
183    /// `resident_solid_handles`).
184    fn consumed_feature_names(&self) -> std::collections::HashSet<String> {
185        let request: HistoryRequest = match serde_json::from_value(self.history.prefix_request()) {
186            Ok(request) => request,
187            Err(_) => return std::collections::HashSet::new(),
188        };
189        brep_kernel::execute_history(&request)
190            .results
191            .iter()
192            .flat_map(|feature| feature.removed.iter().cloned())
193            .collect()
194    }
195
196    /// (Re)build the persistent committed-sketch SHEET SOLIDS. For every committed
197    /// sketch that should show — [`committed_sketch_ids`](Self::committed_sketch_ids)
198    /// minus [`hidden_sketches`] — synthesize its display from the run's solved
199    /// profile AND its own model segments, and insert it as a scene solid (keyed by
200    /// the sketch id, dim-cyan, flagged `is_sketch`); remove any sheet inserted on
201    /// the PREVIOUS refresh but not this one (rolled back, deleted, hidden, or
202    /// became the active edit). A sketch that closes a region gets its planar sheet;
203    /// one that closes NOTHING (an open chain — the single line of the 2026-09-02
204    /// report) still draws its segments as named edges, so it is visible and
205    /// pickable instead of vanishing; one holding ONLY points (a hole-placement
206    /// sketch) draws them as vertices, so it too is visible, listed and pickable
207    /// (a vertex hit carries its owning sketch, which the `SKETCH` pick lane
208    /// admits). Only a sketch with no drawable geometry at all (empty /
209    /// construction-only) is skipped. Marks dirty.
210    pub fn refresh_committed_sketches(&mut self) {
211        let visible: Vec<(String, String, String, Option<String>)> = self
212            .committed_curve_features_typed()
213            .into_iter()
214            .filter(|(id, _, _, _)| !self.hidden_sketches.contains(id))
215            .collect();
216
217        // Phase 1 (immutable borrow of the surfaced profiles + paths + points):
218        // build a display payload per visible sketch — its closed profile's sheet
219        // (when it has one), every model segment that sheet does not already
220        // draw, and every model point no segment covers. A sketch with nothing to
221        // draw (no face + no edges + no points) is skipped.
222        let payloads: Vec<(String, [f32; 3], brep_kernel::DisplaySolidPayload)> = visible
223            .iter()
224            .filter_map(|(id, feature_type, prefix, point_prefix)| {
225                let profile = self
226                    .sketch_profiles
227                    .iter()
228                    .find(|(name, _)| name == id)
229                    .map(|(_, profile)| profile);
230                // The feature's own model segments, published under its segment
231                // prefix (a sketch: one per geometry, `{id}:G{gid}`; a helix: its
232                // one `{id}:HelixEdge`). The whole-chain `{id}` path duplicates
233                // them and `{id}:REF:{source}` is projected reference geometry,
234                // so neither is drawn here.
235                let segments: Vec<(String, Vec<brep_kernel::NurbsCurve>)> = self
236                    .sketch_paths
237                    .iter()
238                    .filter(|(name, _)| name.starts_with(prefix.as_str()))
239                    .cloned()
240                    .collect();
241                // The sketch's own MODEL points, published under `{id}:P{pid}`.
242                // Construction points constrain and never model, so — like
243                // construction geometry — they do not draw.
244                let points: Vec<brep_kernel::Vec3> = match point_prefix {
245                    Some(point_prefix) => self
246                        .sketch_points
247                        .iter()
248                        .filter(|(name, point)| {
249                            name.starts_with(point_prefix.as_str()) && !point.construction
250                        })
251                        .map(|(_, point)| point.position)
252                        .collect(),
253                    None => Vec::new(),
254                };
255                let payload = brep_kernel::sketch_display_payload(profile, &segments, &points);
256                if payload.mesh.indices.is_empty()
257                    && payload.edges.is_empty()
258                    && payload.vertices.is_empty()
259                {
260                    return None;
261                }
262                Some((id.clone(), self.curve_feature_color(id, feature_type), payload))
263            })
264            .collect();
265
266        // Phase 2 (mutable borrow of the scene): insert each sheet as a scene solid.
267        let mut fed: Vec<String> = Vec::with_capacity(payloads.len());
268        for (id, color, payload) in payloads {
269            let mut solid = crate::scene::solid_display_from_payload(&id, payload);
270            solid.is_sketch = true;
271            solid.color_override = Some(color);
272            self.scene.insert_solid(solid);
273            fed.push(id);
274        }
275
276        // Remove any sketch sheet inserted last time but not now.
277        let fed_set: std::collections::HashSet<&str> = fed.iter().map(String::as_str).collect();
278        let stale: Vec<String> = self
279            .shown_sketch_ids
280            .iter()
281            .filter(|id| !fed_set.contains(id.as_str()))
282            .cloned()
283            .collect();
284        for id in stale {
285            self.scene.remove_solid(&id);
286        }
287
288        self.shown_sketch_ids = fed;
289        self.dirty = true;
290    }
291
292    /// Whether the committed sketch `id`'s persistent overlay is shown (absent from
293    /// [`hidden_sketches`] = visible).
294    pub fn sketch_visible(&self, id: &str) -> bool {
295        !self.hidden_sketches.contains(id)
296    }
297
298    /// Show/hide the committed sketch `id`'s persistent overlay (the Scene-tree
299    /// checkbox). Toggles [`hidden_sketches`], rebuilds the committed overlays (so the
300    /// group is fed or cleared immediately), and marks dirty.
301    pub fn set_sketch_visible(&mut self, id: &str, visible: bool) {
302        if visible {
303            self.hidden_sketches.remove(id);
304        } else {
305            self.hidden_sketches.insert(id.to_string());
306        }
307        self.refresh_committed_sketches();
308    }
309
310    /// The committed sketches to list in the Scene tree: every `"S"` feature at the
311    /// current rollback (minus the active edit), each with its live visibility — the
312    /// ordered `(id, visible)` list the Scene panel snapshots.
313    pub fn committed_sketches(&self) -> Vec<(String, bool)> {
314        crate::visibility::named_visibility(self.committed_sketch_ids(), &self.hidden_sketches)
315    }
316
317    /// The committed sketches as JSON (`[{"name":<id>,"visible":<bool>}]`) — the
318    /// sibling of [`scene_entities_json`](Self::scene_entities_json) the Scene panel
319    /// publishes for the headed verifier (kept a SEPARATE method so the solids array's
320    /// shape is unchanged).
321    pub fn sketch_entities_json(&self) -> String {
322        crate::visibility::named_visibility_json(self.committed_sketches())
323    }
324}
325
326// BREP private tests: 695cab9bcf26f855
327