1use 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
15pub const STOCK_GROUP: &str = "cam-sim-stock";
17pub const TOOL_GROUP: &str = "cam-sim-tool";
18pub const MARKS_GROUP: &str = "cam-sim-marks";
19
20pub const STOCK_COLOR: [f32; 3] = [0.72, 0.74, 0.70];
22pub 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];
26pub 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
30pub 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
48pub 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
66pub 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
99pub 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
104pub 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
119pub 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
142pub 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
168pub 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 = a.cross(if a.z.abs() < 0.9 { Point3::Z } else { Point3::X }).normalized().unwrap_or(Point3::X);
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 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#[derive(Clone, Debug, PartialEq)]
220pub struct Marker {
221 pub at: Point3,
223 pub gouge: bool,
224}
225
226pub 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
241pub fn stock_stl(mesh: &StockMesh, to_model: &RigidTransform, name: &str) -> String {
243 let mut out = String::with_capacity(mesh.triangle_count() * 160 + 64);
244 out.push_str(&format!("solid {name}\n"));
245 for i in 0..mesh.triangle_count() {
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 out.push_str(&format!(" facet normal {} {} {}\n outer loop\n", nrm.x, nrm.y, nrm.z));
249 for p in tri {
250 let q = to_model.apply_point(p);
251 out.push_str(&format!(" vertex {} {} {}\n", q.x, q.y, q.z));
252 }
253 out.push_str(" endloop\n endfacet\n");
254 }
255 out.push_str(&format!("endsolid {name}\n"));
256 out
257}
258
259impl crate::engine_state::EngineState {
263 pub fn cam_sim_submit(&mut self, job: crate::runner::CamSimJob) -> bool {
266 self.runner.submit_cam_sim(job)
267 }
268
269 pub fn cam_sim_poll(&mut self) -> Option<crate::runner::CamSimReply> {
271 self.runner.poll_cam_sim()
272 }
273
274 pub fn cam_sim_poll_progress(&mut self) -> Option<crate::runner::CamSimProgress> {
276 self.runner.poll_cam_sim_progress()
277 }
278
279 pub fn cam_sim_cancel(&mut self) {
282 self.runner.cancel_cam_sim();
283 }
284}
285