use brep_cam::cutter::{ToolPart, ToolProfile};
use brep_cam::model::{Point3, RigidTransform};
use brep_cam::stock_sim::mesh::StockMesh;
use brep_cam::stock_sim::remaining::RemainingStockMesh;
use brep_cam::verify::{DeviationClass, DeviationMap};
use serde_json::{json, Value};
pub const STOCK_GROUP: &str = "cam-sim-stock";
pub const TOOL_GROUP: &str = "cam-sim-tool";
pub const MARKS_GROUP: &str = "cam-sim-marks";
pub const STOCK_COLOR: [f32; 3] = [0.72, 0.74, 0.70];
pub const WITHIN_COLOR: [f32; 3] = [0.35, 0.75, 0.40];
pub const EXCESS_COLOR: [f32; 3] = [0.25, 0.45, 0.95];
pub const GOUGE_COLOR: [f32; 3] = [0.95, 0.25, 0.20];
pub const COLLISION_COLOR: [f32; 3] = [1.0, 0.3, 0.1];
pub const GOUGE_MARK_COLOR: [f32; 3] = [1.0, 0.1, 0.5];
pub fn deviation_color(value: f64, map: &DeviationMap, scale: f64) -> [f32; 3] {
match map.classify(value) {
DeviationClass::Within => WITHIN_COLOR,
DeviationClass::Excess => blend(WITHIN_COLOR, EXCESS_COLOR, ((value - map.tolerance) / scale.max(1e-9)).clamp(0.0, 1.0) as f32),
DeviationClass::Gouge => blend(WITHIN_COLOR, GOUGE_COLOR, ((-value - map.tolerance) / scale.max(1e-9)).clamp(0.0, 1.0) as f32),
}
}
fn blend(a: [f32; 3], b: [f32; 3], t: f32) -> [f32; 3] {
if t >= 1.0 {
return b;
}
let t = 0.35 + 0.65 * t;
[a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t, a[2] + (b[2] - a[2]) * t]
}
pub fn deviation_legend(map: &DeviationMap, scale: f64) -> Value {
let (mut lo, mut hi) = (0.0f64, 0.0f64);
for v in map.values.iter().flatten() {
lo = lo.min(*v);
hi = hi.max(*v);
}
let stops: Vec<Value> = [lo, -map.tolerance, 0.0, map.tolerance, hi]
.iter()
.map(|v| {
let c = deviation_color(*v, map, scale);
json!({ "value": v, "color": c })
})
.collect();
json!({ "min": lo, "max": hi, "tolerance": map.tolerance, "scale": scale, "stops": stops })
}
pub fn stock_group(mesh: &StockMesh, to_model: &RigidTransform, deviation: Option<(&DeviationMap, f64)>) -> Value {
if mesh.triangle_count() == 0 {
return json!({ "name": STOCK_GROUP });
}
let n = mesh.triangle_count();
let mut positions: Vec<f64> = Vec::with_capacity(n * 9);
let mut colors: Vec<f32> = Vec::with_capacity(n * 9);
let mut normals: Vec<f64> = Vec::with_capacity(n * 9);
for i in 0..n {
let tri = mesh.triangle(i);
let nrm = tri[1].sub(tri[0]).cross(tri[2].sub(tri[0])).normalized().unwrap_or(Point3::Z);
let nrm = to_model.apply_vector(nrm);
for p in tri {
let c = match deviation {
Some((map, scale)) => match map.value_at(p.x, p.y) {
Some(v) => deviation_color(v, map, scale),
None => STOCK_COLOR,
},
None => STOCK_COLOR,
};
let q = to_model.apply_point(p);
positions.extend_from_slice(&[q.x, q.y, q.z]);
colors.extend_from_slice(&c);
normals.extend_from_slice(&[nrm.x, nrm.y, nrm.z]);
}
}
json!({ "name": STOCK_GROUP, "renderOrder": 6, "tris": { "positions": positions, "colors": colors, "normals": normals } })
}
pub const EXCESS_HIGH_COLOR: [f32; 3] = [0.95, 0.80, 0.20];
pub const BELOW_PART_COLOR: [f32; 3] = [0.62, 0.52, 0.80];
pub fn remaining_color(v: f64, tol: f64) -> [f32; 3] {
if v < -tol {
GOUGE_COLOR
} else if v <= tol {
WITHIN_COLOR
} else if v <= 10.0 * tol {
EXCESS_COLOR
} else {
EXCESS_HIGH_COLOR
}
}
pub fn remaining_legend(r: &RemainingStockMesh) -> Value {
let tol = r.tolerance;
let (within, excess, gouge) = r.counts();
let high = (0..r.excess.len()).filter(|&i| !r.is_below_part(i) && r.excess[i] > 10.0 * tol).count();
json!({
"tolerance": tol,
"bands": [
{ "band": "gouge", "to": -tol, "color": GOUGE_COLOR },
{ "band": "within", "from": -tol, "to": tol, "color": WITHIN_COLOR },
{ "band": "excess", "from": tol, "to": 10.0 * tol, "color": EXCESS_COLOR },
{ "band": "excess-high", "from": 10.0 * tol, "color": EXCESS_HIGH_COLOR },
{ "band": "below-part", "color": BELOW_PART_COLOR },
],
"counts": { "within": within, "excess": excess, "gouge": gouge, "excessHigh": high, "belowPart": r.below_part_count() },
"maxExcess": r.max.max(0.0),
"minValue": r.min,
"vertices": r.excess.len(),
})
}
pub fn remaining_group(r: &RemainingStockMesh, to_model: &RigidTransform) -> Value {
let mesh = &r.mesh;
if mesh.triangle_count() == 0 {
return json!({ "name": STOCK_GROUP });
}
let n = mesh.triangle_count();
let mut positions: Vec<f64> = Vec::with_capacity(n * 9);
let mut colors: Vec<f32> = Vec::with_capacity(n * 9);
let mut normals: Vec<f64> = Vec::with_capacity(n * 9);
for t in mesh.indices.chunks_exact(3) {
let tri = [mesh.positions[t[0] as usize], mesh.positions[t[1] as usize], mesh.positions[t[2] as usize]];
let nrm = tri[1].sub(tri[0]).cross(tri[2].sub(tri[0])).normalized().unwrap_or(Point3::Z);
let nrm = to_model.apply_vector(nrm);
for (k, p) in tri.into_iter().enumerate() {
let q = to_model.apply_point(p);
positions.extend_from_slice(&[q.x, q.y, q.z]);
let i = t[k] as usize;
colors.extend_from_slice(&if r.is_below_part(i) { BELOW_PART_COLOR } else { remaining_color(r.excess[i], r.tolerance) });
normals.extend_from_slice(&[nrm.x, nrm.y, nrm.z]);
}
}
json!({ "name": STOCK_GROUP, "renderOrder": 6, "tris": { "positions": positions, "colors": colors, "normals": normals } })
}
pub fn tool_group(profile: &ToolProfile, tip: Point3, axis: Point3, to_model: &RigidTransform) -> Value {
const SIDES: usize = 24;
const CUTTING: [f32; 3] = [0.95, 0.90, 0.35];
const SHANK: [f32; 3] = [0.75, 0.75, 0.78];
const HOLDER: [f32; 3] = [0.55, 0.58, 0.62];
let a = axis.normalized().unwrap_or(Point3::Z);
let e1 = brep_cam::geom::perpendicular(a);
let e2 = a.cross(e1);
let at = |radius: f64, height: f64, angle: f64| -> Point3 {
to_model.apply_point(tip.add(a.scale(height)).add(e1.scale(radius * angle.cos())).add(e2.scale(radius * angle.sin())))
};
let colour = |part: ToolPart| match part {
ToolPart::Holder => HOLDER,
ToolPart::Shank => SHANK,
ToolPart::Cutting => CUTTING,
};
let mut positions: Vec<f64> = Vec::new();
let mut colors: Vec<f32> = Vec::new();
let mut line = |p: Point3, q: Point3, c: [f32; 3]| {
positions.extend_from_slice(&[p.x, p.y, p.z, q.x, q.y, q.z]);
colors.extend_from_slice(&c);
colors.extend_from_slice(&c);
};
for point in &profile.points {
if point.radius <= 0.0 {
continue;
}
let c = colour(point.part);
for i in 0..SIDES {
let a0 = i as f64 / SIDES as f64 * std::f64::consts::TAU;
let a1 = (i + 1) as f64 / SIDES as f64 * std::f64::consts::TAU;
line(at(point.radius, point.height, a0), at(point.radius, point.height, a1), c);
}
}
for w in profile.points.windows(2) {
let c = colour(w[1].part);
for k in 0..4 {
let angle = k as f64 * std::f64::consts::FRAC_PI_2;
line(at(w[0].radius, w[0].height, angle), at(w[1].radius, w[1].height, angle), c);
}
}
let top = profile.top_height();
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]);
json!({ "name": TOOL_GROUP, "renderOrder": 8, "lines": { "positions": positions, "colors": colors } })
}
#[derive(Clone, Debug, PartialEq)]
pub struct Marker {
pub at: Point3,
pub gouge: bool,
}
pub fn marks_group(marks: &[Marker], to_model: &RigidTransform) -> Value {
if marks.is_empty() {
return json!({ "name": MARKS_GROUP });
}
let mut positions: Vec<f64> = Vec::with_capacity(marks.len() * 3);
let mut colors: Vec<f32> = Vec::with_capacity(marks.len() * 3);
for m in marks {
let p = to_model.apply_point(m.at);
positions.extend_from_slice(&[p.x, p.y, p.z]);
colors.extend_from_slice(&if m.gouge { GOUGE_MARK_COLOR } else { COLLISION_COLOR });
}
json!({ "name": MARKS_GROUP, "renderOrder": 9, "points": { "positions": positions, "colors": colors, "size": 12.0 } })
}
pub fn stock_stl(mesh: &StockMesh, to_model: &RigidTransform, name: &str) -> String {
brep_kernel::write_ascii_stl(
name,
(0..mesh.triangle_count()).map(|i| {
let tri = mesh.triangle(i);
let nrm = to_model.apply_vector(tri[1].sub(tri[0]).cross(tri[2].sub(tri[0])).normalized().unwrap_or(Point3::Z));
let corners = tri.map(|p| {
let q = to_model.apply_point(p);
[q.x, q.y, q.z]
});
([nrm.x, nrm.y, nrm.z], corners)
}),
)
}
impl crate::engine_state::EngineState {
pub fn cam_sim_submit(&mut self, job: crate::runner::CamSimJob) -> bool {
self.runner.submit_cam_sim(job)
}
pub fn cam_sim_poll(&mut self) -> Option<crate::runner::CamSimReply> {
self.runner.poll_cam_sim()
}
pub fn cam_sim_poll_progress(&mut self) -> Option<crate::runner::CamSimProgress> {
self.runner.poll_cam_sim_progress()
}
pub fn cam_sim_cancel(&mut self) {
self.runner.cancel_cam_sim();
}
}