use super::{boolean_field, decimals_of, dimension_fields, id_field, options_field, params, reference_field, schema_entry, PmiTypeDef};
use crate::feature_pipeline::pmi::resolve::{a3, class_of, direction_of, resolve_reference};
use crate::feature_pipeline::pmi::{format_dimension, PmiAnnotation, PmiContext, PmiGeometry, Resolved, ToleranceBlock};
use crate::feature_pipeline::SelectionProbe;
use crate::{SelectionGeometry, Vec3};
pub const DEF: PmiTypeDef = PmiTypeDef {
type_id: "angle",
short_name: "ANG",
icon: "\u{2220}",
long_name: "\u{2220} Angle dimension",
label: "Angle dimension",
applicable,
schema,
resolve,
};
fn applicable(probe: &SelectionProbe) -> bool {
probe.faces + probe.edges + probe.planes == 2
&& probe.vertices == 0
&& probe.solids == 0
&& probe.sketches == 0
}
fn schema() -> serde_json::Value {
let mut fields = params(vec![
("id", id_field()),
(
"targets",
reference_field(
"Targets",
&["FACE", "EDGE", "PLANE"],
true,
2,
2,
"Two planar faces, planes or straight edges",
),
),
(
"angleType",
options_field(
"Angle",
&["acute", "obtuse", "reflex"],
"acute",
"Which of the four angles between the elements to dimension",
),
),
(
"reverseElementOrder",
boolean_field("Reverse order", false, "Sweep the arc from the second element to the first"),
),
]);
dimension_fields(&mut fields, 1);
schema_entry(&DEF, fields)
}
fn arc_direction(geometry: &SelectionGeometry, axis: Vec3) -> Vec3 {
match geometry {
SelectionGeometry::Plane { normal, .. } => normal.cross(axis),
other => direction_of(other).unwrap_or(Vec3::new(1.0, 0.0, 0.0)),
}
}
fn vertex_of(first: &SelectionGeometry, second: &SelectionGeometry, axis: Vec3) -> Vec3 {
let origin = |g: &SelectionGeometry| g.representative_point();
match (first, second) {
(
SelectionGeometry::Plane {
origin: oa,
normal: na,
},
SelectionGeometry::Plane {
origin: ob,
normal: nb,
},
) => {
let mid = oa.add(*ob).scale(0.5);
let na = *na;
let nb = *nb;
let da = oa.sub(mid).dot(na);
let db = ob.sub(mid).dot(nb);
let nn = na.dot(nb);
let det = 1.0 - nn * nn;
if det.abs() < 1e-12 {
return mid;
}
let ka = (da - db * nn) / det;
let kb = (db - da * nn) / det;
let point = mid.add(na.scale(ka)).add(nb.scale(kb));
let t = mid.sub(point).dot(axis);
point.add(axis.scale(t))
}
(SelectionGeometry::Plane { origin: po, normal }, line) | (line, SelectionGeometry::Plane { origin: po, normal }) => {
let lo = origin(line);
let ld = direction_of(line).unwrap_or(Vec3::new(1.0, 0.0, 0.0));
let denom = ld.dot(*normal);
if denom.abs() > 1e-9 {
let t = po.sub(lo).dot(*normal) / denom;
lo.add(ld.scale(t))
} else {
lo
}
}
(a, b) => {
let (oa, da) = (origin(a), direction_of(a).unwrap_or(Vec3::new(1.0, 0.0, 0.0)));
let (ob, db) = (origin(b), direction_of(b).unwrap_or(Vec3::new(0.0, 1.0, 0.0)));
let w = oa.sub(ob);
let aa = da.dot(da);
let bb = da.dot(db);
let cc = db.dot(db);
let dd = da.dot(w);
let ee = db.dot(w);
let denom = aa * cc - bb * bb;
if denom.abs() < 1e-12 {
return oa.add(ob).scale(0.5);
}
let s = (bb * ee - cc * dd) / denom;
let t = (aa * ee - bb * dd) / denom;
oa.add(da.scale(s)).add(ob.add(db.scale(t))).scale(0.5)
}
}
}
fn resolve(annotation: &PmiAnnotation, context: &PmiContext<'_>) -> Result<Resolved, String> {
let targets = annotation.references("targets");
let [first, second] = targets.as_slice() else {
return Err("select two planar faces, planes or straight edges".into());
};
let mut ga = resolve_reference(context.scene, first)?;
let mut gb = resolve_reference(context.scene, second)?;
if annotation.flag("reverseElementOrder") {
std::mem::swap(&mut ga, &mut gb);
}
for (name, geometry) in [(first, &ga), (second, &gb)] {
if direction_of(geometry).is_none() || matches!(geometry, SelectionGeometry::Circle { .. } | SelectionGeometry::Axis { .. }) {
return Err(format!(
"'{name}' is a {} — an angle needs planar faces, planes or straight edges",
class_of(geometry)
));
}
}
let na = direction_of(&ga).expect("checked");
let nb = direction_of(&gb).expect("checked");
let axis = na.cross(nb);
if axis.length() < 1e-7 {
return Err("the two elements are parallel — there is no angle to dimension".into());
}
let axis = axis.normalized().map_err(|e| e.to_string())?;
let vertex = vertex_of(&ga, &gb, axis);
let u = arc_direction(&ga, axis).normalized().map_err(|e| e.to_string())?;
let v = arc_direction(&gb, axis).normalized().map_err(|e| e.to_string())?;
let mut sweep = u.cross(v).dot(axis).atan2(u.dot(v)).to_degrees();
if sweep < 0.0 {
sweep += 360.0;
}
let interior = if sweep > 180.0 { 360.0 - sweep } else { sweep };
let (dir_a, dir_b, degrees) = match annotation.text("angleType").to_ascii_lowercase().as_str() {
"obtuse" => {
if interior >= 90.0 {
(u, v, sweep.min(360.0 - sweep))
} else {
(u, v.scale(-1.0), 180.0 - interior)
}
}
"reflex" => {
let acute = if interior <= 90.0 { (u, v) } else { (u, v.scale(-1.0)) };
let inner = if interior <= 90.0 { interior } else { 180.0 - interior };
(acute.0, acute.1, 360.0 - (180.0 - inner))
}
_ => {
if interior <= 90.0 {
(u, v, interior)
} else {
(u, v.scale(-1.0), 180.0 - interior)
}
}
};
let signed = dir_a.cross(dir_b).dot(axis).atan2(dir_a.dot(dir_b)).to_degrees();
let axis = if signed < 0.0 { axis.scale(-1.0) } else { axis };
let decimals = decimals_of(annotation, context, 1.0);
let tolerance = ToleranceBlock::read(annotation, context.env)?;
let text = format_dimension(degrees, decimals, &tolerance, annotation.flag("isReference"), "", "\u{00B0}");
let half = (degrees * 0.5).to_radians();
let bisector = rotate(dir_a, axis, half);
let scale = ga.representative_point().sub(vertex).length().max(gb.representative_point().sub(vertex).length()).max(1.0);
let default_label = vertex.add(bisector.scale(scale * 0.6));
Ok(Resolved {
text,
value: Some(degrees),
unit: "deg",
references: targets,
geometry: PmiGeometry::Angular {
vertex: a3(vertex),
dir_a: a3(dir_a),
dir_b: a3(dir_b),
axis: a3(axis),
degrees,
},
default_label: a3(default_label),
})
}
pub fn rotate(v: Vec3, axis: Vec3, angle: f64) -> Vec3 {
let (s, c) = angle.sin_cos();
v.scale(c)
.add(axis.cross(v).scale(s))
.add(axis.scale(axis.dot(v) * (1.0 - c)))
}