use std::collections::HashMap;
use std::path::Path;
use serde::{Deserialize, Serialize};
use super::probe::{Probe, Role};
use super::rules_hole::{aligned_quadrants, hole_finding, Group};
use crate::linter::Diagnostic;
pub const EPS_VOLUME: f64 = 0.01;
pub const INSET: f64 = 0.1;
pub const RING_WIDTH: f64 = 0.8;
pub const PROBE_DEPTH: f64 = 2.0;
pub const BORE_MARGIN: f64 = 0.5;
pub const NEAR_MARGIN: f64 = 4.0;
pub const OPENING_DEPTH: f64 = 3.0;
pub const UNMEASURED_HEIGHT: f64 = 2.0;
pub const BODY_INTERFERENCE: &str = "body_interference";
pub const HOLE_MISALIGNED: &str = "hole_misaligned";
pub const CONNECTOR_BLOCKED: &str = "connector_blocked";
#[derive(Debug, Clone, Deserialize)]
pub struct Measurement {
pub probe: String,
pub body: String,
pub volume: f64,
#[serde(default)]
pub at: Option<[f64; 3]>,
#[serde(default)]
pub collision: Option<String>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct PairMeasurement {
pub a: String,
pub b: String,
pub volume: f64,
#[serde(default)]
pub at: Option<[f64; 3]>,
#[serde(default)]
pub collision: Option<String>,
}
#[derive(Debug, Default, Deserialize)]
pub struct Response {
#[serde(default)]
pub hits: Vec<Measurement>,
#[serde(default)]
pub pairs: Vec<PairMeasurement>,
#[serde(default)]
pub invalid: Vec<String>,
#[serde(default)]
pub version: u32,
#[serde(default)]
pub measured: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct Violation {
pub rule: &'static str,
pub message: String,
pub parts: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub volume: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub at: Option<[f64; 3]>,
#[serde(skip_serializing_if = "Option::is_none")]
pub collision: Option<String>,
}
impl Violation {
fn from_hit(rule: &'static str, message: String, parts: [&str; 2], hit: &Measurement) -> Self {
Self {
rule,
message,
parts: parts.map(str::to_string).to_vec(),
volume: Some(hit.volume),
at: hit.at,
collision: hit.collision.clone(),
}
}
pub fn diagnostic(&self) -> Diagnostic {
let help = match self.rule {
BODY_INTERFERENCE => {
"move the body or the part, or reshape the body around the board; two bodies \
that overlap by more than 0.01 mm³ blow the fuse, so give a lip a clearance"
}
HOLE_MISALIGNED => {
"line the boss up with the hole and keep its bore at least as wide, or remove the boss"
}
_ => "open the wall in front of the connector or move the body away from the edge",
};
Diagnostic {
rule: self.rule.to_string(),
message: self.message.clone(),
help: Some(help.to_string()),
..Default::default()
}
}
}
pub(super) fn solid(volume: f64) -> bool {
volume > EPS_VOLUME
}
fn violation_of(
probe: &Probe,
hit: &Measurement,
quadrants: &HashMap<Group, usize>,
) -> Option<Violation> {
let (body, board, volume) = (hit.body.as_str(), probe.board.as_str(), hit.volume);
let (rule, message) = match &probe.role {
Role::Slab => (
BODY_INTERFERENCE,
format!("body `{body}` overlaps board `{board}` by {volume:.2} mm³"),
),
Role::KeepOut { component } => (
BODY_INTERFERENCE,
format!(
"body `{body}` overlaps component `{component}` of board `{board}` by {volume:.2} mm³"
),
),
Role::Opening { component, side } => (
CONNECTOR_BLOCKED,
format!(
"body `{body}` blocks the opening of `{component}` on the {side} side of board `{board}` \
by {volume:.2} mm³"
),
),
Role::HoleNear { .. } | Role::HoleBore { .. } => hole_finding(probe, hit, quadrants)?,
Role::HoleQuadrant { .. } => return None,
};
Some(Violation::from_hit(rule, message, [body, board], hit))
}
pub fn violations(probes: &[Probe], response: &Response) -> Vec<Violation> {
let roles: HashMap<&str, &Probe> = probes.iter().map(|p| (p.id.as_str(), p)).collect();
let quadrants = aligned_quadrants(&roles, &response.hits);
let from_probes = response
.hits
.iter()
.filter(|hit| solid(hit.volume))
.filter_map(|hit| violation_of(roles.get(hit.probe.as_str())?, hit, &quadrants));
let from_pairs = response
.pairs
.iter()
.filter(|pair| solid(pair.volume))
.map(|pair| Violation {
rule: BODY_INTERFERENCE,
message: format!(
"body `{}` overlaps body `{}` by {:.2} mm³",
pair.a, pair.b, pair.volume
),
parts: vec![pair.a.clone(), pair.b.clone()],
volume: Some(pair.volume),
at: pair.at,
collision: pair.collision.clone(),
});
from_probes.chain(from_pairs).collect()
}
#[derive(Serialize)]
struct Report<'a> {
version: u32,
fuse: &'static str,
violations: &'a [Violation],
}
pub fn write_report(path: &Path, violations: &[Violation]) -> std::io::Result<()> {
let report = Report {
version: 1,
fuse: if violations.is_empty() {
"intact"
} else {
"blown"
},
violations,
};
let text = serde_json::to_string_pretty(&report).map_err(std::io::Error::other)?;
super::atomic::write_atomic(path, (text + "\n").as_bytes())
}