use serde_json::Value;
use crate::feature_dimensions::{
append_plain_leader, leaders_buffers, FeatureDimAnnotation,
};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ConstraintOverlayKind {
Distance,
Angle,
Leader,
}
#[derive(Clone, Debug)]
pub struct ConstraintOverlay {
pub id: String,
pub constraint_type: String,
pub icon: String,
pub status: String,
pub message: String,
pub kind: ConstraintOverlayKind,
pub anchors: Vec<[f64; 3]>,
pub groups: Vec<Vec<usize>>,
pub annotation: Option<FeatureDimAnnotation>,
pub value: Option<f64>,
pub unit: String,
pub draggable: bool,
pub input_params: Value,
pub elements: Vec<String>,
}
impl ConstraintOverlay {
pub fn field_key(&self) -> Option<&'static str> {
match self.kind {
ConstraintOverlayKind::Distance => Some("distance"),
ConstraintOverlayKind::Angle => Some("angle"),
ConstraintOverlayKind::Leader => None,
}
}
pub fn label_anchor(&self, world_per_pixel: f64) -> Option<[f64; 3]> {
if let Some(annotation) = &self.annotation {
return Some(match self.kind {
ConstraintOverlayKind::Angle => {
crate::feature_dimensions::angular_chip_anchor(annotation, world_per_pixel)
}
_ => annotation.midpoint(),
});
}
if let Some((span, _)) = self.role_leaders() {
return Some(co_mid(span.0, span.1));
}
match self.anchors.len() {
0 => None,
1 => Some(self.anchors[0]),
_ => {
let a = self.anchors[0];
let b = self.anchors[1];
Some([(a[0] + b[0]) * 0.5, (a[1] + b[1]) * 0.5, (a[2] + b[2]) * 0.5])
}
}
}
fn role_leaders(&self) -> Option<(([f64; 3], [f64; 3]), ([f64; 3], [f64; 3]))> {
let [width, tab] = self.groups.as_slice() else {
return None;
};
let [w0, w1] = width.as_slice() else {
return None;
};
let (a, b) = (*self.anchors.get(*w0)?, *self.anchors.get(*w1)?);
if tab.is_empty() {
return None;
}
let mut centroid = [0.0f64; 3];
for &index in tab {
let p = self.anchors.get(index)?;
for k in 0..3 {
centroid[k] += p[k] / tab.len() as f64;
}
}
Some(((a, b), (co_mid(a, b), centroid)))
}
pub fn label_text(&self) -> String {
let lead = if self.icon.is_empty() { self.id.as_str() } else { self.icon.as_str() };
match self.value {
Some(value) => {
let n = crate::formatting::compact_decimal(value, 2);
if self.unit == "deg" {
format!("{lead} {n}\u{00b0}")
} else if self.unit.is_empty() {
format!("{lead} {n}")
} else {
format!("{lead} {n} {}", self.unit)
}
}
None => lead.to_string(),
}
}
}
pub fn status_color(status: &str) -> [f32; 3] {
let [r, g, b] = crate::assembly_status::status_color_rgb(status);
[r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0]
}
pub fn build_constraint_overlays(
overlay_rows: &Value,
state_constraints: &Value,
) -> Vec<ConstraintOverlay> {
let Some(rows) = overlay_rows.as_array() else {
return Vec::new();
};
rows.iter()
.filter_map(|row| build_row(row, state_constraints))
.collect()
}
fn build_row(row: &Value, state_constraints: &Value) -> Option<ConstraintOverlay> {
let id = row.get("id")?.as_str()?.to_string();
let constraint_type = row
.get("type")
.and_then(Value::as_str)
.unwrap_or("")
.to_string();
let icon = brep_kernel::constraint_type(&constraint_type)
.map(|def| def.icon.to_string())
.unwrap_or_default();
let status = row
.get("status")
.and_then(Value::as_str)
.unwrap_or("")
.to_string();
let message = row
.get("message")
.and_then(Value::as_str)
.unwrap_or("")
.to_string();
let anchors = read_points(row.get("anchors"));
let directions = read_dirs(row.get("directions"));
let geoms = read_strings(row.get("geoms"));
let groups = read_groups(row.get("groups"));
let value = row.get("value").and_then(Value::as_f64);
let unit = row
.get("unit")
.and_then(Value::as_str)
.unwrap_or("")
.to_string();
let input_params = state_constraints
.as_array()
.and_then(|list| {
list.iter().find(|entry| {
entry
.get("inputParams")
.and_then(|p| p.get("id"))
.and_then(Value::as_str)
== Some(id.as_str())
})
})
.and_then(|entry| entry.get("inputParams"))
.cloned()
.unwrap_or_else(|| Value::Object(serde_json::Map::new()));
let elements = crate::json_support::string_values(input_params.get("elements"))
.map(str::to_string)
.collect();
let kind = match constraint_type.as_str() {
"distance" => ConstraintOverlayKind::Distance,
"angle" => ConstraintOverlayKind::Angle,
_ => ConstraintOverlayKind::Leader,
};
let draggable = match kind {
ConstraintOverlayKind::Leader => false,
ConstraintOverlayKind::Distance => param_allows_drag(&input_params, "distance"),
ConstraintOverlayKind::Angle => param_allows_drag(&input_params, "angle"),
};
let annotation = match kind {
ConstraintOverlayKind::Distance => {
build_distance_annotation(&anchors, &directions, &geoms, value)
}
ConstraintOverlayKind::Angle => build_angle_annotation(&anchors, &directions, value),
ConstraintOverlayKind::Leader => None,
};
Some(ConstraintOverlay {
id,
constraint_type,
icon,
status,
message,
kind,
anchors,
groups,
annotation,
value: match kind {
ConstraintOverlayKind::Leader => None,
_ => value,
},
unit,
draggable,
input_params,
elements,
})
}
fn param_allows_drag(params: &Value, key: &str) -> bool {
match params.get(key) {
None | Some(Value::Null) => true,
Some(Value::Number(_)) => true,
Some(Value::String(text)) => text.trim().parse::<f64>().is_ok(),
_ => false,
}
}
fn build_distance_annotation(
anchors: &[[f64; 3]],
directions: &[Option<[f64; 3]>],
geoms: &[String],
value: Option<f64>,
) -> Option<FeatureDimAnnotation> {
if anchors.len() < 2 {
return None;
}
let base = (0..2).find(|&i| {
geoms.get(i).map(String::as_str) == Some("plane")
&& directions
.get(i)
.copied()
.flatten()
.is_some_and(|n| co_norm(n) > 1e-9)
});
if let Some(base) = base {
let n = directions[base].expect("base index checked above");
let len = co_norm(n);
let n = [n[0] / len, n[1] / len, n[2] / len];
let q = anchors[base];
let p = anchors[1 - base];
let s = co_dot(co_sub(p, q), n);
let foot = [p[0] - n[0] * s, p[1] - n[1] * s, p[2] - n[2] * s];
let mut annotation =
FeatureDimAnnotation::linear("distance", foot, p, value.unwrap_or(s), "D");
annotation.axis = n;
return Some(annotation);
}
let a = anchors[0];
let b = anchors[1];
let value = value.unwrap_or_else(|| co_norm(co_sub(b, a)));
Some(FeatureDimAnnotation::linear("distance", a, b, value, "D"))
}
fn build_angle_annotation(
anchors: &[[f64; 3]],
directions: &[Option<[f64; 3]>],
value: Option<f64>,
) -> Option<FeatureDimAnnotation> {
if anchors.len() < 2 || directions.len() < 2 {
return None;
}
let d0 = directions[0]?;
let d1 = directions[1]?;
let axis = co_cross(d0, d1);
let center = carrier_closest_midpoint(anchors[0], d0, anchors[1], d1);
let value = value.unwrap_or(0.0).clamp(-360.0, 360.0);
Some(FeatureDimAnnotation::angular(
"angle", center, axis, d0, value, "A",
))
}
fn carrier_closest_midpoint(a: [f64; 3], da: [f64; 3], b: [f64; 3], db: [f64; 3]) -> [f64; 3] {
let mid = |p: [f64; 3], q: [f64; 3]| {
[(p[0] + q[0]) * 0.5, (p[1] + q[1]) * 0.5, (p[2] + q[2]) * 0.5]
};
let da_n = co_norm(da);
let db_n = co_norm(db);
if da_n < 1e-9 || db_n < 1e-9 {
return mid(a, b);
}
let u = [da[0] / da_n, da[1] / da_n, da[2] / da_n];
let v = [db[0] / db_n, db[1] / db_n, db[2] / db_n];
let w0 = co_sub(a, b);
let b_uv = co_dot(u, v);
let denom = 1.0 - b_uv * b_uv;
if denom.abs() < 1e-9 {
return mid(a, b); }
let d = co_dot(u, w0);
let e = co_dot(v, w0);
let t = (b_uv * e - d) / denom;
let s = (e - b_uv * d) / denom;
let p = [a[0] + u[0] * t, a[1] + u[1] * t, a[2] + u[2] * t];
let q = [b[0] + v[0] * s, b[1] + v[1] * s, b[2] + v[2] * s];
mid(p, q)
}
pub fn constraint_overlay_buffers(
overlays: &[ConstraintOverlay],
world_per_pixel: f64,
) -> (Vec<f32>, Vec<f32>) {
let annotations: Vec<FeatureDimAnnotation> = overlays
.iter()
.filter_map(|overlay| overlay.annotation.clone())
.collect();
let (mut positions, mut colors) = leaders_buffers(&annotations, world_per_pixel);
for overlay in overlays {
if overlay.kind != ConstraintOverlayKind::Leader {
continue;
}
if let Some((span, tab)) = overlay.role_leaders() {
append_plain_leader(&mut positions, &mut colors, span.0, span.1, world_per_pixel);
append_plain_leader(&mut positions, &mut colors, tab.0, tab.1, world_per_pixel);
} else if overlay.anchors.len() >= 2 {
append_plain_leader(
&mut positions,
&mut colors,
overlay.anchors[0],
overlay.anchors[1],
world_per_pixel,
);
}
}
(positions, colors)
}
fn read_points(value: Option<&Value>) -> Vec<[f64; 3]> {
value
.and_then(Value::as_array)
.map(|list| list.iter().filter_map(read_point3).collect())
.unwrap_or_default()
}
fn read_dirs(value: Option<&Value>) -> Vec<Option<[f64; 3]>> {
value
.and_then(Value::as_array)
.map(|list| list.iter().map(read_point3).collect())
.unwrap_or_default()
}
fn read_strings(value: Option<&Value>) -> Vec<String> {
value
.and_then(Value::as_array)
.map(|list| {
list.iter()
.map(|v| v.as_str().unwrap_or("").to_string())
.collect()
})
.unwrap_or_default()
}
fn read_groups(value: Option<&Value>) -> Vec<Vec<usize>> {
value
.and_then(Value::as_array)
.map(|groups| {
groups
.iter()
.map(|group| {
group
.as_array()
.map(|list| {
list.iter()
.filter_map(Value::as_u64)
.map(|index| index as usize)
.collect()
})
.unwrap_or_default()
})
.collect()
})
.unwrap_or_default()
}
fn read_point3(value: &Value) -> Option<[f64; 3]> {
let list = value.as_array()?;
Some([
list.first()?.as_f64()?,
list.get(1)?.as_f64()?,
list.get(2)?.as_f64()?,
])
}
fn co_mid(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
[(a[0] + b[0]) * 0.5, (a[1] + b[1]) * 0.5, (a[2] + b[2]) * 0.5]
}
fn co_sub(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
[a[0] - b[0], a[1] - b[1], a[2] - b[2]]
}
fn co_dot(a: [f64; 3], b: [f64; 3]) -> f64 {
a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
}
fn co_norm(v: [f64; 3]) -> f64 {
co_dot(v, v).sqrt()
}
fn co_cross(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
[
a[1] * b[2] - a[2] * b[1],
a[2] * b[0] - a[0] * b[2],
a[0] * b[1] - a[1] * b[0],
]
}