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 limited = input_params.get("limit").and_then(Value::as_bool).unwrap_or(false);
let draggable = match kind {
ConstraintOverlayKind::Leader => false,
ConstraintOverlayKind::Distance => !limited && param_allows_drag(&input_params, "distance"),
ConstraintOverlayKind::Angle => !limited && 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,
&geoms,
value,
row.get("angleAxis")
.cloned()
.and_then(|v| serde_json::from_value(v).ok()),
),
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]>],
geoms: &[String],
value: Option<f64>,
reference_axis: Option<[f64; 3]>,
) -> Option<FeatureDimAnnotation> {
if anchors.len() < 2 || directions.len() < 2 {
return None;
}
let d0 = directions[0]?;
let d1 = directions[1]?;
let axis = reference_axis.unwrap_or_else(|| co_cross(d0, d1));
let center = angle_vertex(anchors, d0, d1, geoms);
let ref_dir = if geoms.get(0).map(String::as_str) == Some("plane")
&& geoms.get(1).map(String::as_str) == Some("plane")
{
let tangent = co_cross(axis, d0);
if co_dot(tangent, co_sub(anchors[0], center)) < 0.0 {
co_scale(tangent, -1.0)
} else {
tangent
}
} else {
d0
};
let value = value.unwrap_or(0.0).clamp(-360.0, 360.0);
Some(FeatureDimAnnotation::angular(
"angle", center, axis, ref_dir, value, "A",
))
}
const PARALLEL_SIN: f64 = 1e-6;
fn angle_vertex(anchors: &[[f64; 3]], d0: [f64; 3], d1: [f64; 3], geoms: &[String]) -> [f64; 3] {
let is_plane = |i: usize| geoms.get(i).map(String::as_str) == Some("plane");
let (a0, a1) = (anchors[0], anchors[1]);
match (is_plane(0), is_plane(1)) {
(true, true) => plane_hinge_foot(a0, d0, a1, d1).unwrap_or_else(|| co_mid(a0, a1)),
(true, false) => {
line_plane_pierce(a1, d1, a0, d0).unwrap_or_else(|| co_mid(a0, a1))
}
(false, true) => {
line_plane_pierce(a0, d0, a1, d1).unwrap_or_else(|| co_mid(a0, a1))
}
(false, false) => carrier_closest_midpoint(a0, d0, a1, d1),
}
}
fn plane_hinge_foot(
p0: [f64; 3],
n0: [f64; 3],
p1: [f64; 3],
n1: [f64; 3],
) -> Option<[f64; 3]> {
let (l0, l1) = (co_norm(n0), co_norm(n1));
if l0 < 1e-9 || l1 < 1e-9 {
return None;
}
let n0 = co_scale(n0, 1.0 / l0);
let n1 = co_scale(n1, 1.0 / l1);
let c = co_dot(n0, n1);
let det = 1.0 - c * c; if det < PARALLEL_SIN * PARALLEL_SIN {
return None;
}
let m = co_mid(p0, p1);
let r0 = co_dot(n0, co_sub(p0, m));
let r1 = co_dot(n1, co_sub(p1, m));
let alpha = (r0 - c * r1) / det;
let beta = (r1 - c * r0) / det;
Some(co_add(m, co_add(co_scale(n0, alpha), co_scale(n1, beta))))
}
fn line_plane_pierce(l: [f64; 3], d: [f64; 3], p: [f64; 3], n: [f64; 3]) -> Option<[f64; 3]> {
let (ld, ln) = (co_norm(d), co_norm(n));
if ld < 1e-9 || ln < 1e-9 {
return None;
}
let d = co_scale(d, 1.0 / ld);
let n = co_scale(n, 1.0 / ln);
let denom = co_dot(d, n);
if denom.abs() < PARALLEL_SIN {
return None;
}
let t = co_dot(n, co_sub(p, l)) / denom;
Some(co_add(l, co_scale(d, t)))
}
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_add(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
[a[0] + b[0], a[1] + b[1], a[2] + b[2]]
}
fn co_scale(v: [f64; 3], k: f64) -> [f64; 3] {
[v[0] * k, v[1] * k, v[2] * k]
}
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],
]
}