use super::ConstraintEntry;
use crate::feature_pipeline::component::ComponentRecord;
use crate::feature_pipeline::{Env, SceneMap};
use crate::{
resolve_edge_selection, resolve_face_selection, resolve_named_selection,
resolve_vertex_selection, AffineTransform, MateAlign, MateKind, SelectionGeometry, Vec3,
};
#[derive(Debug, Clone)]
pub(super) struct ConstraintFailure {
pub status: &'static str,
pub message: String,
}
impl ConstraintFailure {
pub fn new(status: &'static str, message: impl Into<String>) -> Self {
Self {
status,
message: message.into(),
}
}
pub(super) fn unsupported(message: impl Into<String>) -> Self {
Self::new("unsupported-selection", message)
}
pub(super) fn invalid(message: impl Into<String>) -> Self {
Self::new("invalid-selection", message)
}
}
#[derive(Debug, Clone)]
pub(super) struct ResolvedElement {
pub name: String,
pub component: String,
pub world: SelectionGeometry,
pub local: SelectionGeometry,
pub transform: AffineTransform,
pub curve: Option<crate::MateCurve>,
pub freeform: Option<ConstraintFailure>,
}
pub(super) fn resolve_element(
scene: &SceneMap,
name: &str,
) -> Result<ResolvedElement, ConstraintFailure> {
let record = scene.owning_component(name).ok_or_else(|| {
ConstraintFailure::invalid(format!(
"selection '{name}' does not belong to an assembly component — only component geometry participates in constraints"
))
})?;
if let Some(follow) = &record.pattern {
return Err(ConstraintFailure::invalid(format!(
"'{}' is instance {} of pattern {}: it follows its seed {} and takes no mates of its own — mate {} instead",
record.id, follow.key, follow.pattern_id, follow.seed, follow.seed
)));
}
let curve = edge_curve(scene, name, &record.transform)?;
let (world, freeform) = match resolve_world_geometry(scene, name, record) {
Ok(world) => (world, None),
Err(failure) if failure.status == "unsupported-selection" && curve.is_some() => {
let chord = curve.as_ref().and_then(|c| {
let mid = c.curve.evaluate(0.5 * (c.t0 + c.t1)).ok()?;
Some(record.transform.point(mid))
});
match chord {
Some(position) => (SelectionGeometry::Point { position }, Some(failure)),
None => return Err(failure),
}
}
Err(failure) => return Err(failure),
};
let inverse = record.transform.rigid_inverse().map_err(|error| {
ConstraintFailure::new(
"error",
format!("component '{}': non-rigid pose: {error}", record.id),
)
})?;
let local = world.transformed(&inverse).map_err(|error| {
ConstraintFailure::new("error", format!("selection '{name}': {error}"))
})?;
Ok(ResolvedElement {
curve,
freeform,
name: name.to_string(),
component: record.id.clone(),
world,
local,
transform: record.transform,
})
}
fn edge_curve(
scene: &SceneMap,
name: &str,
transform: &AffineTransform,
) -> Result<Option<crate::MateCurve>, ConstraintFailure> {
let Some(edge) = scene.resolve_edge(name) else { return Ok(None) };
let inverse = transform.rigid_inverse().map_err(ConstraintFailure::invalid)?;
crate::with_registered_solid_str(edge.handle, |solid| {
let Some(record) = solid.edges.iter().find(|record| record.id == edge.edge_id) else {
return Ok(None);
};
let curve = crate::transform_topology::transform_curve(&record.curve, inverse)?;
Ok(Some(crate::MateCurve { curve, t0: record.t0, t1: record.t1 }))
})
.map_err(ConstraintFailure::invalid)
}
fn resolve_world_geometry(
scene: &SceneMap,
name: &str,
record: &ComponentRecord,
) -> Result<SelectionGeometry, ConstraintFailure> {
if let Some((solid_name, coords)) = name.split_once('@') {
let handle = scene.resolve_solid(solid_name).ok_or_else(|| {
ConstraintFailure::invalid(format!("vertex ref '{name}': unknown solid '{solid_name}'"))
})?;
let local = parse_triple(coords).ok_or_else(|| {
ConstraintFailure::invalid(format!(
"vertex ref '{name}': position must be 'x,y,z' numbers"
))
})?;
let world_query = record.transform.point(local);
return crate::with_registered_solid_str(handle, |solid| {
Ok(resolve_vertex_selection(solid, world_query))
})
.map_err(ConstraintFailure::invalid)?
.map_err(|error| ConstraintFailure::new(error.status(), error.to_string()));
}
let (_, local_name) = crate::split_component_namespace(name);
if crate::is_component_reference(local_name) {
let prefix = format!("{name}:");
let members: Vec<(String, u32)> = scene
.component_solids(&record.id)
.into_iter()
.filter(|(member, _)| record.id == name || member.starts_with(&prefix))
.collect();
if members.is_empty() {
return Err(ConstraintFailure::invalid(format!(
"component ref '{name}' has no member solids"
)));
}
return component_point(&members);
}
if let Some(face) = scene.resolve_face(name) {
return crate::with_registered_solid_str(face.handle, |solid| {
Ok(resolve_face_selection(solid, face.face_id))
})
.map_err(ConstraintFailure::invalid)?
.map_err(|error| ConstraintFailure::new(error.status(), error.to_string()));
}
if let Some(edge) = scene.resolve_edge(name) {
return crate::with_registered_solid_str(edge.handle, |solid| {
Ok(resolve_edge_selection(solid, edge.edge_id))
})
.map_err(ConstraintFailure::invalid)?
.map_err(|error| ConstraintFailure::new(error.status(), error.to_string()));
}
if let Some(handle) = scene.resolve_solid(name) {
return component_point(&[(name.to_string(), handle)]);
}
for (_member, handle) in scene.component_solids(&record.id) {
let found = crate::with_registered_solid_str(handle, |solid| {
Ok(resolve_named_selection(solid, name).ok())
})
.map_err(ConstraintFailure::invalid)?;
if let Some(geometry) = found {
return Ok(geometry);
}
}
Err(ConstraintFailure::invalid(format!(
"selection '{name}' not found in the scene"
)))
}
fn component_point(members: &[(String, u32)]) -> Result<SelectionGeometry, ConstraintFailure> {
let mut low = Vec3::new(f64::INFINITY, f64::INFINITY, f64::INFINITY);
let mut high = Vec3::new(f64::NEG_INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY);
let mut any = false;
for (member, handle) in members {
let (lo, hi, non_empty) = crate::with_registered_solid_str(*handle, |solid| {
let mut lo = Vec3::new(f64::INFINITY, f64::INFINITY, f64::INFINITY);
let mut hi = Vec3::new(f64::NEG_INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY);
let mut non_empty = false;
let mut include = |point: Vec3| {
lo = Vec3::new(lo.x.min(point.x), lo.y.min(point.y), lo.z.min(point.z));
hi = Vec3::new(hi.x.max(point.x), hi.y.max(point.y), hi.z.max(point.z));
non_empty = true;
};
for vertex in &solid.vertices {
include(vertex.point);
}
for shell in &solid.shells {
for face in &shell.faces {
for row in &face.surface.control_points {
for control in row {
include(control.point()?);
}
}
}
}
Ok((lo, hi, non_empty))
})
.map_err(|error| {
ConstraintFailure::invalid(format!("component member '{member}': {error}"))
})?;
if non_empty {
low = Vec3::new(low.x.min(lo.x), low.y.min(lo.y), low.z.min(lo.z));
high = Vec3::new(high.x.max(hi.x), high.y.max(hi.y), high.z.max(hi.z));
any = true;
}
}
if !any {
return Err(ConstraintFailure::invalid(
"component has no geometry to anchor",
));
}
Ok(SelectionGeometry::Point {
position: low.add(high).scale(0.5),
})
}
fn parse_triple(coords: &str) -> Option<Vec3> {
let mut parts = coords.split(',').map(str::trim);
let x = parts.next()?.parse::<f64>().ok()?;
let y = parts.next()?.parse::<f64>().ok()?;
let z = parts.next()?.parse::<f64>().ok()?;
if parts.next().is_some() {
return None;
}
Some(Vec3::new(x, y, z))
}
#[derive(Debug, Clone)]
pub(super) struct MappedMate {
pub body_a: String,
pub body_b: String,
pub body_c: Option<String>,
pub kind: MateKind,
}
#[derive(Debug, Clone, Default)]
pub(super) struct MappedConstraint {
pub mates: Vec<MappedMate>,
pub pending_params: Vec<(String, serde_json::Value)>,
pub pending_persistent: Vec<(String, serde_json::Value)>,
pub measured: Option<(f64, &'static str)>,
pub target: Option<f64>,
pub angle_axis: Option<Vec3>,
pub groups: Vec<Vec<usize>>,
pub note: Option<String>,
}
pub(super) fn map_constraint(
entry: &mut ConstraintEntry,
resolved: &[ResolvedElement],
env: &Env,
) -> Result<MappedConstraint, ConstraintFailure> {
let pair = || -> Result<(&ResolvedElement, &ResolvedElement), ConstraintFailure> {
match resolved {
[a, b] => Ok((a, b)),
_ => Err(ConstraintFailure::invalid(format!(
"{} takes exactly two elements ({} given)",
entry.constraint_type,
resolved.len()
))),
}
};
if !matches!(entry.constraint_type.as_str(), "path" | "cam") {
if let Some(refusal) = resolved.iter().find_map(|element| element.freeform.clone()) {
return Err(refusal);
}
}
match entry.constraint_type.as_str() {
"coincident" => {
let (a, b) = pair()?;
super::constraints::coincident::map_entry(entry, a, b)
}
"touch_align" => {
let (a, b) = pair()?;
super::constraints::touch_align::map(entry, a, b)
}
"parallel" => {
let (a, b) = pair()?;
super::constraints::parallel::map(entry, a, b)
}
"distance" => {
let (a, b) = pair()?;
let mut mapped = super::constraints::distance::map(entry, a, b, env)?;
super::constraints::apply_limit(entry, &mut mapped, env, |value| value)?;
Ok(mapped)
}
"angle" => {
let (a, b) = pair()?;
let mut mapped = super::constraints::angle::map(entry, a, b, env)?;
let exterior = entry.flag("exteriorAngle");
super::constraints::apply_limit(entry, &mut mapped, env, |value| {
if exterior { 180.0 - value } else { value }
})?;
Ok(mapped)
}
"concentric" => {
let (a, b) = pair()?;
super::constraints::concentric::map(entry, a, b)
}
"perpendicular" => pair().and_then(|(a, b)| super::constraints::perpendicular::map(a, b)),
"tangent" => pair().and_then(|(a, b)| super::constraints::tangent::map(a, b)),
"center" => super::constraints::center::map(entry, resolved),
"symmetric" => super::constraints::symmetric::map(resolved),
"gear" => {
let (a, b) = pair()?;
super::constraints::gear::map(entry, a, b, env)
}
"rack_pinion" => {
let (a, b) = pair()?;
super::constraints::rack_pinion::map(entry, a, b, env)
}
"screw" => {
let (a, b) = pair()?;
super::constraints::screw::map(entry, a, b, env)
}
"path" => {
let (a, b) = pair()?;
super::constraints::path::map(entry, a, b, env)
}
"slot" => super::constraints::slot::map(entry, resolved, env),
"cam" => {
let (a, b) = pair()?;
super::constraints::cam::map(entry, a, b, env)
}
other => Err(ConstraintFailure::new(
"error",
format!("Unknown constraint type: {other}"),
)),
}
}
pub(super) fn mate(a: &ResolvedElement, b: &ResolvedElement, kind: MateKind) -> MappedMate {
MappedMate {
body_a: a.component.clone(),
body_b: b.component.clone(),
body_c: None,
kind,
}
}
pub(super) fn direction_of(geometry: &SelectionGeometry) -> Option<Vec3> {
match *geometry {
SelectionGeometry::Plane { normal, .. } => Some(normal),
SelectionGeometry::Axis { direction, .. } | SelectionGeometry::Line { direction, .. } => {
Some(direction)
}
SelectionGeometry::Circle { axis, .. } => Some(axis),
SelectionGeometry::Sphere { .. } | SelectionGeometry::Point { .. } => None,
}
}
pub(super) fn local_point(element: &ResolvedElement) -> [f64; 3] {
let p = element.local.representative_point();
[p.x, p.y, p.z]
}
pub(super) fn require_direction(element: &ResolvedElement) -> Result<(Vec3, Vec3), ConstraintFailure> {
let world = direction_of(&element.world).ok_or_else(|| {
ConstraintFailure::unsupported(format!(
"selection '{}' carries no direction (needs a planar face, straight edge, axis face, or circular edge)",
element.name
))
})?;
let local = direction_of(&element.local).expect("local mirrors world kind");
Ok((world, local))
}
pub(super) fn require_axis(element: &ResolvedElement) -> Result<crate::MateAxis, ConstraintFailure> {
element.local.mate_axis().ok_or_else(|| {
ConstraintFailure::unsupported(format!(
"selection '{}' carries no axis (needs a cylindrical/conical face, circular edge, or straight edge)",
element.name
))
})
}
pub(super) fn require_plane(element: &ResolvedElement) -> Result<crate::MatePlane, ConstraintFailure> {
element.local.mate_plane().ok_or_else(|| {
ConstraintFailure::unsupported(format!(
"selection '{}' is not a planar face",
element.name
))
})
}
pub(super) fn pair_signature(elements: &[String]) -> String {
let mut pair: Vec<&str> = elements.iter().map(String::as_str).collect();
pair.sort_unstable();
pair.join("\n")
}
pub(super) fn effective_align(
entry: &mut ConstraintEntry,
dir_a: Vec3,
dir_b: Vec3,
legacy: &str,
) -> MateAlign {
let dot = dir_a.dot(dir_b);
entry.set_persistent("lastOrientationDot", serde_json::json!(dot));
match entry.input_params.get(FACING).and_then(|value| value.as_str()) {
Some(OPPOSED) => return MateAlign::AntiAligned,
Some(_) => return MateAlign::Aligned,
None => {}
}
let signature = pair_signature(&entry.elements());
let cached = entry
.persistent("preferredOpposeSignature")
.and_then(|value| value.as_str())
.map(|stored| stored == signature)
.unwrap_or(false)
.then(|| entry.persistent("preferredOppose").and_then(|v| v.as_bool()))
.flatten();
let opposed = cached.unwrap_or(dot < 0.0) != entry.flag(legacy);
if let Some(params) = entry.input_params.as_object_mut() {
params.remove(legacy);
params.insert(FACING.into(), serde_json::json!(if opposed { OPPOSED } else { ALIGNED }));
}
entry.remove_persistent("preferredOppose");
entry.remove_persistent("preferredOpposeSignature");
if opposed {
MateAlign::AntiAligned
} else {
MateAlign::Aligned
}
}
pub(super) const FACING: &str = "facing";
pub(super) const ALIGNED: &str = "aligned";
pub(super) const OPPOSED: &str = "opposed";
pub(super) fn facing_field(hint: &str) -> serde_json::Value {
serde_json::json!({
"type": "options",
"options": [ALIGNED, OPPOSED],
"default_value": null,
"label": "Facing",
"hint": hint,
})
}
#[allow(clippy::type_complexity)]
pub(super) fn first_solve_target(
entry: &ConstraintEntry,
param_key: &str,
flag_key: &str,
configured: f64,
current: f64,
) -> (
f64,
(Vec<(String, serde_json::Value)>, Vec<(String, serde_json::Value)>),
) {
let initialized = entry
.persistent(flag_key)
.and_then(|value| value.as_bool())
.unwrap_or(false);
let given = entry.input_params.get(param_key).is_some_and(|value| !value.is_null());
if initialized {
(configured, (Vec::new(), Vec::new()))
} else if given {
(configured, (Vec::new(), vec![(flag_key.to_string(), serde_json::Value::Bool(true))]))
} else {
(
current,
(
vec![(param_key.to_string(), serde_json::json!(current))],
vec![(flag_key.to_string(), serde_json::Value::Bool(true))],
),
)
}
}
pub(super) fn angle_between_deg(a: Vec3, b: Vec3) -> f64 {
let denominator = a.length() * b.length();
if denominator <= 0.0 {
return 0.0;
}
(a.dot(b) / denominator).clamp(-1.0, 1.0).acos().to_degrees()
}
pub(super) fn point_line_distance(point: Vec3, origin: Vec3, direction: Vec3) -> f64 {
let offset = point.sub(origin);
let along = offset.dot(direction) / direction.dot(direction).max(1e-300);
offset.sub(direction.scale(along)).length()
}
pub(super) fn line_line_distance(oa: Vec3, da: Vec3, ob: Vec3, db: Vec3) -> f64 {
let cross = da.cross(db);
let denominator = cross.length();
if denominator < 1e-9 * da.length().max(db.length()).max(1.0) {
return point_line_distance(ob, oa, da);
}
(ob.sub(oa).dot(cross) / denominator).abs()
}
pub(super) fn elements_scale(elements: &[ResolvedElement]) -> f64 {
let mut scale = 1.0f64;
for point in elements.iter().map(|element| element.world.representative_point()) {
scale = scale
.max(point.x.abs())
.max(point.y.abs())
.max(point.z.abs());
}
scale
}
pub(super) fn matrix_to_quaternion(transform: &AffineTransform) -> [f64; 4] {
let m = &transform.elements;
let (r00, r01, r02) = (m[0], m[1], m[2]);
let (r10, r11, r12) = (m[4], m[5], m[6]);
let (r20, r21, r22) = (m[8], m[9], m[10]);
let trace = r00 + r11 + r22;
let q = if trace > 0.0 {
let s = (trace + 1.0).sqrt() * 2.0;
[s / 4.0, (r21 - r12) / s, (r02 - r20) / s, (r10 - r01) / s]
} else if r00 > r11 && r00 > r22 {
let s = (1.0 + r00 - r11 - r22).sqrt() * 2.0;
[(r21 - r12) / s, s / 4.0, (r01 + r10) / s, (r02 + r20) / s]
} else if r11 > r22 {
let s = (1.0 + r11 - r00 - r22).sqrt() * 2.0;
[(r02 - r20) / s, (r01 + r10) / s, s / 4.0, (r12 + r21) / s]
} else {
let s = (1.0 + r22 - r00 - r11).sqrt() * 2.0;
[(r10 - r01) / s, (r02 + r20) / s, (r12 + r21) / s, s / 4.0]
};
normalize_quaternion(q)
}
pub(super) fn normalize_quaternion(q: [f64; 4]) -> [f64; 4] {
let norm = (q[0] * q[0] + q[1] * q[1] + q[2] * q[2] + q[3] * q[3]).sqrt();
if norm <= 0.0 || !norm.is_finite() {
return [1.0, 0.0, 0.0, 0.0];
}
[q[0] / norm, q[1] / norm, q[2] / norm, q[3] / norm]
}
pub(super) fn pose_to_transform(
rotation: [f64; 4],
translation: [f64; 3],
) -> Result<AffineTransform, String> {
let [w, x, y, z] = normalize_quaternion(rotation);
AffineTransform::new([
1.0 - 2.0 * (y * y + z * z), 2.0 * (x * y - w * z), 2.0 * (x * z + w * y), translation[0],
2.0 * (x * y + w * z), 1.0 - 2.0 * (x * x + z * z), 2.0 * (y * z - w * x), translation[1],
2.0 * (x * z - w * y), 2.0 * (y * z + w * x), 1.0 - 2.0 * (x * x + y * y), translation[2],
0.0, 0.0, 0.0, 1.0,
])
}
pub(super) fn pose_params_to_transform(pose: &serde_json::Value) -> Result<AffineTransform, String> {
let triple = |key: &str| -> Result<[f64; 3], String> {
let values = pose.get(key).and_then(|value| value.as_array());
let Some(values) = values else { return Ok([0.0; 3]) };
let mut out = [0.0; 3];
for (slot, value) in out.iter_mut().zip(values) {
*slot = value.as_f64().ok_or_else(|| format!("pose {key}: not a number"))?;
}
Ok(out)
};
let translate = triple("translate")?;
let degrees = triple("rotateEulerDeg")?;
AffineTransform::new(crate::feature_pipeline::features::common::compose_trs_matrix(
translate,
[degrees[0].to_radians(), degrees[1].to_radians(), degrees[2].to_radians()],
[1.0, 1.0, 1.0],
[0.0, 0.0, 0.0],
))
}
pub fn transform_to_pose_params(transform: &AffineTransform) -> serde_json::Value {
let m = &transform.elements;
let sb = m[2].clamp(-1.0, 1.0);
let (a, b, c) = if sb.abs() < 1.0 - 1e-9 {
(
(-m[6]).atan2(m[10]),
sb.asin(),
(-m[1]).atan2(m[0]),
)
} else {
(m[9].atan2(m[5]), sb.asin(), 0.0)
};
serde_json::json!({
"translate": [m[3], m[7], m[11]],
"rotateEulerDeg": [a.to_degrees(), b.to_degrees(), c.to_degrees()],
})
}