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
310
311
312fn mint_next_id<'a>(ids: impl Iterator<Item = &'a Value>) -> Value {
313 crate::mint_sketch_id(ids, |_| false)
314}
315
316#[derive(Clone, Copy, Debug)]
324pub struct Sketch3dConstraintType {
325 pub name: &'static str,
326 pub arity: usize,
328 pub dimensional: bool,
330 pub meaning: &'static str,
331}
332
333pub const SKETCH3D_CONSTRAINT_TYPES: &[Sketch3dConstraintType] = &[
337 Sketch3dConstraintType { name: "fixed", arity: 1, dimensional: false, meaning: "ground a point where it is" },
338 Sketch3dConstraintType { name: "coincident", arity: 2, dimensional: false, meaning: "two points touch" },
339 Sketch3dConstraintType { name: "distance", arity: 2, dimensional: true, meaning: "distance between two points (a line's length)" },
340 Sketch3dConstraintType { name: "onLine", arity: 3, dimensional: false, meaning: "[p, a0, a1]: point on the infinite line a0→a1" },
341 Sketch3dConstraintType { name: "lineDistance", arity: 3, dimensional: true, meaning: "[p, a0, a1]: distance from the point to the infinite line" },
342 Sketch3dConstraintType { name: "onPlane", arity: 3, dimensional: false, meaning: "[p, origin, normalTip]: point on the plane" },
343 Sketch3dConstraintType { name: "planeDistance", arity: 3, dimensional: true, meaning: "[p, origin, normalTip]: signed height of the point above the plane" },
344 Sketch3dConstraintType { name: "parallel", arity: 4, dimensional: false, meaning: "[a0, a1, b0, b1]: the two directions are parallel (either sense)" },
345 Sketch3dConstraintType { name: "perpendicular", arity: 4, dimensional: false, meaning: "[a0, a1, b0, b1]: the two directions are perpendicular" },
346 Sketch3dConstraintType { name: "angle", arity: 4, dimensional: true, meaning: "[a0, a1, b0, b1]: angle in degrees between the two directions" },
347 Sketch3dConstraintType { name: "alongX", arity: 2, dimensional: false, meaning: "the line runs along world X" },
348 Sketch3dConstraintType { name: "alongY", arity: 2, dimensional: false, meaning: "the line runs along world Y" },
349 Sketch3dConstraintType { name: "alongZ", arity: 2, dimensional: false, meaning: "the line runs along world Z" },
350 Sketch3dConstraintType { name: "equal", arity: 4, dimensional: false, meaning: "[a0, a1, b0, b1]: equal lengths (an arc's radius is center→start)" },
351 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" },
352];
353
354pub fn sketch3d_constraint_type(name: &str) -> Option<&'static Sketch3dConstraintType> {
356 SKETCH3D_CONSTRAINT_TYPES.iter().find(|t| t.name == name)
357}
358
359#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
364pub struct Sketch3dSolveOptions {
365 #[serde(default = "default_tolerance")]
368 pub tolerance: f64,
369 #[serde(default = "default_max_iterations")]
371 pub max_iterations: usize,
372}
373
374impl Default for Sketch3dSolveOptions {
375 fn default() -> Self {
376 Self {
377 tolerance: default_tolerance(),
378 max_iterations: default_max_iterations(),
379 }
380 }
381}
382
383#[derive(Debug, Deserialize)]
385pub struct SolveSketch3dRequest {
386 pub sketch: Value,
387 #[serde(default)]
388 pub tolerance: Option<f64>,
389 #[serde(default)]
390 pub max_iterations: Option<usize>,
391}
392
393#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
398pub struct Sketch3dDiagnostics {
399 #[serde(default)]
400 pub dof: i64,
401 #[serde(default)]
402 pub rank: i64,
403 #[serde(default)]
404 pub unknowns: i64,
405 #[serde(default)]
406 pub equations: i64,
407 #[serde(default)]
408 pub redundant: i64,
409 #[serde(default)]
411 pub status: String,
412 #[serde(default)]
413 pub conflicting: bool,
414 #[serde(default, rename = "conflictingConstraints")]
417 pub conflicting_constraints: Vec<String>,
418 #[serde(default, rename = "pointMobility")]
420 pub point_mobility: std::collections::BTreeMap<String, String>,
421 #[serde(default, rename = "geometryMobility")]
422 pub geometry_mobility: std::collections::BTreeMap<String, String>,
423 #[serde(default)]
425 pub iterations: i64,
426 #[serde(default, rename = "maxResidual")]
428 pub max_residual: f64,
429}
430
431impl Sketch3dDiagnostics {
432 pub fn point_movable(&self, id: &Value) -> Option<bool> {
433 self.point_mobility.get(&id_key(id)).map(|v| v == "movable")
434 }
435
436 pub fn geometry_movable(&self, id: &Value) -> Option<bool> {
437 self.geometry_mobility.get(&id_key(id)).map(|v| v == "movable")
438 }
439
440 pub fn constraint_conflicting(&self, id: &Value) -> bool {
441 if self.conflicting_constraints.is_empty() {
442 return false;
443 }
444 let key = id_key(id);
445 self.conflicting_constraints.iter().any(|c| *c == key)
446 }
447}
448
449#[derive(Clone, Copy, Debug)]
457enum Atom {
458 Coincide(usize, usize),
460 Distance(usize, usize, f64),
462 Lateral(usize, usize, usize),
465 LineDistance(usize, usize, usize, f64),
467 PlaneOffset(usize, usize, usize, f64),
469 DirCross(usize, usize, usize, usize),
471 DirDot(usize, usize, usize, usize, f64),
473 AxisCross(usize, usize, Vec3),
475 EqualLength(usize, usize, usize, usize),
477 TangentTouch(usize, usize, usize, usize),
480 InArcPlane(usize, usize, usize, usize, usize),
483}
484
485impl Atom {
486 fn rows(&self) -> usize {
487 match self {
488 Atom::Coincide(..) | Atom::Lateral(..) | Atom::DirCross(..) | Atom::AxisCross(..) => 3,
489 Atom::Distance(..)
490 | Atom::LineDistance(..)
491 | Atom::PlaneOffset(..)
492 | Atom::DirDot(..)
493 | Atom::EqualLength(..)
494 | Atom::TangentTouch(..)
495 | Atom::InArcPlane(..) => 1,
496 }
497 }
498
499 fn eff_rows(&self) -> usize {
500 match self {
501 Atom::Coincide(..) => 3,
502 Atom::Lateral(..) | Atom::DirCross(..) | Atom::AxisCross(..) => 2,
503 _ => 1,
504 }
505 }
506
507 fn eval(&self, p: &[Vec3], dir_eps: f64, out: &mut Vec<f64>) {
508 let unit = |v: Vec3| {
509 let len = v.length();
510 if len > dir_eps {
511 v.scale(1.0 / len)
512 } else {
513 v.scale(1.0 / dir_eps)
514 }
515 };
516 match *self {
517 Atom::Coincide(a, b) => {
518 let d = p[a].sub(p[b]);
519 out.extend([d.x, d.y, d.z]);
520 }
521 Atom::Distance(a, b, target) => out.push(p[a].sub(p[b]).length() - target),
522 Atom::Lateral(pt, o, q) => {
523 let d = unit(p[q].sub(p[o]));
524 let v = p[pt].sub(p[o]);
525 let lateral = v.sub(d.scale(d.dot(v)));
526 out.extend([lateral.x, lateral.y, lateral.z]);
527 }
528 Atom::LineDistance(pt, o, q, target) => {
529 let d = unit(p[q].sub(p[o]));
530 let v = p[pt].sub(p[o]);
531 let lateral = v.sub(d.scale(d.dot(v)));
532 out.push(lateral.length() - target);
533 }
534 Atom::PlaneOffset(pt, o, t, target) => {
535 let n = unit(p[t].sub(p[o]));
536 out.push(n.dot(p[pt].sub(p[o])) - target);
537 }
538 Atom::DirCross(a0, a1, b0, b1) => {
539 let c = unit(p[a1].sub(p[a0])).cross(unit(p[b1].sub(p[b0])));
540 out.extend([c.x, c.y, c.z]);
541 }
542 Atom::DirDot(a0, a1, b0, b1, target) => {
543 out.push(unit(p[a1].sub(p[a0])).dot(unit(p[b1].sub(p[b0]))) - target);
544 }
545 Atom::AxisCross(a0, a1, axis) => {
546 let c = unit(p[a1].sub(p[a0])).cross(axis);
547 out.extend([c.x, c.y, c.z]);
548 }
549 Atom::EqualLength(a0, a1, b0, b1) => {
550 out.push(p[a1].sub(p[a0]).length() - p[b1].sub(p[b0]).length());
551 }
552 Atom::TangentTouch(a0, a1, c, t) => {
553 out.push(unit(p[t].sub(p[c])).dot(unit(p[a1].sub(p[a0]))));
554 }
555 Atom::InArcPlane(a0, a1, c, s, e) => {
556 let n = unit(p[s].sub(p[c]).cross(p[e].sub(p[c])));
557 out.push(unit(p[a1].sub(p[a0])).dot(n));
558 }
559 }
560 }
561}
562
563struct Prepared {
568 coords: Vec<Vec3>,
570 free: Vec<usize>,
572 atoms: Vec<Atom>,
573 atom_owner: Vec<Option<usize>>,
575 m: usize,
576 effective_rows: usize,
577 scale: f64,
578 tight: f64,
579}
580
581fn prepare(doc: &Sketch3dDoc, options: &Sketch3dSolveOptions) -> Result<Prepared, String> {
582 let mut index: HashMap<String, usize> = HashMap::with_capacity(doc.points.len());
583 let mut coords: Vec<Vec3> = Vec::with_capacity(doc.points.len());
584 let mut fixed: Vec<bool> = Vec::with_capacity(doc.points.len());
585 for (i, point) in doc.points.iter().enumerate() {
586 if !(point.x.is_finite() && point.y.is_finite() && point.z.is_finite()) {
587 return Err(format!("sketch3d: point {} has a non-finite coordinate", id_key(&point.id)));
588 }
589 index.insert(id_key(&point.id), i);
591 coords.push(point.position());
592 fixed.push(point.fixed);
593 }
594 let lookup = |what: &str, id: &Value| -> Result<usize, String> {
595 index
596 .get(&id_key(id))
597 .copied()
598 .ok_or_else(|| format!("sketch3d: {what} references unknown point {}", id_key(id)))
599 };
600
601 let mut atoms: Vec<Atom> = Vec::new();
602 let mut atom_owner: Vec<Option<usize>> = Vec::new();
603
604 for (ci, constraint) in doc.constraints.iter().enumerate() {
606 let Some(ctype) = constraint.ctype() else {
607 return Err(format!("sketch3d: constraint {ci} has no type"));
608 };
609 let spec = sketch3d_constraint_type(ctype)
610 .ok_or_else(|| format!("sketch3d: unknown constraint type '{ctype}'"))?;
611 let ids = constraint.points();
612 if ids.len() < spec.arity {
613 return Err(format!(
614 "sketch3d: constraint '{ctype}' needs {} points, got {}",
615 spec.arity,
616 ids.len()
617 ));
618 }
619 let what = format!("constraint '{ctype}'");
620 let mut idx = Vec::with_capacity(spec.arity);
621 for id in &ids[..spec.arity] {
622 idx.push(lookup(&what, id)?);
623 }
624 let value = || -> Result<f64, String> {
625 constraint
626 .value()
627 .filter(|v| v.is_finite())
628 .ok_or_else(|| format!("sketch3d: constraint '{ctype}' needs a finite value"))
629 };
630 let mut push = |atom: Atom| {
631 atoms.push(atom);
632 atom_owner.push(Some(ci));
633 };
634 match ctype {
635 "fixed" => fixed[idx[0]] = true,
636 "coincident" => push(Atom::Coincide(idx[0], idx[1])),
637 "distance" => {
638 let target = value()?;
639 if target.abs() <= 0.0 {
640 push(Atom::Coincide(idx[0], idx[1]));
641 } else {
642 push(Atom::Distance(idx[0], idx[1], target.abs()));
643 }
644 }
645 "onLine" => push(Atom::Lateral(idx[0], idx[1], idx[2])),
646 "lineDistance" => {
647 let target = value()?;
648 if target.abs() <= 0.0 {
649 push(Atom::Lateral(idx[0], idx[1], idx[2]));
650 } else {
651 push(Atom::LineDistance(idx[0], idx[1], idx[2], target.abs()));
652 }
653 }
654 "onPlane" => push(Atom::PlaneOffset(idx[0], idx[1], idx[2], 0.0)),
655 "planeDistance" => push(Atom::PlaneOffset(idx[0], idx[1], idx[2], value()?)),
656 "parallel" => push(Atom::DirCross(idx[0], idx[1], idx[2], idx[3])),
657 "perpendicular" => push(Atom::DirDot(idx[0], idx[1], idx[2], idx[3], 0.0)),
658 "angle" => {
659 let target = value()?.to_radians().cos();
660 push(Atom::DirDot(idx[0], idx[1], idx[2], idx[3], target));
661 }
662 "alongX" => push(Atom::AxisCross(idx[0], idx[1], Vec3::new(1.0, 0.0, 0.0))),
663 "alongY" => push(Atom::AxisCross(idx[0], idx[1], Vec3::new(0.0, 1.0, 0.0))),
664 "alongZ" => push(Atom::AxisCross(idx[0], idx[1], Vec3::new(0.0, 0.0, 1.0))),
665 "equal" => push(Atom::EqualLength(idx[0], idx[1], idx[2], idx[3])),
666 "tangent" => {
667 let (a0, a1, c, s, e) = (idx[0], idx[1], idx[2], idx[3], idx[4]);
670 let touch = if s == a0 || s == a1 {
671 s
672 } else if e == a0 || e == a1 {
673 e
674 } else {
675 let near = |t: usize| {
676 coords[t]
677 .sub(coords[a0])
678 .length()
679 .min(coords[t].sub(coords[a1]).length())
680 };
681 if near(s) <= near(e) {
682 s
683 } else {
684 e
685 }
686 };
687 push(Atom::TangentTouch(a0, a1, c, touch));
688 push(Atom::InArcPlane(a0, a1, c, s, e));
689 }
690 other => return Err(format!("sketch3d: unknown constraint type '{other}'")),
691 }
692 }
693
694 for geometry in &doc.geometries {
696 match geometry.geom_type.as_str() {
697 "arc" if geometry.points.len() >= 3 => {
698 let what = "arc geometry";
699 let c = lookup(what, &geometry.points[0])?;
700 let s = lookup(what, &geometry.points[1])?;
701 let e = lookup(what, &geometry.points[2])?;
702 atoms.push(Atom::EqualLength(c, s, c, e));
703 atom_owner.push(None);
704 }
705 "bezier" if geometry.points.len() >= 7 => {
706 let what = "bezier geometry";
707 let ids = &geometry.points;
708 let span_count = (ids.len() - 1) / 3;
709 let last_anchor = span_count * 3;
710 let mut i = 3usize;
711 while i < last_anchor {
712 let prev = lookup(what, &ids[i - 1])?;
713 let anchor = lookup(what, &ids[i])?;
714 let next = lookup(what, &ids[i + 1])?;
715 if prev != anchor && next != anchor && prev != next {
716 atoms.push(Atom::Lateral(anchor, prev, next));
717 atom_owner.push(None);
718 }
719 i += 3;
720 }
721 }
722 _ => {}
723 }
724 }
725
726 let free: Vec<usize> = (0..coords.len()).filter(|&i| !fixed[i]).collect();
727 let m: usize = atoms.iter().map(Atom::rows).sum();
728 let effective_rows: usize = atoms.iter().map(Atom::eff_rows).sum();
729
730 let mut scale = 1.0f64;
731 for c in &coords {
732 scale = scale.max(c.x.abs()).max(c.y.abs()).max(c.z.abs());
733 }
734 let tight = options.tolerance * scale;
735 Ok(Prepared {
736 coords,
737 free,
738 atoms,
739 atom_owner,
740 m,
741 effective_rows,
742 scale,
743 tight,
744 })
745}
746
747fn apply_delta(base: &[Vec3], free: &[usize], delta: &[f64], out: &mut Vec<Vec3>) {
752 out.clear();
753 out.extend_from_slice(base);
754 for (k, &i) in free.iter().enumerate() {
755 out[i] = base[i].add(Vec3::new(delta[3 * k], delta[3 * k + 1], delta[3 * k + 2]));
756 }
757}
758
759fn eval_residuals(
760 atoms: &[Atom],
761 base: &[Vec3],
762 free: &[usize],
763 delta: &[f64],
764 dir_eps: f64,
765 scratch: &mut Vec<Vec3>,
766 out: &mut Vec<f64>,
767) {
768 apply_delta(base, free, delta, scratch);
769 out.clear();
770 for atom in atoms {
771 atom.eval(scratch, dir_eps, out);
772 }
773}
774
775fn finite_diff_jacobian(
776 atoms: &[Atom],
777 base: &[Vec3],
778 free: &[usize],
779 h: f64,
780 dir_eps: f64,
781 m: usize,
782) -> Vec<Vec<f64>> {
783 let n = 3 * free.len();
784 let mut jac = vec![vec![0.0f64; n]; m];
785 if n == 0 || m == 0 {
786 return jac;
787 }
788 let mut delta = vec![0.0f64; n];
789 let mut scratch: Vec<Vec3> = Vec::with_capacity(base.len());
790 let mut rp: Vec<f64> = Vec::with_capacity(m);
791 let mut rm: Vec<f64> = Vec::with_capacity(m);
792 let inv = 1.0 / (2.0 * h);
793 for col in 0..n {
794 delta[col] = h;
795 eval_residuals(atoms, base, free, &delta, dir_eps, &mut scratch, &mut rp);
796 delta[col] = -h;
797 eval_residuals(atoms, base, free, &delta, dir_eps, &mut scratch, &mut rm);
798 delta[col] = 0.0;
799 for i in 0..m {
800 jac[i][col] = (rp[i] - rm[i]) * inv;
801 }
802 }
803 jac
804}
805
806fn lm_core(prep: &Prepared, coords: &mut Vec<Vec3>, max_iterations: usize) -> Result<usize, String> {
810 let atoms = &prep.atoms;
811 let free = &prep.free;
812 let n = 3 * free.len();
813 let m = prep.m;
814 let scale = prep.scale;
815 let dir_eps = DIR_EPS_REL * scale;
816 let h = FD_STEP * scale;
817 let step_tol = STEP_REL_TOL * scale;
818 let tight = prep.tight;
819
820 let max_abs = |v: &[f64]| v.iter().fold(0.0f64, |acc, &x| acc.max(x.abs()));
821 let sum_sq = |v: &[f64]| v.iter().fold(0.0f64, |acc, &x| acc + x * x);
822
823 let zeros = vec![0.0f64; n];
824 let mut scratch: Vec<Vec3> = Vec::with_capacity(coords.len());
825 let mut r: Vec<f64> = Vec::with_capacity(m);
826 eval_residuals(atoms, coords, free, &zeros, dir_eps, &mut scratch, &mut r);
827 if !max_abs(&r).is_finite() {
828 return Err("sketch3d: non-finite residual at the initial coordinates".into());
829 }
830
831 let mut iterations = 0usize;
832 let mut s = sum_sq(&r);
833 let mut best_coords = coords.clone();
834 let mut best_max = max_abs(&r);
835 let mut lambda = -1.0f64;
836
837 if n > 0 && m > 0 {
838 'outer: for _iter in 0..max_iterations {
839 if best_max <= tight {
840 break;
841 }
842 let jac = finite_diff_jacobian(atoms, coords, free, h, dir_eps, m);
843 let mut hm = vec![vec![0.0f64; n]; n];
844 let mut g = vec![0.0f64; n];
845 for i in 0..m {
846 let row = &jac[i];
847 let ri = r[i];
848 for a in 0..n {
849 let ja = row[a];
850 if ja == 0.0 {
851 continue;
852 }
853 g[a] += ja * ri;
854 for b in a..n {
855 hm[a][b] += ja * row[b];
856 }
857 }
858 }
859 for a in 0..n {
860 for b in (a + 1)..n {
861 hm[b][a] = hm[a][b];
862 }
863 }
864 let mut max_diag = 0.0f64;
865 for a in 0..n {
866 max_diag = max_diag.max(hm[a][a]);
867 }
868 if !(max_diag > 0.0) {
869 break;
870 }
871 if lambda < 0.0 {
872 lambda = 1e-9 * max_diag;
873 }
874 let lambda_ceiling = 1e12 * max_diag;
875
876 let mut stepped = false;
877 let mut step_small = false;
878 for _try in 0..MAX_DAMPING {
879 let mut a_mat = hm.clone();
880 for d in 0..n {
881 a_mat[d][d] += lambda;
882 }
883 let mut delta: Vec<f64> = g.iter().map(|v| -v).collect();
884 if !solve_spd(&mut a_mat, &mut delta) {
885 lambda *= LAMBDA_UP;
886 if lambda > lambda_ceiling {
887 break;
888 }
889 continue;
890 }
891 let mut r_trial: Vec<f64> = Vec::with_capacity(m);
892 eval_residuals(atoms, coords, free, &delta, dir_eps, &mut scratch, &mut r_trial);
893 let s_trial = sum_sq(&r_trial);
894 if s_trial.is_finite() && s_trial < s {
895 let step_norm = sum_sq(&delta).sqrt();
896 apply_delta(coords, free, &delta, &mut scratch);
897 coords.copy_from_slice(&scratch);
898 eval_residuals(atoms, coords, free, &zeros, dir_eps, &mut scratch, &mut r);
899 s = sum_sq(&r);
900 let m_now = max_abs(&r);
901 if m_now < best_max {
902 best_max = m_now;
903 best_coords.copy_from_slice(coords);
904 }
905 lambda = (lambda * LAMBDA_DOWN).max(1e-12 * max_diag);
906 iterations += 1;
907 stepped = true;
908 step_small = step_norm <= step_tol;
909 break;
910 }
911 lambda *= LAMBDA_UP;
912 if lambda > lambda_ceiling {
913 break;
914 }
915 }
916 if !stepped || step_small {
917 break 'outer;
918 }
919 }
920 }
921 coords.copy_from_slice(&best_coords);
922 Ok(iterations)
923}
924
925fn rank_and_nullspace(mut a: Vec<Vec<f64>>, m: usize, n: usize) -> (usize, Vec<Vec<f64>>) {
928 if n == 0 {
929 return (0, Vec::new());
930 }
931 let mut maxabs = 0.0f64;
932 for row in &a {
933 for &v in row {
934 maxabs = maxabs.max(v.abs());
935 }
936 }
937 let tol = if maxabs > 0.0 {
938 RANK_REL_TOL * maxabs
939 } else {
940 RANK_REL_TOL
941 };
942 let mut pivot_cols: Vec<usize> = Vec::new();
943 row_reduce(&mut a, m, n, tol, |col| pivot_cols.push(col));
944 let rank = pivot_cols.len();
945 let mut is_pivot = vec![false; n];
946 for &pc in &pivot_cols {
947 is_pivot[pc] = true;
948 }
949 let mut basis: Vec<Vec<f64>> = Vec::new();
950 for free_col in 0..n {
951 if is_pivot[free_col] {
952 continue;
953 }
954 let mut v = vec![0.0f64; n];
955 v[free_col] = 1.0;
956 for (i, &pc) in pivot_cols.iter().enumerate() {
957 v[pc] = -a[i][free_col];
958 }
959 basis.push(v);
960 }
961 (rank, basis)
962}
963
964pub fn solve_sketch3d_doc(
971 doc: &mut Sketch3dDoc,
972 options: &Sketch3dSolveOptions,
973) -> Result<Sketch3dDiagnostics, String> {
974 let prep = prepare(doc, options)?;
975 let mut coords = prep.coords.clone();
976 let iterations = lm_core(&prep, &mut coords, options.max_iterations)?;
977
978 let n = 3 * prep.free.len();
979 let m = prep.m;
980 let dir_eps = DIR_EPS_REL * prep.scale;
981 let zeros = vec![0.0f64; n];
982 let mut scratch: Vec<Vec3> = Vec::with_capacity(coords.len());
983 let mut r: Vec<f64> = Vec::with_capacity(m);
984 eval_residuals(&prep.atoms, &coords, &prep.free, &zeros, dir_eps, &mut scratch, &mut r);
985
986 let jac = finite_diff_jacobian(&prep.atoms, &coords, &prep.free, FD_STEP * prep.scale, dir_eps, m);
987 let row_norms: Vec<f64> = jac
991 .iter()
992 .map(|row| row.iter().fold(0.0f64, |acc, &v| acc + v * v).sqrt())
993 .collect();
994 let max_row_norm = row_norms.iter().fold(0.0f64, |a, &v| a.max(v));
995 let norm_floor = if max_row_norm > 0.0 { 1e-9 * max_row_norm } else { 1.0 };
996 let scaled: Vec<f64> = r
997 .iter()
998 .zip(row_norms.iter())
999 .map(|(&v, &norm)| v / norm.max(norm_floor))
1000 .collect();
1001 let max_residual = scaled.iter().fold(0.0f64, |acc, &v| acc.max(v.abs()));
1002
1003 let (rank, basis) = rank_and_nullspace(jac, m, n);
1004 let dof = n - rank;
1005 let redundant = prep.effective_rows.saturating_sub(rank);
1006
1007 let conflict_tol = CONFLICT_REL_TOL * prep.scale;
1010 let mut flagged = vec![false; doc.constraints.len()];
1011 let mut row = 0usize;
1012 for (ai, atom) in prep.atoms.iter().enumerate() {
1013 let rows = atom.rows();
1014 let violated = scaled[row..row + rows].iter().any(|v| v.abs() > conflict_tol);
1015 if violated {
1016 if let Some(owner) = prep.atom_owner[ai] {
1017 flagged[owner] = true;
1018 }
1019 }
1020 row += rows;
1021 }
1022 let mut conflicting_constraints: Vec<String> = Vec::new();
1023 for (ci, &hit) in flagged.iter().enumerate() {
1024 if hit {
1025 if let Some(id) = doc.constraints[ci].id() {
1026 conflicting_constraints.push(id_key(id));
1027 }
1028 }
1029 }
1030 if max_residual > conflict_tol && conflicting_constraints.is_empty() {
1035 let mut row = 0usize;
1036 for (ai, atom) in prep.atoms.iter().enumerate() {
1037 let rows = atom.rows();
1038 let violated = scaled[row..row + rows].iter().any(|v| v.abs() > conflict_tol);
1039 row += rows;
1040 if !violated || prep.atom_owner[ai].is_some() {
1041 continue;
1042 }
1043 for (ci, constraint) in doc.constraints.iter().enumerate() {
1044 if flagged[ci] {
1045 continue;
1046 }
1047 if let Some(id) = constraint.id() {
1048 flagged[ci] = true;
1049 conflicting_constraints.push(id_key(id));
1050 }
1051 }
1052 }
1053 }
1054 let conflicting = !conflicting_constraints.is_empty();
1055
1056 let status = if redundant > 0 {
1057 "over"
1058 } else if dof > 0 {
1059 "under"
1060 } else {
1061 "well"
1062 };
1063
1064 let basis_max: Vec<f64> = basis
1067 .iter()
1068 .map(|v| v.iter().fold(0.0f64, |a, &x| a.max(x.abs())).max(1e-300))
1069 .collect();
1070 let mut col_of: HashMap<usize, usize> = HashMap::with_capacity(prep.free.len());
1071 for (k, &i) in prep.free.iter().enumerate() {
1072 col_of.insert(i, k);
1073 }
1074 let mut point_mobility = std::collections::BTreeMap::new();
1075 for (i, point) in doc.points.iter().enumerate() {
1076 let movable = match col_of.get(&i) {
1077 None => false,
1078 Some(&k) => basis.iter().enumerate().any(|(b, v)| {
1079 let c = 3 * k;
1080 let comp = ((v[c] / basis_max[b]).powi(2)
1081 + (v[c + 1] / basis_max[b]).powi(2)
1082 + (v[c + 2] / basis_max[b]).powi(2))
1083 .sqrt();
1084 comp > MOBILITY_TOL
1085 }),
1086 };
1087 point_mobility.insert(
1088 id_key(&point.id),
1089 (if movable { "movable" } else { "locked" }).to_string(),
1090 );
1091 }
1092 let mut geometry_mobility = std::collections::BTreeMap::new();
1093 for geometry in &doc.geometries {
1094 let movable = geometry
1095 .points
1096 .iter()
1097 .any(|pid| point_mobility.get(&id_key(pid)).map(String::as_str) == Some("movable"));
1098 geometry_mobility.insert(
1099 id_key(&geometry.id),
1100 (if movable { "movable" } else { "locked" }).to_string(),
1101 );
1102 }
1103
1104 for (i, point) in doc.points.iter_mut().enumerate() {
1106 point.x = coords[i].x;
1107 point.y = coords[i].y;
1108 point.z = coords[i].z;
1109 }
1110
1111 Ok(Sketch3dDiagnostics {
1112 dof: dof as i64,
1113 rank: rank as i64,
1114 unknowns: n as i64,
1115 equations: m as i64,
1116 redundant: redundant as i64,
1117 status: status.into(),
1118 conflicting,
1119 conflicting_constraints,
1120 point_mobility,
1121 geometry_mobility,
1122 iterations: iterations as i64,
1123 max_residual,
1124 })
1125}
1126
1127pub fn solve_sketch3d(request: &SolveSketch3dRequest) -> Result<Value, String> {
1130 let mut doc: Sketch3dDoc = serde_json::from_value(request.sketch.clone())
1131 .map_err(|e| format!("sketch3d: invalid sketch document: {e}"))?;
1132 let options = Sketch3dSolveOptions {
1133 tolerance: request.tolerance.unwrap_or_else(default_tolerance),
1134 max_iterations: request.max_iterations.unwrap_or_else(default_max_iterations),
1135 };
1136 let diagnostics = solve_sketch3d_doc(&mut doc, &options)?;
1137 let mut sketch = serde_json::to_value(&doc).map_err(|e| format!("sketch3d: {e}"))?;
1138 if let Some(map) = sketch.as_object_mut() {
1139 map.insert(
1140 "diagnostics".into(),
1141 serde_json::to_value(&diagnostics).map_err(|e| format!("sketch3d: {e}"))?,
1142 );
1143 }
1144 Ok(serde_json::json!({ "sketch": sketch }))
1145}
1146
1147pub fn solve_sketch3d_from_json(request: &str) -> Result<String, String> {
1149 let request: SolveSketch3dRequest =
1150 serde_json::from_str(request).map_err(|e| format!("sketch3d: invalid request: {e}"))?;
1151 let response = solve_sketch3d(&request)?;
1152 serde_json::to_string(&response).map_err(|e| format!("sketch3d: {e}"))
1153}
1154