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