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