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 pub transform: AffineTransform,
61 pub curve: Option<crate::MateCurve>,
64 pub freeform: Option<ConstraintFailure>,
68}
69
70pub(super) fn resolve_element(
74 scene: &SceneMap,
75 name: &str,
76) -> Result<ResolvedElement, ConstraintFailure> {
77 let record = scene.owning_component(name).ok_or_else(|| {
78 ConstraintFailure::invalid(format!(
79 "selection '{name}' does not belong to an assembly component — only component geometry participates in constraints"
80 ))
81 })?;
82 if let Some(follow) = &record.pattern {
83 return Err(ConstraintFailure::invalid(format!(
84 "'{}' is instance {} of pattern {}: it follows its seed {} and takes no mates of its own — mate {} instead",
85 record.id, follow.key, follow.pattern_id, follow.seed, follow.seed
86 )));
87 }
88 let curve = edge_curve(scene, name, &record.transform)?;
89 let (world, freeform) = match resolve_world_geometry(scene, name, record) {
90 Ok(world) => (world, None),
91 Err(failure) if failure.status == "unsupported-selection" && curve.is_some() => {
92 let chord = curve.as_ref().and_then(|c| {
93 let mid = c.curve.evaluate(0.5 * (c.t0 + c.t1)).ok()?;
94 Some(record.transform.point(mid))
95 });
96 match chord {
97 Some(position) => (SelectionGeometry::Point { position }, Some(failure)),
98 None => return Err(failure),
99 }
100 }
101 Err(failure) => return Err(failure),
102 };
103 let inverse = record.transform.rigid_inverse().map_err(|error| {
104 ConstraintFailure::new(
105 "error",
106 format!("component '{}': non-rigid pose: {error}", record.id),
107 )
108 })?;
109 let local = world.transformed(&inverse).map_err(|error| {
110 ConstraintFailure::new("error", format!("selection '{name}': {error}"))
111 })?;
112 Ok(ResolvedElement {
113 curve,
114 freeform,
115 name: name.to_string(),
116 component: record.id.clone(),
117 world,
118 local,
119 transform: record.transform,
120 })
121}
122
123fn edge_curve(
126 scene: &SceneMap,
127 name: &str,
128 transform: &AffineTransform,
129) -> Result<Option<crate::MateCurve>, ConstraintFailure> {
130 let Some(edge) = scene.resolve_edge(name) else { return Ok(None) };
131 let inverse = transform.rigid_inverse().map_err(ConstraintFailure::invalid)?;
132 crate::with_registered_solid_str(edge.handle, |solid| {
133 let Some(record) = solid.edges.iter().find(|record| record.id == edge.edge_id) else {
134 return Ok(None);
135 };
136 let curve = crate::transform_topology::transform_curve(&record.curve, inverse)?;
137 Ok(Some(crate::MateCurve { curve, t0: record.t0, t1: record.t1 }))
138 })
139 .map_err(ConstraintFailure::invalid)
140}
141
142fn resolve_world_geometry(
143 scene: &SceneMap,
144 name: &str,
145 record: &ComponentRecord,
146) -> Result<SelectionGeometry, ConstraintFailure> {
147 if let Some((solid_name, coords)) = name.split_once('@') {
149 let handle = scene.resolve_solid(solid_name).ok_or_else(|| {
150 ConstraintFailure::invalid(format!("vertex ref '{name}': unknown solid '{solid_name}'"))
151 })?;
152 let local = parse_triple(coords).ok_or_else(|| {
153 ConstraintFailure::invalid(format!(
154 "vertex ref '{name}': position must be 'x,y,z' numbers"
155 ))
156 })?;
157 let world_query = record.transform.point(local);
158 return crate::with_registered_solid_str(handle, |solid| {
159 Ok(resolve_vertex_selection(solid, world_query))
160 })
161 .map_err(ConstraintFailure::invalid)?
162 .map_err(|error| ConstraintFailure::new(error.status(), error.to_string()));
163 }
164
165 let (_, local_name) = crate::split_component_namespace(name);
169 if crate::is_component_reference(local_name) {
170 let prefix = format!("{name}:");
171 let members: Vec<(String, u32)> = scene
172 .component_solids(&record.id)
173 .into_iter()
174 .filter(|(member, _)| record.id == name || member.starts_with(&prefix))
175 .collect();
176 if members.is_empty() {
177 return Err(ConstraintFailure::invalid(format!(
178 "component ref '{name}' has no member solids"
179 )));
180 }
181 return component_point(&members);
182 }
183
184 if let Some(face) = scene.resolve_face(name) {
187 return crate::with_registered_solid_str(face.handle, |solid| {
188 Ok(resolve_face_selection(solid, face.face_id))
189 })
190 .map_err(ConstraintFailure::invalid)?
191 .map_err(|error| ConstraintFailure::new(error.status(), error.to_string()));
192 }
193 if let Some(edge) = scene.resolve_edge(name) {
194 return crate::with_registered_solid_str(edge.handle, |solid| {
195 Ok(resolve_edge_selection(solid, edge.edge_id))
196 })
197 .map_err(ConstraintFailure::invalid)?
198 .map_err(|error| ConstraintFailure::new(error.status(), error.to_string()));
199 }
200 if let Some(handle) = scene.resolve_solid(name) {
203 return component_point(&[(name.to_string(), handle)]);
204 }
205 for (_member, handle) in scene.component_solids(&record.id) {
208 let found = crate::with_registered_solid_str(handle, |solid| {
209 Ok(resolve_named_selection(solid, name).ok())
210 })
211 .map_err(ConstraintFailure::invalid)?;
212 if let Some(geometry) = found {
213 return Ok(geometry);
214 }
215 }
216 Err(ConstraintFailure::invalid(format!(
217 "selection '{name}' not found in the scene"
218 )))
219}
220
221fn component_point(members: &[(String, u32)]) -> Result<SelectionGeometry, ConstraintFailure> {
226 let mut low = Vec3::new(f64::INFINITY, f64::INFINITY, f64::INFINITY);
227 let mut high = Vec3::new(f64::NEG_INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY);
228 let mut any = false;
229 for (member, handle) in members {
230 let (lo, hi, non_empty) = crate::with_registered_solid_str(*handle, |solid| {
231 let mut lo = Vec3::new(f64::INFINITY, f64::INFINITY, f64::INFINITY);
232 let mut hi = Vec3::new(f64::NEG_INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY);
233 let mut non_empty = false;
234 let mut include = |point: Vec3| {
235 lo = Vec3::new(lo.x.min(point.x), lo.y.min(point.y), lo.z.min(point.z));
236 hi = Vec3::new(hi.x.max(point.x), hi.y.max(point.y), hi.z.max(point.z));
237 non_empty = true;
238 };
239 for vertex in &solid.vertices {
240 include(vertex.point);
241 }
242 for shell in &solid.shells {
243 for face in &shell.faces {
244 for row in &face.surface.control_points {
245 for control in row {
246 include(control.point()?);
247 }
248 }
249 }
250 }
251 Ok((lo, hi, non_empty))
252 })
253 .map_err(|error| {
254 ConstraintFailure::invalid(format!("component member '{member}': {error}"))
255 })?;
256 if non_empty {
257 low = Vec3::new(low.x.min(lo.x), low.y.min(lo.y), low.z.min(lo.z));
258 high = Vec3::new(high.x.max(hi.x), high.y.max(hi.y), high.z.max(hi.z));
259 any = true;
260 }
261 }
262 if !any {
263 return Err(ConstraintFailure::invalid(
264 "component has no geometry to anchor",
265 ));
266 }
267 Ok(SelectionGeometry::Point {
268 position: low.add(high).scale(0.5),
269 })
270}
271
272fn parse_triple(coords: &str) -> Option<Vec3> {
273 let mut parts = coords.split(',').map(str::trim);
274 let x = parts.next()?.parse::<f64>().ok()?;
275 let y = parts.next()?.parse::<f64>().ok()?;
276 let z = parts.next()?.parse::<f64>().ok()?;
277 if parts.next().is_some() {
278 return None;
279 }
280 Some(Vec3::new(x, y, z))
281}
282
283#[derive(Debug, Clone)]
289pub(super) struct MappedMate {
290 pub body_a: String,
291 pub body_b: String,
292 pub body_c: Option<String>,
295 pub kind: MateKind,
296}
297
298#[derive(Debug, Clone, Default)]
302pub(super) struct MappedConstraint {
303 pub mates: Vec<MappedMate>,
304 pub pending_params: Vec<(String, serde_json::Value)>,
305 pub pending_persistent: Vec<(String, serde_json::Value)>,
306 pub measured: Option<(f64, &'static str)>,
309 pub target: Option<f64>,
311 pub angle_axis: Option<Vec3>,
313 pub groups: Vec<Vec<usize>>,
317 pub note: Option<String>,
321}
322
323pub(super) fn map_constraint(
329 entry: &mut ConstraintEntry,
330 resolved: &[ResolvedElement],
331 env: &Env,
332) -> Result<MappedConstraint, ConstraintFailure> {
333 let pair = || -> Result<(&ResolvedElement, &ResolvedElement), ConstraintFailure> {
335 match resolved {
336 [a, b] => Ok((a, b)),
337 _ => Err(ConstraintFailure::invalid(format!(
338 "{} takes exactly two elements ({} given)",
339 entry.constraint_type,
340 resolved.len()
341 ))),
342 }
343 };
344 if !matches!(entry.constraint_type.as_str(), "path" | "cam") {
346 if let Some(refusal) = resolved.iter().find_map(|element| element.freeform.clone()) {
347 return Err(refusal);
348 }
349 }
350 match entry.constraint_type.as_str() {
351 "coincident" => {
352 let (a, b) = pair()?;
353 super::constraints::coincident::map_entry(entry, a, b)
354 }
355 "touch_align" => {
356 let (a, b) = pair()?;
357 super::constraints::touch_align::map(entry, a, b)
358 }
359 "parallel" => {
360 let (a, b) = pair()?;
361 super::constraints::parallel::map(entry, a, b)
362 }
363 "distance" => {
364 let (a, b) = pair()?;
365 let mut mapped = super::constraints::distance::map(entry, a, b, env)?;
366 super::constraints::apply_limit(entry, &mut mapped, env, |value| value)?;
367 Ok(mapped)
368 }
369 "angle" => {
370 let (a, b) = pair()?;
371 let mut mapped = super::constraints::angle::map(entry, a, b, env)?;
372 let exterior = entry.flag("exteriorAngle");
373 super::constraints::apply_limit(entry, &mut mapped, env, |value| {
374 if exterior { 180.0 - value } else { value }
375 })?;
376 Ok(mapped)
377 }
378 "concentric" => {
379 let (a, b) = pair()?;
380 super::constraints::concentric::map(entry, a, b)
381 }
382 "perpendicular" => pair().and_then(|(a, b)| super::constraints::perpendicular::map(a, b)),
383 "tangent" => pair().and_then(|(a, b)| super::constraints::tangent::map(a, b)),
384 "center" => super::constraints::center::map(entry, resolved),
385 "symmetric" => super::constraints::symmetric::map(resolved),
386 "gear" => {
387 let (a, b) = pair()?;
388 super::constraints::gear::map(entry, a, b, env)
389 }
390 "rack_pinion" => {
391 let (a, b) = pair()?;
392 super::constraints::rack_pinion::map(entry, a, b, env)
393 }
394 "screw" => {
395 let (a, b) = pair()?;
396 super::constraints::screw::map(entry, a, b, env)
397 }
398 "path" => {
399 let (a, b) = pair()?;
400 super::constraints::path::map(entry, a, b, env)
401 }
402 "slot" => super::constraints::slot::map(entry, resolved, env),
403 "cam" => {
404 let (a, b) = pair()?;
405 super::constraints::cam::map(entry, a, b, env)
406 }
407 other => Err(ConstraintFailure::new(
408 "error",
409 format!("Unknown constraint type: {other}"),
410 )),
411 }
412}
413
414pub(super) fn mate(a: &ResolvedElement, b: &ResolvedElement, kind: MateKind) -> MappedMate {
415 MappedMate {
416 body_a: a.component.clone(),
417 body_b: b.component.clone(),
418 body_c: None,
419 kind,
420 }
421}
422
423pub(super) fn direction_of(geometry: &SelectionGeometry) -> Option<Vec3> {
426 match *geometry {
427 SelectionGeometry::Plane { normal, .. } => Some(normal),
428 SelectionGeometry::Axis { direction, .. } | SelectionGeometry::Line { direction, .. } => {
429 Some(direction)
430 }
431 SelectionGeometry::Circle { axis, .. } => Some(axis),
432 SelectionGeometry::Sphere { .. } | SelectionGeometry::Point { .. } => None,
433 }
434}
435
436pub(super) fn local_point(element: &ResolvedElement) -> [f64; 3] {
437 let p = element.local.representative_point();
438 [p.x, p.y, p.z]
439}
440
441pub(super) fn require_direction(element: &ResolvedElement) -> Result<(Vec3, Vec3), ConstraintFailure> {
442 let world = direction_of(&element.world).ok_or_else(|| {
443 ConstraintFailure::unsupported(format!(
444 "selection '{}' carries no direction (needs a planar face, straight edge, axis face, or circular edge)",
445 element.name
446 ))
447 })?;
448 let local = direction_of(&element.local).expect("local mirrors world kind");
449 Ok((world, local))
450}
451
452pub(super) fn require_axis(element: &ResolvedElement) -> Result<crate::MateAxis, ConstraintFailure> {
453 element.local.mate_axis().ok_or_else(|| {
454 ConstraintFailure::unsupported(format!(
455 "selection '{}' carries no axis (needs a cylindrical/conical face, circular edge, or straight edge)",
456 element.name
457 ))
458 })
459}
460
461pub(super) fn require_plane(element: &ResolvedElement) -> Result<crate::MatePlane, ConstraintFailure> {
462 element.local.mate_plane().ok_or_else(|| {
463 ConstraintFailure::unsupported(format!(
464 "selection '{}' is not a planar face",
465 element.name
466 ))
467 })
468}
469
470pub(super) fn pair_signature(elements: &[String]) -> String {
473 let mut pair: Vec<&str> = elements.iter().map(String::as_str).collect();
474 pair.sort_unstable();
475 pair.join("\n")
476}
477
478pub(super) fn effective_align(
489 entry: &mut ConstraintEntry,
490 dir_a: Vec3,
491 dir_b: Vec3,
492 legacy: &str,
493) -> MateAlign {
494 let dot = dir_a.dot(dir_b);
495 entry.set_persistent("lastOrientationDot", serde_json::json!(dot));
496 match entry.input_params.get(FACING).and_then(|value| value.as_str()) {
497 Some(OPPOSED) => return MateAlign::AntiAligned,
498 Some(_) => return MateAlign::Aligned,
499 None => {}
500 }
501 let signature = pair_signature(&entry.elements());
502 let cached = entry
503 .persistent("preferredOpposeSignature")
504 .and_then(|value| value.as_str())
505 .map(|stored| stored == signature)
506 .unwrap_or(false)
507 .then(|| entry.persistent("preferredOppose").and_then(|v| v.as_bool()))
508 .flatten();
509 let opposed = cached.unwrap_or(dot < 0.0) != entry.flag(legacy);
510 if let Some(params) = entry.input_params.as_object_mut() {
511 params.remove(legacy);
512 params.insert(FACING.into(), serde_json::json!(if opposed { OPPOSED } else { ALIGNED }));
513 }
514 entry.remove_persistent("preferredOppose");
515 entry.remove_persistent("preferredOpposeSignature");
516 if opposed {
517 MateAlign::AntiAligned
518 } else {
519 MateAlign::Aligned
520 }
521}
522
523pub(super) const FACING: &str = "facing";
525pub(super) const ALIGNED: &str = "aligned";
526pub(super) const OPPOSED: &str = "opposed";
527
528pub(super) fn facing_field(hint: &str) -> serde_json::Value {
531 serde_json::json!({
532 "type": "options",
533 "options": [ALIGNED, OPPOSED],
534 "default_value": null,
535 "label": "Facing",
536 "hint": hint,
537 })
538}
539
540
541
542
543
544#[allow(clippy::type_complexity)]
551pub(super) fn first_solve_target(
552 entry: &ConstraintEntry,
553 param_key: &str,
554 flag_key: &str,
555 configured: f64,
556 current: f64,
557) -> (
558 f64,
559 (Vec<(String, serde_json::Value)>, Vec<(String, serde_json::Value)>),
560) {
561 let initialized = entry
562 .persistent(flag_key)
563 .and_then(|value| value.as_bool())
564 .unwrap_or(false);
565 let given = entry.input_params.get(param_key).is_some_and(|value| !value.is_null());
566 if initialized {
567 (configured, (Vec::new(), Vec::new()))
568 } else if given {
569 (configured, (Vec::new(), vec![(flag_key.to_string(), serde_json::Value::Bool(true))]))
570 } else {
571 (
572 current,
573 (
574 vec![(param_key.to_string(), serde_json::json!(current))],
575 vec![(flag_key.to_string(), serde_json::Value::Bool(true))],
576 ),
577 )
578 }
579}
580
581
582
583
584
585pub(super) fn angle_between_deg(a: Vec3, b: Vec3) -> f64 {
590 let denominator = a.length() * b.length();
591 if denominator <= 0.0 {
592 return 0.0;
593 }
594 (a.dot(b) / denominator).clamp(-1.0, 1.0).acos().to_degrees()
595}
596
597pub(super) fn point_line_distance(point: Vec3, origin: Vec3, direction: Vec3) -> f64 {
598 let offset = point.sub(origin);
599 let along = offset.dot(direction) / direction.dot(direction).max(1e-300);
600 offset.sub(direction.scale(along)).length()
601}
602
603pub(super) fn line_line_distance(oa: Vec3, da: Vec3, ob: Vec3, db: Vec3) -> f64 {
606 let cross = da.cross(db);
607 let denominator = cross.length();
608 if denominator < 1e-9 * da.length().max(db.length()).max(1.0) {
609 return point_line_distance(ob, oa, da);
610 }
611 (ob.sub(oa).dot(cross) / denominator).abs()
612}
613
614pub(super) fn elements_scale(elements: &[ResolvedElement]) -> f64 {
617 let mut scale = 1.0f64;
618 for point in elements.iter().map(|element| element.world.representative_point()) {
619 scale = scale
620 .max(point.x.abs())
621 .max(point.y.abs())
622 .max(point.z.abs());
623 }
624 scale
625}
626
627pub(super) fn matrix_to_quaternion(transform: &AffineTransform) -> [f64; 4] {
634 let m = &transform.elements;
635 let (r00, r01, r02) = (m[0], m[1], m[2]);
636 let (r10, r11, r12) = (m[4], m[5], m[6]);
637 let (r20, r21, r22) = (m[8], m[9], m[10]);
638 let trace = r00 + r11 + r22;
639 let q = if trace > 0.0 {
640 let s = (trace + 1.0).sqrt() * 2.0;
641 [s / 4.0, (r21 - r12) / s, (r02 - r20) / s, (r10 - r01) / s]
642 } else if r00 > r11 && r00 > r22 {
643 let s = (1.0 + r00 - r11 - r22).sqrt() * 2.0;
644 [(r21 - r12) / s, s / 4.0, (r01 + r10) / s, (r02 + r20) / s]
645 } else if r11 > r22 {
646 let s = (1.0 + r11 - r00 - r22).sqrt() * 2.0;
647 [(r02 - r20) / s, (r01 + r10) / s, s / 4.0, (r12 + r21) / s]
648 } else {
649 let s = (1.0 + r22 - r00 - r11).sqrt() * 2.0;
650 [(r10 - r01) / s, (r02 + r20) / s, (r12 + r21) / s, s / 4.0]
651 };
652 normalize_quaternion(q)
653}
654
655pub(super) fn normalize_quaternion(q: [f64; 4]) -> [f64; 4] {
656 let norm = (q[0] * q[0] + q[1] * q[1] + q[2] * q[2] + q[3] * q[3]).sqrt();
657 if norm <= 0.0 || !norm.is_finite() {
658 return [1.0, 0.0, 0.0, 0.0];
659 }
660 [q[0] / norm, q[1] / norm, q[2] / norm, q[3] / norm]
661}
662
663pub(super) fn pose_to_transform(
667 rotation: [f64; 4],
668 translation: [f64; 3],
669) -> Result<AffineTransform, String> {
670 let [w, x, y, z] = normalize_quaternion(rotation);
671 AffineTransform::new([
672 1.0 - 2.0 * (y * y + z * z), 2.0 * (x * y - w * z), 2.0 * (x * z + w * y), translation[0],
673 2.0 * (x * y + w * z), 1.0 - 2.0 * (x * x + z * z), 2.0 * (y * z - w * x), translation[1],
674 2.0 * (x * z - w * y), 2.0 * (y * z + w * x), 1.0 - 2.0 * (x * x + y * y), translation[2],
675 0.0, 0.0, 0.0, 1.0,
676 ])
677}
678
679pub(super) fn pose_params_to_transform(pose: &serde_json::Value) -> Result<AffineTransform, String> {
683 let triple = |key: &str| -> Result<[f64; 3], String> {
684 let values = pose.get(key).and_then(|value| value.as_array());
685 let Some(values) = values else { return Ok([0.0; 3]) };
686 let mut out = [0.0; 3];
687 for (slot, value) in out.iter_mut().zip(values) {
688 *slot = value.as_f64().ok_or_else(|| format!("pose {key}: not a number"))?;
689 }
690 Ok(out)
691 };
692 let translate = triple("translate")?;
693 let degrees = triple("rotateEulerDeg")?;
694 AffineTransform::new(crate::feature_pipeline::features::common::compose_trs_matrix(
695 translate,
696 [degrees[0].to_radians(), degrees[1].to_radians(), degrees[2].to_radians()],
697 [1.0, 1.0, 1.0],
698 [0.0, 0.0, 0.0],
699 ))
700}
701
702pub fn transform_to_pose_params(transform: &AffineTransform) -> serde_json::Value {
716 let m = &transform.elements;
717 let sb = m[2].clamp(-1.0, 1.0);
720 let (a, b, c) = if sb.abs() < 1.0 - 1e-9 {
721 (
722 (-m[6]).atan2(m[10]),
723 sb.asin(),
724 (-m[1]).atan2(m[0]),
725 )
726 } else {
727 (m[9].atan2(m[5]), sb.asin(), 0.0)
728 };
729 serde_json::json!({
730 "translate": [m[3], m[7], m[11]],
731 "rotateEulerDeg": [a.to_degrees(), b.to_degrees(), c.to_degrees()],
732 })
733}