1use super::ConstraintEntry;
19use crate::feature_pipeline::component::ComponentRecord;
20use crate::feature_pipeline::{Env, SceneMap};
21use crate::{
22 resolve_edge_selection, resolve_face_selection, resolve_named_selection,
23 resolve_vertex_selection, AffineTransform, MateAlign, MateKind, SelectionGeometry, Vec3,
24};
25
26#[derive(Debug, Clone)]
28pub(super) struct ConstraintFailure {
29 pub status: &'static str,
30 pub message: String,
31}
32
33impl ConstraintFailure {
34 pub fn new(status: &'static str, message: impl Into<String>) -> Self {
35 Self {
36 status,
37 message: message.into(),
38 }
39 }
40 pub(super) fn unsupported(message: impl Into<String>) -> Self {
41 Self::new("unsupported-selection", message)
42 }
43 pub(super) fn invalid(message: impl Into<String>) -> Self {
44 Self::new("invalid-selection", message)
45 }
46}
47
48#[derive(Debug, Clone)]
56pub(super) struct ResolvedElement {
57 pub name: String,
58 pub component: String,
59 pub world: SelectionGeometry,
60 pub local: SelectionGeometry,
61}
62
63pub(super) fn resolve_element(
67 scene: &SceneMap,
68 name: &str,
69) -> Result<ResolvedElement, ConstraintFailure> {
70 let record = scene.owning_component(name).ok_or_else(|| {
71 ConstraintFailure::invalid(format!(
72 "selection '{name}' does not belong to an assembly component — only component geometry participates in constraints"
73 ))
74 })?;
75 let world = resolve_world_geometry(scene, name, record)?;
76 let inverse = rigid_inverse(&record.transform).map_err(|error| {
77 ConstraintFailure::new(
78 "error",
79 format!("component '{}': non-rigid pose: {error}", record.id),
80 )
81 })?;
82 let local = world.transformed(&inverse).map_err(|error| {
83 ConstraintFailure::new("error", format!("selection '{name}': {error}"))
84 })?;
85 Ok(ResolvedElement {
86 name: name.to_string(),
87 component: record.id.clone(),
88 world,
89 local,
90 })
91}
92
93fn resolve_world_geometry(
94 scene: &SceneMap,
95 name: &str,
96 record: &ComponentRecord,
97) -> Result<SelectionGeometry, ConstraintFailure> {
98 if let Some((solid_name, coords)) = name.split_once('@') {
100 let handle = scene.resolve_solid(solid_name).ok_or_else(|| {
101 ConstraintFailure::invalid(format!("vertex ref '{name}': unknown solid '{solid_name}'"))
102 })?;
103 let local = parse_triple(coords).ok_or_else(|| {
104 ConstraintFailure::invalid(format!(
105 "vertex ref '{name}': position must be 'x,y,z' numbers"
106 ))
107 })?;
108 let world_query = record.transform.point(local);
109 return crate::with_registered_solid_str(handle, |solid| {
110 Ok(resolve_vertex_selection(solid, world_query))
111 })
112 .map_err(ConstraintFailure::invalid)?
113 .map_err(|error| ConstraintFailure::new(error.status(), error.to_string()));
114 }
115
116 let (_, local_name) = crate::split_component_namespace(name);
120 if crate::is_component_reference(local_name) {
121 let prefix = format!("{name}:");
122 let members: Vec<(String, u32)> = scene
123 .component_solids(&record.id)
124 .into_iter()
125 .filter(|(member, _)| record.id == name || member.starts_with(&prefix))
126 .collect();
127 if members.is_empty() {
128 return Err(ConstraintFailure::invalid(format!(
129 "component ref '{name}' has no member solids"
130 )));
131 }
132 return component_point(&members);
133 }
134
135 if let Some(face) = scene.resolve_face(name) {
138 return crate::with_registered_solid_str(face.handle, |solid| {
139 Ok(resolve_face_selection(solid, face.face_id))
140 })
141 .map_err(ConstraintFailure::invalid)?
142 .map_err(|error| ConstraintFailure::new(error.status(), error.to_string()));
143 }
144 if let Some(edge) = scene.resolve_edge(name) {
145 return crate::with_registered_solid_str(edge.handle, |solid| {
146 Ok(resolve_edge_selection(solid, edge.edge_id))
147 })
148 .map_err(ConstraintFailure::invalid)?
149 .map_err(|error| ConstraintFailure::new(error.status(), error.to_string()));
150 }
151 if let Some(handle) = scene.resolve_solid(name) {
154 return component_point(&[(name.to_string(), handle)]);
155 }
156 for (_member, handle) in scene.component_solids(&record.id) {
159 let found = crate::with_registered_solid_str(handle, |solid| {
160 Ok(resolve_named_selection(solid, name).ok())
161 })
162 .map_err(ConstraintFailure::invalid)?;
163 if let Some(geometry) = found {
164 return Ok(geometry);
165 }
166 }
167 Err(ConstraintFailure::invalid(format!(
168 "selection '{name}' not found in the scene"
169 )))
170}
171
172fn component_point(members: &[(String, u32)]) -> Result<SelectionGeometry, ConstraintFailure> {
177 let mut low = Vec3::new(f64::INFINITY, f64::INFINITY, f64::INFINITY);
178 let mut high = Vec3::new(f64::NEG_INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY);
179 let mut any = false;
180 for (member, handle) in members {
181 let (lo, hi, non_empty) = crate::with_registered_solid_str(*handle, |solid| {
182 let mut lo = Vec3::new(f64::INFINITY, f64::INFINITY, f64::INFINITY);
183 let mut hi = Vec3::new(f64::NEG_INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY);
184 let mut non_empty = false;
185 let mut include = |point: Vec3| {
186 lo = Vec3::new(lo.x.min(point.x), lo.y.min(point.y), lo.z.min(point.z));
187 hi = Vec3::new(hi.x.max(point.x), hi.y.max(point.y), hi.z.max(point.z));
188 non_empty = true;
189 };
190 for vertex in &solid.vertices {
191 include(vertex.point);
192 }
193 for shell in &solid.shells {
194 for face in &shell.faces {
195 for row in &face.surface.control_points {
196 for control in row {
197 include(control.point()?);
198 }
199 }
200 }
201 }
202 Ok((lo, hi, non_empty))
203 })
204 .map_err(|error| {
205 ConstraintFailure::invalid(format!("component member '{member}': {error}"))
206 })?;
207 if non_empty {
208 low = Vec3::new(low.x.min(lo.x), low.y.min(lo.y), low.z.min(lo.z));
209 high = Vec3::new(high.x.max(hi.x), high.y.max(hi.y), high.z.max(hi.z));
210 any = true;
211 }
212 }
213 if !any {
214 return Err(ConstraintFailure::invalid(
215 "component has no geometry to anchor",
216 ));
217 }
218 Ok(SelectionGeometry::Point {
219 position: low.add(high).scale(0.5),
220 })
221}
222
223fn parse_triple(coords: &str) -> Option<Vec3> {
224 let mut parts = coords.split(',').map(str::trim);
225 let x = parts.next()?.parse::<f64>().ok()?;
226 let y = parts.next()?.parse::<f64>().ok()?;
227 let z = parts.next()?.parse::<f64>().ok()?;
228 if parts.next().is_some() {
229 return None;
230 }
231 Some(Vec3::new(x, y, z))
232}
233
234#[derive(Debug, Clone)]
240pub(super) struct MappedMate {
241 pub body_a: String,
242 pub body_b: String,
243 pub kind: MateKind,
244}
245
246#[derive(Debug, Clone, Default)]
250pub(super) struct MappedConstraint {
251 pub mates: Vec<MappedMate>,
252 pub pending_params: Vec<(String, serde_json::Value)>,
253 pub pending_persistent: Vec<(String, serde_json::Value)>,
254 pub measured: Option<(f64, &'static str)>,
257 pub target: Option<f64>,
259 pub groups: Vec<Vec<usize>>,
263 pub note: Option<String>,
267}
268
269pub(super) fn map_constraint(
276 entry: &mut ConstraintEntry,
277 resolved: &[ResolvedElement],
278 env: &Env,
279) -> Result<MappedConstraint, ConstraintFailure> {
280 let pair = || -> Result<(&ResolvedElement, &ResolvedElement), ConstraintFailure> {
282 match resolved {
283 [a, b] => Ok((a, b)),
284 _ => Err(ConstraintFailure::invalid(format!(
285 "{} takes exactly two elements ({} given)",
286 entry.constraint_type,
287 resolved.len()
288 ))),
289 }
290 };
291 match entry.constraint_type.as_str() {
292 "coincident" => pair().and_then(|(a, b)| super::constraints::coincident::map(a, b)),
293 "touch_align" => {
294 let (a, b) = pair()?;
295 super::constraints::touch_align::map(entry, a, b)
296 }
297 "parallel" => {
298 let (a, b) = pair()?;
299 super::constraints::parallel::map(entry, a, b)
300 }
301 "distance" => {
302 let (a, b) = pair()?;
303 super::constraints::distance::map(entry, a, b, env)
304 }
305 "angle" => {
306 let (a, b) = pair()?;
307 super::constraints::angle::map(entry, a, b, env)
308 }
309 "concentric" => {
310 let (a, b) = pair()?;
311 super::constraints::concentric::map(entry, a, b)
312 }
313 "perpendicular" => pair().and_then(|(a, b)| super::constraints::perpendicular::map(a, b)),
314 "tangent" => pair().and_then(|(a, b)| super::constraints::tangent::map(a, b)),
315 "center" => super::constraints::center::map(entry, resolved),
316 other => Err(ConstraintFailure::new(
317 "error",
318 format!("Unknown constraint type: {other}"),
319 )),
320 }
321}
322
323pub(super) fn mate(a: &ResolvedElement, b: &ResolvedElement, kind: MateKind) -> MappedMate {
324 MappedMate {
325 body_a: a.component.clone(),
326 body_b: b.component.clone(),
327 kind,
328 }
329}
330
331pub(super) fn direction_of(geometry: &SelectionGeometry) -> Option<Vec3> {
334 match *geometry {
335 SelectionGeometry::Plane { normal, .. } => Some(normal),
336 SelectionGeometry::Axis { direction, .. } | SelectionGeometry::Line { direction, .. } => {
337 Some(direction)
338 }
339 SelectionGeometry::Circle { axis, .. } => Some(axis),
340 SelectionGeometry::Sphere { .. } | SelectionGeometry::Point { .. } => None,
341 }
342}
343
344pub(super) fn local_point(element: &ResolvedElement) -> [f64; 3] {
345 let p = element.local.representative_point();
346 [p.x, p.y, p.z]
347}
348
349pub(super) fn require_direction(element: &ResolvedElement) -> Result<(Vec3, Vec3), ConstraintFailure> {
350 let world = direction_of(&element.world).ok_or_else(|| {
351 ConstraintFailure::unsupported(format!(
352 "selection '{}' carries no direction (needs a planar face, straight edge, axis face, or circular edge)",
353 element.name
354 ))
355 })?;
356 let local = direction_of(&element.local).expect("local mirrors world kind");
357 Ok((world, local))
358}
359
360pub(super) fn require_axis(element: &ResolvedElement) -> Result<crate::MateAxis, ConstraintFailure> {
361 element.local.mate_axis().ok_or_else(|| {
362 ConstraintFailure::unsupported(format!(
363 "selection '{}' carries no axis (needs a cylindrical/conical face, circular edge, or straight edge)",
364 element.name
365 ))
366 })
367}
368
369pub(super) fn require_plane(element: &ResolvedElement) -> Result<crate::MatePlane, ConstraintFailure> {
370 element.local.mate_plane().ok_or_else(|| {
371 ConstraintFailure::unsupported(format!(
372 "selection '{}' is not a planar face",
373 element.name
374 ))
375 })
376}
377
378pub(super) fn pair_signature(elements: &[String]) -> String {
381 let mut pair: Vec<&str> = elements.iter().map(String::as_str).collect();
382 pair.sort_unstable();
383 pair.join("\n")
384}
385
386pub(super) fn effective_align(
391 entry: &mut ConstraintEntry,
392 dir_a: Vec3,
393 dir_b: Vec3,
394 reverse: bool,
395) -> MateAlign {
396 let signature = pair_signature(&entry.elements());
397 let dot = dir_a.dot(dir_b);
398 let cached = entry
399 .persistent("preferredOpposeSignature")
400 .and_then(|value| value.as_str())
401 .map(|stored| stored == signature)
402 .unwrap_or(false)
403 .then(|| entry.persistent("preferredOppose").and_then(|v| v.as_bool()))
404 .flatten();
405 let oppose = cached.unwrap_or_else(|| {
406 let oppose = dot < 0.0;
407 entry.set_persistent("preferredOppose", serde_json::Value::Bool(oppose));
408 entry.set_persistent(
409 "preferredOpposeSignature",
410 serde_json::Value::String(signature),
411 );
412 oppose
413 });
414 entry.set_persistent(
415 "lastOrientationDot",
416 serde_json::json!(dot),
417 );
418 if oppose != reverse {
419 MateAlign::AntiAligned
420 } else {
421 MateAlign::Aligned
422 }
423}
424
425
426
427
428
429#[allow(clippy::type_complexity)]
433pub(super) fn first_solve_target(
434 entry: &ConstraintEntry,
435 param_key: &str,
436 flag_key: &str,
437 configured: f64,
438 current: f64,
439) -> (
440 f64,
441 (Vec<(String, serde_json::Value)>, Vec<(String, serde_json::Value)>),
442) {
443 let initialized = entry
444 .persistent(flag_key)
445 .and_then(|value| value.as_bool())
446 .unwrap_or(false);
447 if initialized {
448 (configured, (Vec::new(), Vec::new()))
449 } else {
450 (
451 current,
452 (
453 vec![(param_key.to_string(), serde_json::json!(current))],
454 vec![(flag_key.to_string(), serde_json::Value::Bool(true))],
455 ),
456 )
457 }
458}
459
460
461
462
463
464pub(super) fn angle_between_deg(a: Vec3, b: Vec3) -> f64 {
469 let denominator = a.length() * b.length();
470 if denominator <= 0.0 {
471 return 0.0;
472 }
473 (a.dot(b) / denominator).clamp(-1.0, 1.0).acos().to_degrees()
474}
475
476pub(super) fn point_line_distance(point: Vec3, origin: Vec3, direction: Vec3) -> f64 {
477 let offset = point.sub(origin);
478 let along = offset.dot(direction) / direction.dot(direction).max(1e-300);
479 offset.sub(direction.scale(along)).length()
480}
481
482pub(super) fn line_line_distance(oa: Vec3, da: Vec3, ob: Vec3, db: Vec3) -> f64 {
485 let cross = da.cross(db);
486 let denominator = cross.length();
487 if denominator < 1e-9 * da.length().max(db.length()).max(1.0) {
488 return point_line_distance(ob, oa, da);
489 }
490 (ob.sub(oa).dot(cross) / denominator).abs()
491}
492
493pub(super) fn elements_scale(elements: &[ResolvedElement]) -> f64 {
496 let mut scale = 1.0f64;
497 for point in elements.iter().map(|element| element.world.representative_point()) {
498 scale = scale
499 .max(point.x.abs())
500 .max(point.y.abs())
501 .max(point.z.abs());
502 }
503 scale
504}
505
506pub(super) fn rigid_inverse(transform: &AffineTransform) -> Result<AffineTransform, String> {
512 let m = &transform.elements;
513 let r = [[m[0], m[1], m[2]], [m[4], m[5], m[6]], [m[8], m[9], m[10]]];
514 let t = [m[3], m[7], m[11]];
515 let mut out = [0.0f64; 16];
516 for row in 0..3 {
517 for col in 0..3 {
518 out[row * 4 + col] = r[col][row];
519 }
520 out[row * 4 + 3] = -(0..3).map(|k| r[k][row] * t[k]).sum::<f64>();
521 }
522 out[15] = 1.0;
523 AffineTransform::new(out)
524}
525
526pub(super) fn matrix_to_quaternion(transform: &AffineTransform) -> [f64; 4] {
529 let m = &transform.elements;
530 let (r00, r01, r02) = (m[0], m[1], m[2]);
531 let (r10, r11, r12) = (m[4], m[5], m[6]);
532 let (r20, r21, r22) = (m[8], m[9], m[10]);
533 let trace = r00 + r11 + r22;
534 let q = if trace > 0.0 {
535 let s = (trace + 1.0).sqrt() * 2.0;
536 [s / 4.0, (r21 - r12) / s, (r02 - r20) / s, (r10 - r01) / s]
537 } else if r00 > r11 && r00 > r22 {
538 let s = (1.0 + r00 - r11 - r22).sqrt() * 2.0;
539 [(r21 - r12) / s, s / 4.0, (r01 + r10) / s, (r02 + r20) / s]
540 } else if r11 > r22 {
541 let s = (1.0 + r11 - r00 - r22).sqrt() * 2.0;
542 [(r02 - r20) / s, (r01 + r10) / s, s / 4.0, (r12 + r21) / s]
543 } else {
544 let s = (1.0 + r22 - r00 - r11).sqrt() * 2.0;
545 [(r10 - r01) / s, (r02 + r20) / s, (r12 + r21) / s, s / 4.0]
546 };
547 normalize_quaternion(q)
548}
549
550pub(super) fn normalize_quaternion(q: [f64; 4]) -> [f64; 4] {
551 let norm = (q[0] * q[0] + q[1] * q[1] + q[2] * q[2] + q[3] * q[3]).sqrt();
552 if norm <= 0.0 || !norm.is_finite() {
553 return [1.0, 0.0, 0.0, 0.0];
554 }
555 [q[0] / norm, q[1] / norm, q[2] / norm, q[3] / norm]
556}
557
558pub(super) fn pose_to_transform(
562 rotation: [f64; 4],
563 translation: [f64; 3],
564) -> Result<AffineTransform, String> {
565 let [w, x, y, z] = normalize_quaternion(rotation);
566 AffineTransform::new([
567 1.0 - 2.0 * (y * y + z * z), 2.0 * (x * y - w * z), 2.0 * (x * z + w * y), translation[0],
568 2.0 * (x * y + w * z), 1.0 - 2.0 * (x * x + z * z), 2.0 * (y * z - w * x), translation[1],
569 2.0 * (x * z - w * y), 2.0 * (y * z + w * x), 1.0 - 2.0 * (x * x + y * y), translation[2],
570 0.0, 0.0, 0.0, 1.0,
571 ])
572}
573
574pub fn transform_to_pose_params(transform: &AffineTransform) -> serde_json::Value {
588 let m = &transform.elements;
589 let sb = m[2].clamp(-1.0, 1.0);
592 let (a, b, c) = if sb.abs() < 1.0 - 1e-9 {
593 (
594 (-m[6]).atan2(m[10]),
595 sb.asin(),
596 (-m[1]).atan2(m[0]),
597 )
598 } else {
599 (m[9].atan2(m[5]), sb.asin(), 0.0)
600 };
601 serde_json::json!({
602 "translate": [m[3], m[7], m[11]],
603 "rotateEulerDeg": [a.to_degrees(), b.to_degrees(), c.to_degrees()],
604 })
605}