1use crate::solver_linear_algebra::{row_reduce, solve_spd};
52use crate::Vec3;
53use serde::{Deserialize, Serialize};
54use serde_json::{Map, Value};
55use std::collections::HashMap;
56
57const MAX_DAMPING: usize = 16;
63const LAMBDA_UP: f64 = 8.0;
65const LAMBDA_DOWN: f64 = 0.25;
67const STEP_REL_TOL: f64 = 1e-13;
69const FD_STEP: f64 = 1e-6;
71const RANK_REL_TOL: f64 = 1e-7;
73const MOBILITY_TOL: f64 = 1e-6;
76const CONFLICT_REL_TOL: f64 = 1e-6;
79const DIR_EPS_REL: f64 = 1e-12;
81
82fn default_tolerance() -> f64 {
83 1e-10
84}
85
86fn default_max_iterations() -> usize {
87 200
88}
89
90#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
96pub struct Sketch3dPoint {
97 pub id: Value,
100 pub x: f64,
101 pub y: f64,
102 pub z: f64,
103 #[serde(default)]
105 pub fixed: bool,
106 #[serde(default)]
108 pub construction: bool,
109 #[serde(default, rename = "externalReference")]
112 pub external_reference: bool,
113}
114
115impl Sketch3dPoint {
116 pub fn position(&self) -> Vec3 {
117 Vec3::new(self.x, self.y, self.z)
118 }
119}
120
121#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
123pub struct Sketch3dGeometry {
124 pub id: Value,
125 #[serde(rename = "type")]
126 pub geom_type: String,
127 #[serde(default)]
128 pub points: Vec<Value>,
129 #[serde(flatten)]
132 pub extra: Map<String, Value>,
133}
134
135impl Sketch3dGeometry {
136 pub fn construction(&self) -> bool {
139 self.geom_type == "plane"
140 || self
141 .extra
142 .get("construction")
143 .and_then(Value::as_bool)
144 .unwrap_or(false)
145 }
146}
147
148#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
151#[serde(transparent)]
152pub struct Sketch3dConstraint {
153 pub raw: Map<String, Value>,
154}
155
156impl Sketch3dConstraint {
157 pub fn ctype(&self) -> Option<&str> {
158 self.raw.get("type").and_then(Value::as_str)
159 }
160
161 pub fn id(&self) -> Option<&Value> {
162 self.raw.get("id")
163 }
164
165 pub fn points(&self) -> &[Value] {
166 self.raw
167 .get("points")
168 .and_then(Value::as_array)
169 .map(Vec::as_slice)
170 .unwrap_or(&[])
171 }
172
173 pub fn value(&self) -> Option<f64> {
176 self.raw.get("value").and_then(Value::as_f64)
177 }
178}
179
180#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
182pub struct Sketch3dDoc {
183 #[serde(default)]
184 pub points: Vec<Sketch3dPoint>,
185 #[serde(default)]
186 pub geometries: Vec<Sketch3dGeometry>,
187 #[serde(default)]
188 pub constraints: Vec<Sketch3dConstraint>,
189}
190
191impl Sketch3dDoc {
192 pub fn point(&self, id: &Value) -> Option<&Sketch3dPoint> {
193 let key = id_key(id);
194 self.points.iter().find(|p| id_key(&p.id) == key)
195 }
196
197 pub fn point_mut(&mut self, id: &Value) -> Option<&mut Sketch3dPoint> {
198 let key = id_key(id);
199 self.points.iter_mut().find(|p| id_key(&p.id) == key)
200 }
201
202 pub fn geometry(&self, id: &Value) -> Option<&Sketch3dGeometry> {
203 let key = id_key(id);
204 self.geometries.iter().find(|g| id_key(&g.id) == key)
205 }
206
207 pub fn geometry_mut(&mut self, id: &Value) -> Option<&mut Sketch3dGeometry> {
208 let key = id_key(id);
209 self.geometries.iter_mut().find(|g| id_key(&g.id) == key)
210 }
211
212 pub fn constraint(&self, id: &Value) -> Option<&Sketch3dConstraint> {
213 let key = id_key(id);
214 self.constraints
215 .iter()
216 .find(|c| c.id().map(id_key) == Some(key.clone()))
217 }
218
219 pub fn next_point_id(&self) -> Value {
221 mint_next_id(self.points.iter().map(|p| &p.id))
222 }
223
224 pub fn next_geometry_id(&self) -> Value {
226 mint_next_id(self.geometries.iter().map(|g| &g.id))
227 }
228
229 pub fn next_constraint_id(&self) -> Value {
231 mint_next_id(self.constraints.iter().filter_map(|c| c.id()))
232 }
233
234 pub fn add_point(&mut self, p: Vec3) -> Value {
236 let id = self.next_point_id();
237 self.points.push(Sketch3dPoint {
238 id: id.clone(),
239 x: p.x,
240 y: p.y,
241 z: p.z,
242 fixed: false,
243 construction: false,
244 external_reference: false,
245 });
246 id
247 }
248
249 pub fn snap_or_add_point(&mut self, p: Vec3, radius: f64) -> Value {
252 let mut best: Option<(f64, Value)> = None;
253 for point in &self.points {
254 let d = point.position().sub(p).length();
255 if d <= radius && best.as_ref().map_or(true, |(bd, _)| d < *bd) {
256 best = Some((d, point.id.clone()));
257 }
258 }
259 match best {
260 Some((_, id)) => id,
261 None => self.add_point(p),
262 }
263 }
264
265 pub fn add_geometry(&mut self, geom_type: &str, points: Vec<Value>, construction: bool) -> Value {
267 let id = self.next_geometry_id();
268 let mut extra = Map::new();
269 if construction {
270 extra.insert("construction".into(), Value::Bool(true));
271 }
272 self.geometries.push(Sketch3dGeometry {
273 id: id.clone(),
274 geom_type: geom_type.to_string(),
275 points,
276 extra,
277 });
278 id
279 }
280
281 pub fn add_constraint(&mut self, ctype: &str, points: Vec<Value>, value: Option<f64>) -> Value {
284 let id = self.next_constraint_id();
285 let mut raw = Map::new();
286 raw.insert("id".into(), id.clone());
287 raw.insert("type".into(), Value::String(ctype.to_string()));
288 raw.insert("points".into(), Value::Array(points));
289 if let Some(value) = value {
290 raw.insert("value".into(), Value::from(value));
291 }
292 self.constraints.push(Sketch3dConstraint { raw });
293 id
294 }
295
296 pub fn geometry_positions(&self, geometry: &Sketch3dGeometry) -> Option<Vec<Vec3>> {
299 geometry
300 .points
301 .iter()
302 .map(|id| self.point(id).map(Sketch3dPoint::position))
303 .collect()
304 }
305}
306
307pub use crate::sketch_solver::sketch_id_key as id_key;
309
310fn id_num(value: &Value) -> Option<f64> {
311 match value {
312 Value::Number(number) => number.as_f64(),
313 Value::String(text) => text.trim().parse::<f64>().ok(),
314 _ => None,
315 }
316}
317
318fn mint_next_id<'a>(ids: impl Iterator<Item = &'a Value>) -> Value {
319 let ids: Vec<&Value> = ids.collect();
320 let mut max: Option<i64> = None;
321 for &id in &ids {
322 if let Some(n) = id_num(id) {
323 if n.is_finite() {
324 let i = n.floor() as i64;
325 max = Some(max.map_or(i, |m| m.max(i)));
326 }
327 }
328 }
329 let mut candidate = max.map(|m| m + 1).unwrap_or(ids.len() as i64);
330 let existing: std::collections::HashSet<String> = ids.iter().map(|&id| id_key(id)).collect();
331 while existing.contains(&id_key(&Value::from(candidate))) {
332 candidate += 1;
333 }
334 Value::from(candidate)
335}
336
337#[derive(Clone, Copy, Debug)]
345pub struct Sketch3dConstraintType {
346 pub name: &'static str,
347 pub arity: usize,
349 pub dimensional: bool,
351 pub meaning: &'static str,
352}
353
354pub const SKETCH3D_CONSTRAINT_TYPES: &[Sketch3dConstraintType] = &[
358 Sketch3dConstraintType { name: "fixed", arity: 1, dimensional: false, meaning: "ground a point where it is" },
359 Sketch3dConstraintType { name: "coincident", arity: 2, dimensional: false, meaning: "two points touch" },
360 Sketch3dConstraintType { name: "distance", arity: 2, dimensional: true, meaning: "distance between two points (a line's length)" },
361 Sketch3dConstraintType { name: "onLine", arity: 3, dimensional: false, meaning: "[p, a0, a1]: point on the infinite line a0→a1" },
362 Sketch3dConstraintType { name: "lineDistance", arity: 3, dimensional: true, meaning: "[p, a0, a1]: distance from the point to the infinite line" },
363 Sketch3dConstraintType { name: "onPlane", arity: 3, dimensional: false, meaning: "[p, origin, normalTip]: point on the plane" },
364 Sketch3dConstraintType { name: "planeDistance", arity: 3, dimensional: true, meaning: "[p, origin, normalTip]: signed height of the point above the plane" },
365 Sketch3dConstraintType { name: "parallel", arity: 4, dimensional: false, meaning: "[a0, a1, b0, b1]: the two directions are parallel (either sense)" },
366 Sketch3dConstraintType { name: "perpendicular", arity: 4, dimensional: false, meaning: "[a0, a1, b0, b1]: the two directions are perpendicular" },
367 Sketch3dConstraintType { name: "angle", arity: 4, dimensional: true, meaning: "[a0, a1, b0, b1]: angle in degrees between the two directions" },
368 Sketch3dConstraintType { name: "alongX", arity: 2, dimensional: false, meaning: "the line runs along world X" },
369 Sketch3dConstraintType { name: "alongY", arity: 2, dimensional: false, meaning: "the line runs along world Y" },
370 Sketch3dConstraintType { name: "alongZ", arity: 2, dimensional: false, meaning: "the line runs along world Z" },
371 Sketch3dConstraintType { name: "equal", arity: 4, dimensional: false, meaning: "[a0, a1, b0, b1]: equal lengths (an arc's radius is center→start)" },
372 Sketch3dConstraintType { name: "tangent", arity: 5, dimensional: false, meaning: "[a0, a1, center, start, end]: the line is tangent to the arc at the arc end it shares" },
373];
374
375pub fn sketch3d_constraint_type(name: &str) -> Option<&'static Sketch3dConstraintType> {
377 SKETCH3D_CONSTRAINT_TYPES.iter().find(|t| t.name == name)
378}
379
380#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
385pub struct Sketch3dSolveOptions {
386 #[serde(default = "default_tolerance")]
389 pub tolerance: f64,
390 #[serde(default = "default_max_iterations")]
392 pub max_iterations: usize,
393}
394
395impl Default for Sketch3dSolveOptions {
396 fn default() -> Self {
397 Self {
398 tolerance: default_tolerance(),
399 max_iterations: default_max_iterations(),
400 }
401 }
402}
403
404#[derive(Debug, Deserialize)]
406pub struct SolveSketch3dRequest {
407 pub sketch: Value,
408 #[serde(default)]
409 pub tolerance: Option<f64>,
410 #[serde(default)]
411 pub max_iterations: Option<usize>,
412}
413
414#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
419pub struct Sketch3dDiagnostics {
420 #[serde(default)]
421 pub dof: i64,
422 #[serde(default)]
423 pub rank: i64,
424 #[serde(default)]
425 pub unknowns: i64,
426 #[serde(default)]
427 pub equations: i64,
428 #[serde(default)]
429 pub redundant: i64,
430 #[serde(default)]
432 pub status: String,
433 #[serde(default)]
434 pub conflicting: bool,
435 #[serde(default, rename = "conflictingConstraints")]
438 pub conflicting_constraints: Vec<String>,
439 #[serde(default, rename = "pointMobility")]
441 pub point_mobility: std::collections::BTreeMap<String, String>,
442 #[serde(default, rename = "geometryMobility")]
443 pub geometry_mobility: std::collections::BTreeMap<String, String>,
444 #[serde(default)]
446 pub iterations: i64,
447 #[serde(default, rename = "maxResidual")]
449 pub max_residual: f64,
450}
451
452impl Sketch3dDiagnostics {
453 pub fn point_movable(&self, id: &Value) -> Option<bool> {
454 self.point_mobility.get(&id_key(id)).map(|v| v == "movable")
455 }
456
457 pub fn geometry_movable(&self, id: &Value) -> Option<bool> {
458 self.geometry_mobility.get(&id_key(id)).map(|v| v == "movable")
459 }
460
461 pub fn constraint_conflicting(&self, id: &Value) -> bool {
462 if self.conflicting_constraints.is_empty() {
463 return false;
464 }
465 let key = id_key(id);
466 self.conflicting_constraints.iter().any(|c| *c == key)
467 }
468}
469
470#[derive(Clone, Copy, Debug)]
478enum Atom {
479 Coincide(usize, usize),
481 Distance(usize, usize, f64),
483 Lateral(usize, usize, usize),
486 LineDistance(usize, usize, usize, f64),
488 PlaneOffset(usize, usize, usize, f64),
490 DirCross(usize, usize, usize, usize),
492 DirDot(usize, usize, usize, usize, f64),
494 AxisCross(usize, usize, Vec3),
496 EqualLength(usize, usize, usize, usize),
498 TangentTouch(usize, usize, usize, usize),
501 InArcPlane(usize, usize, usize, usize, usize),
504}
505
506impl Atom {
507 fn rows(&self) -> usize {
508 match self {
509 Atom::Coincide(..) | Atom::Lateral(..) | Atom::DirCross(..) | Atom::AxisCross(..) => 3,
510 Atom::Distance(..)
511 | Atom::LineDistance(..)
512 | Atom::PlaneOffset(..)
513 | Atom::DirDot(..)
514 | Atom::EqualLength(..)
515 | Atom::TangentTouch(..)
516 | Atom::InArcPlane(..) => 1,
517 }
518 }
519
520 fn eff_rows(&self) -> usize {
521 match self {
522 Atom::Coincide(..) => 3,
523 Atom::Lateral(..) | Atom::DirCross(..) | Atom::AxisCross(..) => 2,
524 _ => 1,
525 }
526 }
527
528 fn eval(&self, p: &[Vec3], dir_eps: f64, out: &mut Vec<f64>) {
529 let unit = |v: Vec3| {
530 let len = v.length();
531 if len > dir_eps {
532 v.scale(1.0 / len)
533 } else {
534 v.scale(1.0 / dir_eps)
535 }
536 };
537 match *self {
538 Atom::Coincide(a, b) => {
539 let d = p[a].sub(p[b]);
540 out.extend([d.x, d.y, d.z]);
541 }
542 Atom::Distance(a, b, target) => out.push(p[a].sub(p[b]).length() - target),
543 Atom::Lateral(pt, o, q) => {
544 let d = unit(p[q].sub(p[o]));
545 let v = p[pt].sub(p[o]);
546 let lateral = v.sub(d.scale(d.dot(v)));
547 out.extend([lateral.x, lateral.y, lateral.z]);
548 }
549 Atom::LineDistance(pt, o, q, target) => {
550 let d = unit(p[q].sub(p[o]));
551 let v = p[pt].sub(p[o]);
552 let lateral = v.sub(d.scale(d.dot(v)));
553 out.push(lateral.length() - target);
554 }
555 Atom::PlaneOffset(pt, o, t, target) => {
556 let n = unit(p[t].sub(p[o]));
557 out.push(n.dot(p[pt].sub(p[o])) - target);
558 }
559 Atom::DirCross(a0, a1, b0, b1) => {
560 let c = unit(p[a1].sub(p[a0])).cross(unit(p[b1].sub(p[b0])));
561 out.extend([c.x, c.y, c.z]);
562 }
563 Atom::DirDot(a0, a1, b0, b1, target) => {
564 out.push(unit(p[a1].sub(p[a0])).dot(unit(p[b1].sub(p[b0]))) - target);
565 }
566 Atom::AxisCross(a0, a1, axis) => {
567 let c = unit(p[a1].sub(p[a0])).cross(axis);
568 out.extend([c.x, c.y, c.z]);
569 }
570 Atom::EqualLength(a0, a1, b0, b1) => {
571 out.push(p[a1].sub(p[a0]).length() - p[b1].sub(p[b0]).length());
572 }
573 Atom::TangentTouch(a0, a1, c, t) => {
574 out.push(unit(p[t].sub(p[c])).dot(unit(p[a1].sub(p[a0]))));
575 }
576 Atom::InArcPlane(a0, a1, c, s, e) => {
577 let n = unit(p[s].sub(p[c]).cross(p[e].sub(p[c])));
578 out.push(unit(p[a1].sub(p[a0])).dot(n));
579 }
580 }
581 }
582}
583
584struct Prepared {
589 coords: Vec<Vec3>,
591 free: Vec<usize>,
593 atoms: Vec<Atom>,
594 atom_owner: Vec<Option<usize>>,
596 m: usize,
597 effective_rows: usize,
598 scale: f64,
599 tight: f64,
600}
601
602fn prepare(doc: &Sketch3dDoc, options: &Sketch3dSolveOptions) -> Result<Prepared, String> {
603 let mut index: HashMap<String, usize> = HashMap::with_capacity(doc.points.len());
604 let mut coords: Vec<Vec3> = Vec::with_capacity(doc.points.len());
605 let mut fixed: Vec<bool> = Vec::with_capacity(doc.points.len());
606 for (i, point) in doc.points.iter().enumerate() {
607 if !(point.x.is_finite() && point.y.is_finite() && point.z.is_finite()) {
608 return Err(format!("sketch3d: point {} has a non-finite coordinate", id_key(&point.id)));
609 }
610 index.insert(id_key(&point.id), i);
612 coords.push(point.position());
613 fixed.push(point.fixed);
614 }
615 let lookup = |what: &str, id: &Value| -> Result<usize, String> {
616 index
617 .get(&id_key(id))
618 .copied()
619 .ok_or_else(|| format!("sketch3d: {what} references unknown point {}", id_key(id)))
620 };
621
622 let mut atoms: Vec<Atom> = Vec::new();
623 let mut atom_owner: Vec<Option<usize>> = Vec::new();
624
625 for (ci, constraint) in doc.constraints.iter().enumerate() {
627 let Some(ctype) = constraint.ctype() else {
628 return Err(format!("sketch3d: constraint {ci} has no type"));
629 };
630 let spec = sketch3d_constraint_type(ctype)
631 .ok_or_else(|| format!("sketch3d: unknown constraint type '{ctype}'"))?;
632 let ids = constraint.points();
633 if ids.len() < spec.arity {
634 return Err(format!(
635 "sketch3d: constraint '{ctype}' needs {} points, got {}",
636 spec.arity,
637 ids.len()
638 ));
639 }
640 let what = format!("constraint '{ctype}'");
641 let mut idx = Vec::with_capacity(spec.arity);
642 for id in &ids[..spec.arity] {
643 idx.push(lookup(&what, id)?);
644 }
645 let value = || -> Result<f64, String> {
646 constraint
647 .value()
648 .filter(|v| v.is_finite())
649 .ok_or_else(|| format!("sketch3d: constraint '{ctype}' needs a finite value"))
650 };
651 let mut push = |atom: Atom| {
652 atoms.push(atom);
653 atom_owner.push(Some(ci));
654 };
655 match ctype {
656 "fixed" => fixed[idx[0]] = true,
657 "coincident" => push(Atom::Coincide(idx[0], idx[1])),
658 "distance" => {
659 let target = value()?;
660 if target.abs() <= 0.0 {
661 push(Atom::Coincide(idx[0], idx[1]));
662 } else {
663 push(Atom::Distance(idx[0], idx[1], target.abs()));
664 }
665 }
666 "onLine" => push(Atom::Lateral(idx[0], idx[1], idx[2])),
667 "lineDistance" => {
668 let target = value()?;
669 if target.abs() <= 0.0 {
670 push(Atom::Lateral(idx[0], idx[1], idx[2]));
671 } else {
672 push(Atom::LineDistance(idx[0], idx[1], idx[2], target.abs()));
673 }
674 }
675 "onPlane" => push(Atom::PlaneOffset(idx[0], idx[1], idx[2], 0.0)),
676 "planeDistance" => push(Atom::PlaneOffset(idx[0], idx[1], idx[2], value()?)),
677 "parallel" => push(Atom::DirCross(idx[0], idx[1], idx[2], idx[3])),
678 "perpendicular" => push(Atom::DirDot(idx[0], idx[1], idx[2], idx[3], 0.0)),
679 "angle" => {
680 let target = value()?.to_radians().cos();
681 push(Atom::DirDot(idx[0], idx[1], idx[2], idx[3], target));
682 }
683 "alongX" => push(Atom::AxisCross(idx[0], idx[1], Vec3::new(1.0, 0.0, 0.0))),
684 "alongY" => push(Atom::AxisCross(idx[0], idx[1], Vec3::new(0.0, 1.0, 0.0))),
685 "alongZ" => push(Atom::AxisCross(idx[0], idx[1], Vec3::new(0.0, 0.0, 1.0))),
686 "equal" => push(Atom::EqualLength(idx[0], idx[1], idx[2], idx[3])),
687 "tangent" => {
688 let (a0, a1, c, s, e) = (idx[0], idx[1], idx[2], idx[3], idx[4]);
691 let touch = if s == a0 || s == a1 {
692 s
693 } else if e == a0 || e == a1 {
694 e
695 } else {
696 let near = |t: usize| {
697 coords[t]
698 .sub(coords[a0])
699 .length()
700 .min(coords[t].sub(coords[a1]).length())
701 };
702 if near(s) <= near(e) {
703 s
704 } else {
705 e
706 }
707 };
708 push(Atom::TangentTouch(a0, a1, c, touch));
709 push(Atom::InArcPlane(a0, a1, c, s, e));
710 }
711 other => return Err(format!("sketch3d: unknown constraint type '{other}'")),
712 }
713 }
714
715 for geometry in &doc.geometries {
717 match geometry.geom_type.as_str() {
718 "arc" if geometry.points.len() >= 3 => {
719 let what = "arc geometry";
720 let c = lookup(what, &geometry.points[0])?;
721 let s = lookup(what, &geometry.points[1])?;
722 let e = lookup(what, &geometry.points[2])?;
723 atoms.push(Atom::EqualLength(c, s, c, e));
724 atom_owner.push(None);
725 }
726 "bezier" if geometry.points.len() >= 7 => {
727 let what = "bezier geometry";
728 let ids = &geometry.points;
729 let span_count = (ids.len() - 1) / 3;
730 let last_anchor = span_count * 3;
731 let mut i = 3usize;
732 while i < last_anchor {
733 let prev = lookup(what, &ids[i - 1])?;
734 let anchor = lookup(what, &ids[i])?;
735 let next = lookup(what, &ids[i + 1])?;
736 if prev != anchor && next != anchor && prev != next {
737 atoms.push(Atom::Lateral(anchor, prev, next));
738 atom_owner.push(None);
739 }
740 i += 3;
741 }
742 }
743 _ => {}
744 }
745 }
746
747 let free: Vec<usize> = (0..coords.len()).filter(|&i| !fixed[i]).collect();
748 let m: usize = atoms.iter().map(Atom::rows).sum();
749 let effective_rows: usize = atoms.iter().map(Atom::eff_rows).sum();
750
751 let mut scale = 1.0f64;
752 for c in &coords {
753 scale = scale.max(c.x.abs()).max(c.y.abs()).max(c.z.abs());
754 }
755 let tight = options.tolerance * scale;
756 Ok(Prepared {
757 coords,
758 free,
759 atoms,
760 atom_owner,
761 m,
762 effective_rows,
763 scale,
764 tight,
765 })
766}
767
768fn apply_delta(base: &[Vec3], free: &[usize], delta: &[f64], out: &mut Vec<Vec3>) {
773 out.clear();
774 out.extend_from_slice(base);
775 for (k, &i) in free.iter().enumerate() {
776 out[i] = base[i].add(Vec3::new(delta[3 * k], delta[3 * k + 1], delta[3 * k + 2]));
777 }
778}
779
780fn eval_residuals(
781 atoms: &[Atom],
782 base: &[Vec3],
783 free: &[usize],
784 delta: &[f64],
785 dir_eps: f64,
786 scratch: &mut Vec<Vec3>,
787 out: &mut Vec<f64>,
788) {
789 apply_delta(base, free, delta, scratch);
790 out.clear();
791 for atom in atoms {
792 atom.eval(scratch, dir_eps, out);
793 }
794}
795
796fn finite_diff_jacobian(
797 atoms: &[Atom],
798 base: &[Vec3],
799 free: &[usize],
800 h: f64,
801 dir_eps: f64,
802 m: usize,
803) -> Vec<Vec<f64>> {
804 let n = 3 * free.len();
805 let mut jac = vec![vec![0.0f64; n]; m];
806 if n == 0 || m == 0 {
807 return jac;
808 }
809 let mut delta = vec![0.0f64; n];
810 let mut scratch: Vec<Vec3> = Vec::with_capacity(base.len());
811 let mut rp: Vec<f64> = Vec::with_capacity(m);
812 let mut rm: Vec<f64> = Vec::with_capacity(m);
813 let inv = 1.0 / (2.0 * h);
814 for col in 0..n {
815 delta[col] = h;
816 eval_residuals(atoms, base, free, &delta, dir_eps, &mut scratch, &mut rp);
817 delta[col] = -h;
818 eval_residuals(atoms, base, free, &delta, dir_eps, &mut scratch, &mut rm);
819 delta[col] = 0.0;
820 for i in 0..m {
821 jac[i][col] = (rp[i] - rm[i]) * inv;
822 }
823 }
824 jac
825}
826
827fn lm_core(prep: &Prepared, coords: &mut Vec<Vec3>, max_iterations: usize) -> Result<usize, String> {
831 let atoms = &prep.atoms;
832 let free = &prep.free;
833 let n = 3 * free.len();
834 let m = prep.m;
835 let scale = prep.scale;
836 let dir_eps = DIR_EPS_REL * scale;
837 let h = FD_STEP * scale;
838 let step_tol = STEP_REL_TOL * scale;
839 let tight = prep.tight;
840
841 let max_abs = |v: &[f64]| v.iter().fold(0.0f64, |acc, &x| acc.max(x.abs()));
842 let sum_sq = |v: &[f64]| v.iter().fold(0.0f64, |acc, &x| acc + x * x);
843
844 let zeros = vec![0.0f64; n];
845 let mut scratch: Vec<Vec3> = Vec::with_capacity(coords.len());
846 let mut r: Vec<f64> = Vec::with_capacity(m);
847 eval_residuals(atoms, coords, free, &zeros, dir_eps, &mut scratch, &mut r);
848 if !max_abs(&r).is_finite() {
849 return Err("sketch3d: non-finite residual at the initial coordinates".into());
850 }
851
852 let mut iterations = 0usize;
853 let mut s = sum_sq(&r);
854 let mut best_coords = coords.clone();
855 let mut best_max = max_abs(&r);
856 let mut lambda = -1.0f64;
857
858 if n > 0 && m > 0 {
859 'outer: for _iter in 0..max_iterations {
860 if best_max <= tight {
861 break;
862 }
863 let jac = finite_diff_jacobian(atoms, coords, free, h, dir_eps, m);
864 let mut hm = vec![vec![0.0f64; n]; n];
865 let mut g = vec![0.0f64; n];
866 for i in 0..m {
867 let row = &jac[i];
868 let ri = r[i];
869 for a in 0..n {
870 let ja = row[a];
871 if ja == 0.0 {
872 continue;
873 }
874 g[a] += ja * ri;
875 for b in a..n {
876 hm[a][b] += ja * row[b];
877 }
878 }
879 }
880 for a in 0..n {
881 for b in (a + 1)..n {
882 hm[b][a] = hm[a][b];
883 }
884 }
885 let mut max_diag = 0.0f64;
886 for a in 0..n {
887 max_diag = max_diag.max(hm[a][a]);
888 }
889 if !(max_diag > 0.0) {
890 break;
891 }
892 if lambda < 0.0 {
893 lambda = 1e-9 * max_diag;
894 }
895 let lambda_ceiling = 1e12 * max_diag;
896
897 let mut stepped = false;
898 let mut step_small = false;
899 for _try in 0..MAX_DAMPING {
900 let mut a_mat = hm.clone();
901 for d in 0..n {
902 a_mat[d][d] += lambda;
903 }
904 let mut delta: Vec<f64> = g.iter().map(|v| -v).collect();
905 if !solve_spd(&mut a_mat, &mut delta) {
906 lambda *= LAMBDA_UP;
907 if lambda > lambda_ceiling {
908 break;
909 }
910 continue;
911 }
912 let mut r_trial: Vec<f64> = Vec::with_capacity(m);
913 eval_residuals(atoms, coords, free, &delta, dir_eps, &mut scratch, &mut r_trial);
914 let s_trial = sum_sq(&r_trial);
915 if s_trial.is_finite() && s_trial < s {
916 let step_norm = sum_sq(&delta).sqrt();
917 apply_delta(coords, free, &delta, &mut scratch);
918 coords.copy_from_slice(&scratch);
919 eval_residuals(atoms, coords, free, &zeros, dir_eps, &mut scratch, &mut r);
920 s = sum_sq(&r);
921 let m_now = max_abs(&r);
922 if m_now < best_max {
923 best_max = m_now;
924 best_coords.copy_from_slice(coords);
925 }
926 lambda = (lambda * LAMBDA_DOWN).max(1e-12 * max_diag);
927 iterations += 1;
928 stepped = true;
929 step_small = step_norm <= step_tol;
930 break;
931 }
932 lambda *= LAMBDA_UP;
933 if lambda > lambda_ceiling {
934 break;
935 }
936 }
937 if !stepped || step_small {
938 break 'outer;
939 }
940 }
941 }
942 coords.copy_from_slice(&best_coords);
943 Ok(iterations)
944}
945
946fn rank_and_nullspace(mut a: Vec<Vec<f64>>, m: usize, n: usize) -> (usize, Vec<Vec<f64>>) {
949 if n == 0 {
950 return (0, Vec::new());
951 }
952 let mut maxabs = 0.0f64;
953 for row in &a {
954 for &v in row {
955 maxabs = maxabs.max(v.abs());
956 }
957 }
958 let tol = if maxabs > 0.0 {
959 RANK_REL_TOL * maxabs
960 } else {
961 RANK_REL_TOL
962 };
963 let mut pivot_cols: Vec<usize> = Vec::new();
964 row_reduce(&mut a, m, n, tol, |col| pivot_cols.push(col));
965 let rank = pivot_cols.len();
966 let mut is_pivot = vec![false; n];
967 for &pc in &pivot_cols {
968 is_pivot[pc] = true;
969 }
970 let mut basis: Vec<Vec<f64>> = Vec::new();
971 for free_col in 0..n {
972 if is_pivot[free_col] {
973 continue;
974 }
975 let mut v = vec![0.0f64; n];
976 v[free_col] = 1.0;
977 for (i, &pc) in pivot_cols.iter().enumerate() {
978 v[pc] = -a[i][free_col];
979 }
980 basis.push(v);
981 }
982 (rank, basis)
983}
984
985pub fn solve_sketch3d_doc(
992 doc: &mut Sketch3dDoc,
993 options: &Sketch3dSolveOptions,
994) -> Result<Sketch3dDiagnostics, String> {
995 let prep = prepare(doc, options)?;
996 let mut coords = prep.coords.clone();
997 let iterations = lm_core(&prep, &mut coords, options.max_iterations)?;
998
999 let n = 3 * prep.free.len();
1000 let m = prep.m;
1001 let dir_eps = DIR_EPS_REL * prep.scale;
1002 let zeros = vec![0.0f64; n];
1003 let mut scratch: Vec<Vec3> = Vec::with_capacity(coords.len());
1004 let mut r: Vec<f64> = Vec::with_capacity(m);
1005 eval_residuals(&prep.atoms, &coords, &prep.free, &zeros, dir_eps, &mut scratch, &mut r);
1006
1007 let jac = finite_diff_jacobian(&prep.atoms, &coords, &prep.free, FD_STEP * prep.scale, dir_eps, m);
1008 let row_norms: Vec<f64> = jac
1012 .iter()
1013 .map(|row| row.iter().fold(0.0f64, |acc, &v| acc + v * v).sqrt())
1014 .collect();
1015 let max_row_norm = row_norms.iter().fold(0.0f64, |a, &v| a.max(v));
1016 let norm_floor = if max_row_norm > 0.0 { 1e-9 * max_row_norm } else { 1.0 };
1017 let scaled: Vec<f64> = r
1018 .iter()
1019 .zip(row_norms.iter())
1020 .map(|(&v, &norm)| v / norm.max(norm_floor))
1021 .collect();
1022 let max_residual = scaled.iter().fold(0.0f64, |acc, &v| acc.max(v.abs()));
1023
1024 let (rank, basis) = rank_and_nullspace(jac, m, n);
1025 let dof = n - rank;
1026 let redundant = prep.effective_rows.saturating_sub(rank);
1027
1028 let conflict_tol = CONFLICT_REL_TOL * prep.scale;
1031 let mut flagged = vec![false; doc.constraints.len()];
1032 let mut row = 0usize;
1033 for (ai, atom) in prep.atoms.iter().enumerate() {
1034 let rows = atom.rows();
1035 let violated = scaled[row..row + rows].iter().any(|v| v.abs() > conflict_tol);
1036 if violated {
1037 if let Some(owner) = prep.atom_owner[ai] {
1038 flagged[owner] = true;
1039 }
1040 }
1041 row += rows;
1042 }
1043 let mut conflicting_constraints: Vec<String> = Vec::new();
1044 for (ci, &hit) in flagged.iter().enumerate() {
1045 if hit {
1046 if let Some(id) = doc.constraints[ci].id() {
1047 conflicting_constraints.push(id_key(id));
1048 }
1049 }
1050 }
1051 if max_residual > conflict_tol && conflicting_constraints.is_empty() {
1056 let mut row = 0usize;
1057 for (ai, atom) in prep.atoms.iter().enumerate() {
1058 let rows = atom.rows();
1059 let violated = scaled[row..row + rows].iter().any(|v| v.abs() > conflict_tol);
1060 row += rows;
1061 if !violated || prep.atom_owner[ai].is_some() {
1062 continue;
1063 }
1064 for (ci, constraint) in doc.constraints.iter().enumerate() {
1065 if flagged[ci] {
1066 continue;
1067 }
1068 if let Some(id) = constraint.id() {
1069 flagged[ci] = true;
1070 conflicting_constraints.push(id_key(id));
1071 }
1072 }
1073 }
1074 }
1075 let conflicting = !conflicting_constraints.is_empty();
1076
1077 let status = if redundant > 0 {
1078 "over"
1079 } else if dof > 0 {
1080 "under"
1081 } else {
1082 "well"
1083 };
1084
1085 let basis_max: Vec<f64> = basis
1088 .iter()
1089 .map(|v| v.iter().fold(0.0f64, |a, &x| a.max(x.abs())).max(1e-300))
1090 .collect();
1091 let mut col_of: HashMap<usize, usize> = HashMap::with_capacity(prep.free.len());
1092 for (k, &i) in prep.free.iter().enumerate() {
1093 col_of.insert(i, k);
1094 }
1095 let mut point_mobility = std::collections::BTreeMap::new();
1096 for (i, point) in doc.points.iter().enumerate() {
1097 let movable = match col_of.get(&i) {
1098 None => false,
1099 Some(&k) => basis.iter().enumerate().any(|(b, v)| {
1100 let c = 3 * k;
1101 let comp = ((v[c] / basis_max[b]).powi(2)
1102 + (v[c + 1] / basis_max[b]).powi(2)
1103 + (v[c + 2] / basis_max[b]).powi(2))
1104 .sqrt();
1105 comp > MOBILITY_TOL
1106 }),
1107 };
1108 point_mobility.insert(
1109 id_key(&point.id),
1110 (if movable { "movable" } else { "locked" }).to_string(),
1111 );
1112 }
1113 let mut geometry_mobility = std::collections::BTreeMap::new();
1114 for geometry in &doc.geometries {
1115 let movable = geometry
1116 .points
1117 .iter()
1118 .any(|pid| point_mobility.get(&id_key(pid)).map(String::as_str) == Some("movable"));
1119 geometry_mobility.insert(
1120 id_key(&geometry.id),
1121 (if movable { "movable" } else { "locked" }).to_string(),
1122 );
1123 }
1124
1125 for (i, point) in doc.points.iter_mut().enumerate() {
1127 point.x = coords[i].x;
1128 point.y = coords[i].y;
1129 point.z = coords[i].z;
1130 }
1131
1132 Ok(Sketch3dDiagnostics {
1133 dof: dof as i64,
1134 rank: rank as i64,
1135 unknowns: n as i64,
1136 equations: m as i64,
1137 redundant: redundant as i64,
1138 status: status.into(),
1139 conflicting,
1140 conflicting_constraints,
1141 point_mobility,
1142 geometry_mobility,
1143 iterations: iterations as i64,
1144 max_residual,
1145 })
1146}
1147
1148pub fn solve_sketch3d(request: &SolveSketch3dRequest) -> Result<Value, String> {
1151 let mut doc: Sketch3dDoc = serde_json::from_value(request.sketch.clone())
1152 .map_err(|e| format!("sketch3d: invalid sketch document: {e}"))?;
1153 let options = Sketch3dSolveOptions {
1154 tolerance: request.tolerance.unwrap_or_else(default_tolerance),
1155 max_iterations: request.max_iterations.unwrap_or_else(default_max_iterations),
1156 };
1157 let diagnostics = solve_sketch3d_doc(&mut doc, &options)?;
1158 let mut sketch = serde_json::to_value(&doc).map_err(|e| format!("sketch3d: {e}"))?;
1159 if let Some(map) = sketch.as_object_mut() {
1160 map.insert(
1161 "diagnostics".into(),
1162 serde_json::to_value(&diagnostics).map_err(|e| format!("sketch3d: {e}"))?,
1163 );
1164 }
1165 Ok(serde_json::json!({ "sketch": sketch }))
1166}
1167
1168pub fn solve_sketch3d_from_json(request: &str) -> Result<String, String> {
1170 let request: SolveSketch3dRequest =
1171 serde_json::from_str(request).map_err(|e| format!("sketch3d: invalid request: {e}"))?;
1172 let response = solve_sketch3d(&request)?;
1173 serde_json::to_string(&response).map_err(|e| format!("sketch3d: {e}"))
1174}
1175