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}
260
261pub(super) fn map_constraint(
266 entry: &mut ConstraintEntry,
267 a: &ResolvedElement,
268 b: &ResolvedElement,
269 env: &Env,
270) -> Result<MappedConstraint, ConstraintFailure> {
271 match entry.constraint_type.as_str() {
272 "coincident" => super::constraints::coincident::map(a, b),
273 "touch_align" => super::constraints::touch_align::map(entry, a, b),
274 "parallel" => super::constraints::parallel::map(entry, a, b),
275 "distance" => super::constraints::distance::map(entry, a, b, env),
276 "angle" => super::constraints::angle::map(entry, a, b, env),
277 "concentric" => super::constraints::concentric::map(entry, a, b),
278 "perpendicular" => super::constraints::perpendicular::map(a, b),
279 "tangent" => super::constraints::tangent::map(a, b),
280 other => Err(ConstraintFailure::new(
281 "error",
282 format!("Unknown constraint type: {other}"),
283 )),
284 }
285}
286
287pub(super) fn mate(a: &ResolvedElement, b: &ResolvedElement, kind: MateKind) -> MappedMate {
288 MappedMate {
289 body_a: a.component.clone(),
290 body_b: b.component.clone(),
291 kind,
292 }
293}
294
295pub(super) fn direction_of(geometry: &SelectionGeometry) -> Option<Vec3> {
298 match *geometry {
299 SelectionGeometry::Plane { normal, .. } => Some(normal),
300 SelectionGeometry::Axis { direction, .. } | SelectionGeometry::Line { direction, .. } => {
301 Some(direction)
302 }
303 SelectionGeometry::Circle { axis, .. } => Some(axis),
304 SelectionGeometry::Sphere { .. } | SelectionGeometry::Point { .. } => None,
305 }
306}
307
308pub(super) fn local_point(element: &ResolvedElement) -> [f64; 3] {
309 let p = element.local.representative_point();
310 [p.x, p.y, p.z]
311}
312
313pub(super) fn require_direction(element: &ResolvedElement) -> Result<(Vec3, Vec3), ConstraintFailure> {
314 let world = direction_of(&element.world).ok_or_else(|| {
315 ConstraintFailure::unsupported(format!(
316 "selection '{}' carries no direction (needs a planar face, straight edge, axis face, or circular edge)",
317 element.name
318 ))
319 })?;
320 let local = direction_of(&element.local).expect("local mirrors world kind");
321 Ok((world, local))
322}
323
324pub(super) fn require_axis(element: &ResolvedElement) -> Result<crate::MateAxis, ConstraintFailure> {
325 element.local.mate_axis().ok_or_else(|| {
326 ConstraintFailure::unsupported(format!(
327 "selection '{}' carries no axis (needs a cylindrical/conical face, circular edge, or straight edge)",
328 element.name
329 ))
330 })
331}
332
333pub(super) fn require_plane(element: &ResolvedElement) -> Result<crate::MatePlane, ConstraintFailure> {
334 element.local.mate_plane().ok_or_else(|| {
335 ConstraintFailure::unsupported(format!(
336 "selection '{}' is not a planar face",
337 element.name
338 ))
339 })
340}
341
342pub(super) fn pair_signature(elements: &[String]) -> String {
345 let mut pair: Vec<&str> = elements.iter().map(String::as_str).collect();
346 pair.sort_unstable();
347 pair.join("\n")
348}
349
350pub(super) fn effective_align(
355 entry: &mut ConstraintEntry,
356 dir_a: Vec3,
357 dir_b: Vec3,
358 reverse: bool,
359) -> MateAlign {
360 let signature = pair_signature(&entry.elements());
361 let dot = dir_a.dot(dir_b);
362 let cached = entry
363 .persistent("preferredOpposeSignature")
364 .and_then(|value| value.as_str())
365 .map(|stored| stored == signature)
366 .unwrap_or(false)
367 .then(|| entry.persistent("preferredOppose").and_then(|v| v.as_bool()))
368 .flatten();
369 let oppose = cached.unwrap_or_else(|| {
370 let oppose = dot < 0.0;
371 entry.set_persistent("preferredOppose", serde_json::Value::Bool(oppose));
372 entry.set_persistent(
373 "preferredOpposeSignature",
374 serde_json::Value::String(signature),
375 );
376 oppose
377 });
378 entry.set_persistent(
379 "lastOrientationDot",
380 serde_json::json!(dot),
381 );
382 if oppose != reverse {
383 MateAlign::AntiAligned
384 } else {
385 MateAlign::Aligned
386 }
387}
388
389
390
391
392
393#[allow(clippy::type_complexity)]
397pub(super) fn first_solve_target(
398 entry: &ConstraintEntry,
399 param_key: &str,
400 flag_key: &str,
401 configured: f64,
402 current: f64,
403) -> (
404 f64,
405 (Vec<(String, serde_json::Value)>, Vec<(String, serde_json::Value)>),
406) {
407 let initialized = entry
408 .persistent(flag_key)
409 .and_then(|value| value.as_bool())
410 .unwrap_or(false);
411 if initialized {
412 (configured, (Vec::new(), Vec::new()))
413 } else {
414 (
415 current,
416 (
417 vec![(param_key.to_string(), serde_json::json!(current))],
418 vec![(flag_key.to_string(), serde_json::Value::Bool(true))],
419 ),
420 )
421 }
422}
423
424
425
426
427
428pub(super) fn angle_between_deg(a: Vec3, b: Vec3) -> f64 {
433 let denominator = a.length() * b.length();
434 if denominator <= 0.0 {
435 return 0.0;
436 }
437 (a.dot(b) / denominator).clamp(-1.0, 1.0).acos().to_degrees()
438}
439
440pub(super) fn point_line_distance(point: Vec3, origin: Vec3, direction: Vec3) -> f64 {
441 let offset = point.sub(origin);
442 let along = offset.dot(direction) / direction.dot(direction).max(1e-300);
443 offset.sub(direction.scale(along)).length()
444}
445
446pub(super) fn line_line_distance(oa: Vec3, da: Vec3, ob: Vec3, db: Vec3) -> f64 {
449 let cross = da.cross(db);
450 let denominator = cross.length();
451 if denominator < 1e-9 * da.length().max(db.length()).max(1.0) {
452 return point_line_distance(ob, oa, da);
453 }
454 (ob.sub(oa).dot(cross) / denominator).abs()
455}
456
457pub(super) fn pair_scale(a: &ResolvedElement, b: &ResolvedElement) -> f64 {
460 let mut scale = 1.0f64;
461 for point in [
462 a.world.representative_point(),
463 b.world.representative_point(),
464 ] {
465 scale = scale
466 .max(point.x.abs())
467 .max(point.y.abs())
468 .max(point.z.abs());
469 }
470 scale
471}
472
473pub(super) fn rigid_inverse(transform: &AffineTransform) -> Result<AffineTransform, String> {
479 let m = &transform.elements;
480 let r = [[m[0], m[1], m[2]], [m[4], m[5], m[6]], [m[8], m[9], m[10]]];
481 let t = [m[3], m[7], m[11]];
482 let mut out = [0.0f64; 16];
483 for row in 0..3 {
484 for col in 0..3 {
485 out[row * 4 + col] = r[col][row];
486 }
487 out[row * 4 + 3] = -(0..3).map(|k| r[k][row] * t[k]).sum::<f64>();
488 }
489 out[15] = 1.0;
490 AffineTransform::new(out)
491}
492
493pub(super) fn matrix_to_quaternion(transform: &AffineTransform) -> [f64; 4] {
496 let m = &transform.elements;
497 let (r00, r01, r02) = (m[0], m[1], m[2]);
498 let (r10, r11, r12) = (m[4], m[5], m[6]);
499 let (r20, r21, r22) = (m[8], m[9], m[10]);
500 let trace = r00 + r11 + r22;
501 let q = if trace > 0.0 {
502 let s = (trace + 1.0).sqrt() * 2.0;
503 [s / 4.0, (r21 - r12) / s, (r02 - r20) / s, (r10 - r01) / s]
504 } else if r00 > r11 && r00 > r22 {
505 let s = (1.0 + r00 - r11 - r22).sqrt() * 2.0;
506 [(r21 - r12) / s, s / 4.0, (r01 + r10) / s, (r02 + r20) / s]
507 } else if r11 > r22 {
508 let s = (1.0 + r11 - r00 - r22).sqrt() * 2.0;
509 [(r02 - r20) / s, (r01 + r10) / s, s / 4.0, (r12 + r21) / s]
510 } else {
511 let s = (1.0 + r22 - r00 - r11).sqrt() * 2.0;
512 [(r10 - r01) / s, (r02 + r20) / s, (r12 + r21) / s, s / 4.0]
513 };
514 normalize_quaternion(q)
515}
516
517pub(super) fn normalize_quaternion(q: [f64; 4]) -> [f64; 4] {
518 let norm = (q[0] * q[0] + q[1] * q[1] + q[2] * q[2] + q[3] * q[3]).sqrt();
519 if norm <= 0.0 || !norm.is_finite() {
520 return [1.0, 0.0, 0.0, 0.0];
521 }
522 [q[0] / norm, q[1] / norm, q[2] / norm, q[3] / norm]
523}
524
525pub(super) fn pose_to_transform(
529 rotation: [f64; 4],
530 translation: [f64; 3],
531) -> Result<AffineTransform, String> {
532 let [w, x, y, z] = normalize_quaternion(rotation);
533 AffineTransform::new([
534 1.0 - 2.0 * (y * y + z * z), 2.0 * (x * y - w * z), 2.0 * (x * z + w * y), translation[0],
535 2.0 * (x * y + w * z), 1.0 - 2.0 * (x * x + z * z), 2.0 * (y * z - w * x), translation[1],
536 2.0 * (x * z - w * y), 2.0 * (y * z + w * x), 1.0 - 2.0 * (x * x + y * y), translation[2],
537 0.0, 0.0, 0.0, 1.0,
538 ])
539}
540
541pub fn transform_to_pose_params(transform: &AffineTransform) -> serde_json::Value {
555 let m = &transform.elements;
556 let sb = m[2].clamp(-1.0, 1.0);
559 let (a, b, c) = if sb.abs() < 1.0 - 1e-9 {
560 (
561 (-m[6]).atan2(m[10]),
562 sb.asin(),
563 (-m[1]).atan2(m[0]),
564 )
565 } else {
566 (m[9].atan2(m[5]), sb.asin(), 0.0)
567 };
568 serde_json::json!({
569 "translate": [m[3], m[7], m[11]],
570 "rotateEulerDeg": [a.to_degrees(), b.to_degrees(), c.to_degrees()],
571 })
572}