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