Skip to main content

brep_render/
cam_sim.rs

1//! CAM simulation player rendering (lane C17, row 2): the viewport overlay
2//! groups of the simulated stock (with an optional deviation colour map and
3//! its legend), the tool with shank and holder at a scrub pose along any
4//! tool axis, and collision / gouge markers. Pure functions over the
5//! `brep_cam` payloads; the panel in BREP_app pushes the groups through
6//! `WidgetRegistry::set_overlay_json`.
7
8use brep_cam::cutter::{ToolPart, ToolProfile};
9use brep_cam::model::{Point3, RigidTransform};
10use brep_cam::stock_sim::mesh::StockMesh;
11use brep_cam::stock_sim::remaining::RemainingStockMesh;
12use brep_cam::verify::{DeviationClass, DeviationMap};
13use serde_json::{json, Value};
14
15/// Overlay group names.
16pub const STOCK_GROUP: &str = "cam-sim-stock";
17pub const TOOL_GROUP: &str = "cam-sim-tool";
18pub const MARKS_GROUP: &str = "cam-sim-marks";
19
20/// Stock surface colour (no deviation map).
21pub const STOCK_COLOR: [f32; 3] = [0.72, 0.74, 0.70];
22/// Deviation colours: within tolerance, excess stock, gouge.
23pub const WITHIN_COLOR: [f32; 3] = [0.35, 0.75, 0.40];
24pub const EXCESS_COLOR: [f32; 3] = [0.25, 0.45, 0.95];
25pub const GOUGE_COLOR: [f32; 3] = [0.95, 0.25, 0.20];
26/// Marker colours.
27pub const COLLISION_COLOR: [f32; 3] = [1.0, 0.3, 0.1];
28pub const GOUGE_MARK_COLOR: [f32; 3] = [1.0, 0.1, 0.5];
29
30/// Colour of a deviation value: blended toward the class colour by how far
31/// it sits beyond the tolerance (saturated at `scale` beyond it).
32pub fn deviation_color(value: f64, map: &DeviationMap, scale: f64) -> [f32; 3] {
33    match map.classify(value) {
34        DeviationClass::Within => WITHIN_COLOR,
35        DeviationClass::Excess => blend(WITHIN_COLOR, EXCESS_COLOR, ((value - map.tolerance) / scale.max(1e-9)).clamp(0.0, 1.0) as f32),
36        DeviationClass::Gouge => blend(WITHIN_COLOR, GOUGE_COLOR, ((-value - map.tolerance) / scale.max(1e-9)).clamp(0.0, 1.0) as f32),
37    }
38}
39
40fn blend(a: [f32; 3], b: [f32; 3], t: f32) -> [f32; 3] {
41    if t >= 1.0 {
42        return b;
43    }
44    let t = 0.35 + 0.65 * t;
45    [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t, a[2] + (b[2] - a[2]) * t]
46}
47
48/// The legend of a deviation map: stops from the largest gouge to the
49/// largest excess with their colours, and the tolerance.
50pub fn deviation_legend(map: &DeviationMap, scale: f64) -> Value {
51    let (mut lo, mut hi) = (0.0f64, 0.0f64);
52    for v in map.values.iter().flatten() {
53        lo = lo.min(*v);
54        hi = hi.max(*v);
55    }
56    let stops: Vec<Value> = [lo, -map.tolerance, 0.0, map.tolerance, hi]
57        .iter()
58        .map(|v| {
59            let c = deviation_color(*v, map, scale);
60            json!({ "value": v, "color": c })
61        })
62        .collect();
63    json!({ "min": lo, "max": hi, "tolerance": map.tolerance, "scale": scale, "stops": stops })
64}
65
66/// The stock surface as a triangle overlay group in MODEL coordinates
67/// (`to_model` is the setup's machine-to-model transform). With a deviation
68/// map each vertex takes the colour of the column under it; without one the
69/// stock colour. An empty mesh removes the group.
70pub fn stock_group(mesh: &StockMesh, to_model: &RigidTransform, deviation: Option<(&DeviationMap, f64)>) -> Value {
71    if mesh.triangle_count() == 0 {
72        return json!({ "name": STOCK_GROUP });
73    }
74    let n = mesh.triangle_count();
75    let mut positions: Vec<f64> = Vec::with_capacity(n * 9);
76    let mut colors: Vec<f32> = Vec::with_capacity(n * 9);
77    let mut normals: Vec<f64> = Vec::with_capacity(n * 9);
78    for i in 0..n {
79        let tri = mesh.triangle(i);
80        let nrm = tri[1].sub(tri[0]).cross(tri[2].sub(tri[0])).normalized().unwrap_or(Point3::Z);
81        let nrm = to_model.apply_vector(nrm);
82        for p in tri {
83            let c = match deviation {
84                Some((map, scale)) => match map.value_at(p.x, p.y) {
85                    Some(v) => deviation_color(v, map, scale),
86                    None => STOCK_COLOR,
87                },
88                None => STOCK_COLOR,
89            };
90            let q = to_model.apply_point(p);
91            positions.extend_from_slice(&[q.x, q.y, q.z]);
92            colors.extend_from_slice(&c);
93            normals.extend_from_slice(&[nrm.x, nrm.y, nrm.z]);
94        }
95    }
96    json!({ "name": STOCK_GROUP, "renderOrder": 6, "tris": { "positions": positions, "colors": colors, "normals": normals } })
97}
98
99/// Remaining-stock colours beyond the deviation ones: excess far above the
100/// tolerance, and stock below the part (never attributed to a face).
101pub const EXCESS_HIGH_COLOR: [f32; 3] = [0.95, 0.80, 0.20];
102pub const BELOW_PART_COLOR: [f32; 3] = [0.62, 0.52, 0.80];
103
104/// Remaining-stock bands, by signed remaining material `v` (mm) and the
105/// operation tolerance `tol`: gouge red below -tol, within green in ±tol,
106/// excess blue up to 10·tol, yellow above (and where the part is out of reach).
107pub fn remaining_color(v: f64, tol: f64) -> [f32; 3] {
108    if v < -tol {
109        GOUGE_COLOR
110    } else if v <= tol {
111        WITHIN_COLOR
112    } else if v <= 10.0 * tol {
113        EXCESS_COLOR
114    } else {
115        EXCESS_HIGH_COLOR
116    }
117}
118
119/// The remaining-stock legend: the four bands with their edges in mm and
120/// colours, the tolerance, the vertex counts per class and per band, and the
121/// largest finite excess and the deepest gouge.
122pub fn remaining_legend(r: &RemainingStockMesh) -> Value {
123    let tol = r.tolerance;
124    let (within, excess, gouge) = r.counts();
125    let high = (0..r.excess.len()).filter(|&i| !r.is_below_part(i) && r.excess[i] > 10.0 * tol).count();
126    json!({
127        "tolerance": tol,
128        "bands": [
129            { "band": "gouge", "to": -tol, "color": GOUGE_COLOR },
130            { "band": "within", "from": -tol, "to": tol, "color": WITHIN_COLOR },
131            { "band": "excess", "from": tol, "to": 10.0 * tol, "color": EXCESS_COLOR },
132            { "band": "excess-high", "from": 10.0 * tol, "color": EXCESS_HIGH_COLOR },
133            { "band": "below-part", "color": BELOW_PART_COLOR },
134        ],
135        "counts": { "within": within, "excess": excess, "gouge": gouge, "excessHigh": high, "belowPart": r.below_part_count() },
136        "maxExcess": r.max.max(0.0),
137        "minValue": r.min,
138        "vertices": r.excess.len(),
139    })
140}
141
142/// The stock surface coloured by remaining material, vertex by vertex (see
143/// [`remaining_color`]), in MODEL coordinates. An empty mesh removes the group.
144pub fn remaining_group(r: &RemainingStockMesh, to_model: &RigidTransform) -> Value {
145    let mesh = &r.mesh;
146    if mesh.triangle_count() == 0 {
147        return json!({ "name": STOCK_GROUP });
148    }
149    let n = mesh.triangle_count();
150    let mut positions: Vec<f64> = Vec::with_capacity(n * 9);
151    let mut colors: Vec<f32> = Vec::with_capacity(n * 9);
152    let mut normals: Vec<f64> = Vec::with_capacity(n * 9);
153    for t in mesh.indices.chunks_exact(3) {
154        let tri = [mesh.positions[t[0] as usize], mesh.positions[t[1] as usize], mesh.positions[t[2] as usize]];
155        let nrm = tri[1].sub(tri[0]).cross(tri[2].sub(tri[0])).normalized().unwrap_or(Point3::Z);
156        let nrm = to_model.apply_vector(nrm);
157        for (k, p) in tri.into_iter().enumerate() {
158            let q = to_model.apply_point(p);
159            positions.extend_from_slice(&[q.x, q.y, q.z]);
160            let i = t[k] as usize;
161            colors.extend_from_slice(&if r.is_below_part(i) { BELOW_PART_COLOR } else { remaining_color(r.excess[i], r.tolerance) });
162            normals.extend_from_slice(&[nrm.x, nrm.y, nrm.z]);
163        }
164    }
165    json!({ "name": STOCK_GROUP, "renderOrder": 6, "tris": { "positions": positions, "colors": colors, "normals": normals } })
166}
167
168/// The tool's wireframe (cutting part, shank, holder in their colours) with
169/// its tip at `tip` along the unit `axis`, in machine coordinates, mapped
170/// to the model by `to_model`.
171pub fn tool_group(profile: &ToolProfile, tip: Point3, axis: Point3, to_model: &RigidTransform) -> Value {
172    const SIDES: usize = 24;
173    const CUTTING: [f32; 3] = [0.95, 0.90, 0.35];
174    const SHANK: [f32; 3] = [0.75, 0.75, 0.78];
175    const HOLDER: [f32; 3] = [0.55, 0.58, 0.62];
176    let a = axis.normalized().unwrap_or(Point3::Z);
177    let e1 = brep_cam::geom::perpendicular(a);
178    let e2 = a.cross(e1);
179    let at = |radius: f64, height: f64, angle: f64| -> Point3 {
180        to_model.apply_point(tip.add(a.scale(height)).add(e1.scale(radius * angle.cos())).add(e2.scale(radius * angle.sin())))
181    };
182    let colour = |part: ToolPart| match part {
183        ToolPart::Holder => HOLDER,
184        ToolPart::Shank => SHANK,
185        ToolPart::Cutting => CUTTING,
186    };
187    let mut positions: Vec<f64> = Vec::new();
188    let mut colors: Vec<f32> = Vec::new();
189    let mut line = |p: Point3, q: Point3, c: [f32; 3]| {
190        positions.extend_from_slice(&[p.x, p.y, p.z, q.x, q.y, q.z]);
191        colors.extend_from_slice(&c);
192        colors.extend_from_slice(&c);
193    };
194    for point in &profile.points {
195        if point.radius <= 0.0 {
196            continue;
197        }
198        let c = colour(point.part);
199        for i in 0..SIDES {
200            let a0 = i as f64 / SIDES as f64 * std::f64::consts::TAU;
201            let a1 = (i + 1) as f64 / SIDES as f64 * std::f64::consts::TAU;
202            line(at(point.radius, point.height, a0), at(point.radius, point.height, a1), c);
203        }
204    }
205    for w in profile.points.windows(2) {
206        let c = colour(w[1].part);
207        for k in 0..4 {
208            let angle = k as f64 * std::f64::consts::FRAC_PI_2;
209            line(at(w[0].radius, w[0].height, angle), at(w[1].radius, w[1].height, angle), c);
210        }
211    }
212    // The axis itself, from the tip past the top.
213    let top = profile.top_height();
214    line(to_model.apply_point(tip), to_model.apply_point(tip.add(a.scale(top * 1.2 + 1.0))), [1.0, 0.95, 0.2]);
215    json!({ "name": TOOL_GROUP, "renderOrder": 8, "lines": { "positions": positions, "colors": colors } })
216}
217
218/// A collision or gouge marker.
219#[derive(Clone, Debug, PartialEq)]
220pub struct Marker {
221    /// Machine-coordinate point.
222    pub at: Point3,
223    pub gouge: bool,
224}
225
226/// The markers as a point group; none removes the group.
227pub fn marks_group(marks: &[Marker], to_model: &RigidTransform) -> Value {
228    if marks.is_empty() {
229        return json!({ "name": MARKS_GROUP });
230    }
231    let mut positions: Vec<f64> = Vec::with_capacity(marks.len() * 3);
232    let mut colors: Vec<f32> = Vec::with_capacity(marks.len() * 3);
233    for m in marks {
234        let p = to_model.apply_point(m.at);
235        positions.extend_from_slice(&[p.x, p.y, p.z]);
236        colors.extend_from_slice(&if m.gouge { GOUGE_MARK_COLOR } else { COLLISION_COLOR });
237    }
238    json!({ "name": MARKS_GROUP, "renderOrder": 9, "points": { "positions": positions, "colors": colors, "size": 12.0 } })
239}
240
241/// ASCII STL of the stock mesh in MODEL coordinates.
242pub fn stock_stl(mesh: &StockMesh, to_model: &RigidTransform, name: &str) -> String {
243    brep_kernel::write_ascii_stl(
244        name,
245        (0..mesh.triangle_count()).map(|i| {
246            let tri = mesh.triangle(i);
247            let nrm = to_model.apply_vector(tri[1].sub(tri[0]).cross(tri[2].sub(tri[0])).normalized().unwrap_or(Point3::Z));
248            let corners = tri.map(|p| {
249                let q = to_model.apply_point(p);
250                [q.x, q.y, q.z]
251            });
252            ([nrm.x, nrm.y, nrm.z], corners)
253        }),
254    )
255}
256
257/// The simulation's door to the background runner (lane C17b): the app's
258/// panel holds the transport and a view of the stock; the runner holds the
259/// stock and advances it in bounded chunks.
260impl crate::engine_state::EngineState {
261    /// Submit one simulation command; `false` when this runner cannot
262    /// simulate (the panel then simulates inline).
263    pub fn cam_sim_submit(&mut self, job: crate::runner::CamSimJob) -> bool {
264        self.runner.submit_cam_sim(job)
265    }
266
267    /// The next landed simulation reply, oldest first.
268    pub fn cam_sim_poll(&mut self) -> Option<crate::runner::CamSimReply> {
269        self.runner.poll_cam_sim()
270    }
271
272    /// The newest progress of the chunk in flight.
273    pub fn cam_sim_poll_progress(&mut self) -> Option<crate::runner::CamSimProgress> {
274        self.runner.poll_cam_sim_progress()
275    }
276
277    /// Stop the chunk in flight between two motions where the runner can
278    /// (the native thread); the panel drops its reply either way.
279    pub fn cam_sim_cancel(&mut self) {
280        self.runner.cancel_cam_sim();
281    }
282}
283