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 Sketch3dConstraintType { name: "midpoint", arity: 3, dimensional: false, meaning: "[p, a0, a1]: the point is halfway between a0 and a1" },
353 Sketch3dConstraintType { name: "onArc", arity: 4, dimensional: false, meaning: "[p, center, start, end]: the point is on the arc's circle, in its plane, at its current radius" },
354 Sketch3dConstraintType { name: "onCircle", arity: 3, dimensional: false, meaning: "[p, center, axisTip]: the point is on the circle whose axis is center->axisTip and whose radius is that length" },
355 Sketch3dConstraintType { name: "symmetric", arity: 4, dimensional: false, meaning: "[a, b, origin, normalTip]: a and b mirror each other in the plane" },
356 Sketch3dConstraintType { name: "onCurve", arity: 5, dimensional: false, meaning: "[p, c0, c1, ...]: the point is on the cubic bezier chain with these 3n + 1 control points" },
357];
358
359pub fn sketch3d_constraint_type(name: &str) -> Option<&'static Sketch3dConstraintType> {
361 SKETCH3D_CONSTRAINT_TYPES.iter().find(|t| t.name == name)
362}
363
364#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
369pub struct Sketch3dSolveOptions {
370 #[serde(default = "default_tolerance")]
373 pub tolerance: f64,
374 #[serde(default = "default_max_iterations")]
376 pub max_iterations: usize,
377}
378
379impl Default for Sketch3dSolveOptions {
380 fn default() -> Self {
381 Self {
382 tolerance: default_tolerance(),
383 max_iterations: default_max_iterations(),
384 }
385 }
386}
387
388#[derive(Debug, Deserialize)]
390pub struct SolveSketch3dRequest {
391 pub sketch: Value,
392 #[serde(default)]
393 pub tolerance: Option<f64>,
394 #[serde(default)]
395 pub max_iterations: Option<usize>,
396}
397
398#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
403pub struct Sketch3dDiagnostics {
404 #[serde(default)]
405 pub dof: i64,
406 #[serde(default)]
407 pub rank: i64,
408 #[serde(default)]
409 pub unknowns: i64,
410 #[serde(default)]
411 pub equations: i64,
412 #[serde(default)]
413 pub redundant: i64,
414 #[serde(default)]
416 pub status: String,
417 #[serde(default)]
418 pub conflicting: bool,
419 #[serde(default, rename = "conflictingConstraints")]
422 pub conflicting_constraints: Vec<String>,
423 #[serde(default, rename = "pointMobility")]
425 pub point_mobility: std::collections::BTreeMap<String, String>,
426 #[serde(default, rename = "geometryMobility")]
427 pub geometry_mobility: std::collections::BTreeMap<String, String>,
428 #[serde(default)]
430 pub iterations: i64,
431 #[serde(default, rename = "maxResidual")]
433 pub max_residual: f64,
434}
435
436impl Sketch3dDiagnostics {
437 pub fn point_movable(&self, id: &Value) -> Option<bool> {
438 self.point_mobility.get(&id_key(id)).map(|v| v == "movable")
439 }
440
441 pub fn geometry_movable(&self, id: &Value) -> Option<bool> {
442 self.geometry_mobility.get(&id_key(id)).map(|v| v == "movable")
443 }
444
445 pub fn constraint_conflicting(&self, id: &Value) -> bool {
446 if self.conflicting_constraints.is_empty() {
447 return false;
448 }
449 let key = id_key(id);
450 self.conflicting_constraints.iter().any(|c| *c == key)
451 }
452}
453
454#[derive(Clone, Debug)]
462enum Atom {
463 Coincide(usize, usize),
465 Distance(usize, usize, f64),
467 Lateral(usize, usize, usize),
470 LineDistance(usize, usize, usize, f64),
472 PlaneOffset(usize, usize, usize, f64),
474 DirCross(usize, usize, usize, usize),
476 DirDot(usize, usize, usize, usize, f64),
478 AxisCross(usize, usize, Vec3),
480 EqualLength(usize, usize, usize, usize),
482 TangentTouch(usize, usize, usize, usize),
485 InArcPlane(usize, usize, usize, usize, usize),
488 Midpoint(usize, usize, usize),
490 Symmetric(usize, usize, usize, usize),
493 OnBezier(usize, Box<[usize]>),
496}
497
498fn nearest_on_bezier(controls: &[Vec3], target: Vec3) -> Vec3 {
501 const SCAN: usize = 16;
502 let value = |c: &[Vec3], t: f64| {
503 let u = 1.0 - t;
504 c[0].scale(u * u * u).add(c[1].scale(3.0 * u * u * t)).add(c[2].scale(3.0 * u * t * t)).add(c[3].scale(t * t * t))
505 };
506 let first = |c: &[Vec3], t: f64| {
507 let u = 1.0 - t;
508 c[1].sub(c[0]).scale(3.0 * u * u).add(c[2].sub(c[1]).scale(6.0 * u * t)).add(c[3].sub(c[2]).scale(3.0 * t * t))
509 };
510 let second = |c: &[Vec3], t: f64| {
511 let d1 = c[2].sub(c[1].scale(2.0)).add(c[0]);
512 let d2 = c[3].sub(c[2].scale(2.0)).add(c[1]);
513 d1.scale(6.0 * (1.0 - t)).add(d2.scale(6.0 * t))
514 };
515 let mut best: Option<(f64, usize, f64)> = None;
516 for span in 0..(controls.len() - 1) / 3 {
517 let c = &controls[span * 3..span * 3 + 4];
518 for i in 0..=SCAN {
519 let t = i as f64 / SCAN as f64;
520 let d = value(c, t).sub(target).length();
521 if best.map_or(true, |(bd, _, _)| d < bd) {
522 best = Some((d, span, t));
523 }
524 }
525 }
526 let Some((_, span, mut t)) = best else { return target };
527 let c = &controls[span * 3..span * 3 + 4];
528 for _ in 0..8 {
529 let offset = value(c, t).sub(target);
530 let d1 = first(c, t);
531 let denominator = d1.dot(d1) + offset.dot(second(c, t));
532 if denominator.abs() <= 1e-14 {
533 break;
534 }
535 t = (t - offset.dot(d1) / denominator).clamp(0.0, 1.0);
536 }
537 value(c, t)
538}
539
540impl Atom {
541 fn rows(&self) -> usize {
542 match self {
543 Atom::Coincide(..) | Atom::Lateral(..) | Atom::DirCross(..) | Atom::AxisCross(..) | Atom::Midpoint(..) | Atom::OnBezier(..) => 3,
544 Atom::Symmetric(..) => 4,
545 Atom::Distance(..)
546 | Atom::LineDistance(..)
547 | Atom::PlaneOffset(..)
548 | Atom::DirDot(..)
549 | Atom::EqualLength(..)
550 | Atom::TangentTouch(..)
551 | Atom::InArcPlane(..) => 1,
552 }
553 }
554
555 fn eff_rows(&self) -> usize {
556 match self {
557 Atom::Coincide(..) | Atom::Midpoint(..) | Atom::Symmetric(..) => 3,
558 Atom::Lateral(..) | Atom::DirCross(..) | Atom::AxisCross(..) | Atom::OnBezier(..) => 2,
559 _ => 1,
560 }
561 }
562
563 fn eval(&self, p: &[Vec3], dir_eps: f64, out: &mut Vec<f64>) {
564 let unit = |v: Vec3| {
565 let len = v.length();
566 if len > dir_eps {
567 v.scale(1.0 / len)
568 } else {
569 v.scale(1.0 / dir_eps)
570 }
571 };
572 match *self {
573 Atom::Coincide(a, b) => {
574 let d = p[a].sub(p[b]);
575 out.extend([d.x, d.y, d.z]);
576 }
577 Atom::Distance(a, b, target) => out.push(p[a].sub(p[b]).length() - target),
578 Atom::Lateral(pt, o, q) => {
579 let d = unit(p[q].sub(p[o]));
580 let v = p[pt].sub(p[o]);
581 let lateral = v.sub(d.scale(d.dot(v)));
582 out.extend([lateral.x, lateral.y, lateral.z]);
583 }
584 Atom::LineDistance(pt, o, q, target) => {
585 let d = unit(p[q].sub(p[o]));
586 let v = p[pt].sub(p[o]);
587 let lateral = v.sub(d.scale(d.dot(v)));
588 out.push(lateral.length() - target);
589 }
590 Atom::PlaneOffset(pt, o, t, target) => {
591 let n = unit(p[t].sub(p[o]));
592 out.push(n.dot(p[pt].sub(p[o])) - target);
593 }
594 Atom::DirCross(a0, a1, b0, b1) => {
595 let c = unit(p[a1].sub(p[a0])).cross(unit(p[b1].sub(p[b0])));
596 out.extend([c.x, c.y, c.z]);
597 }
598 Atom::DirDot(a0, a1, b0, b1, target) => {
599 out.push(unit(p[a1].sub(p[a0])).dot(unit(p[b1].sub(p[b0]))) - target);
600 }
601 Atom::AxisCross(a0, a1, axis) => {
602 let c = unit(p[a1].sub(p[a0])).cross(axis);
603 out.extend([c.x, c.y, c.z]);
604 }
605 Atom::EqualLength(a0, a1, b0, b1) => {
606 out.push(p[a1].sub(p[a0]).length() - p[b1].sub(p[b0]).length());
607 }
608 Atom::TangentTouch(a0, a1, c, t) => {
609 out.push(unit(p[t].sub(p[c])).dot(unit(p[a1].sub(p[a0]))));
610 }
611 Atom::InArcPlane(a0, a1, c, s, e) => {
612 let n = unit(p[s].sub(p[c]).cross(p[e].sub(p[c])));
613 out.push(unit(p[a1].sub(p[a0])).dot(n));
614 }
615 Atom::Midpoint(pt, a, b) => {
616 let d = p[pt].sub(p[a].add(p[b]).scale(0.5));
617 out.extend([d.x, d.y, d.z]);
618 }
619 Atom::Symmetric(a, b, o, t) => {
620 let n = unit(p[t].sub(p[o]));
621 out.push(n.dot(p[a].add(p[b]).scale(0.5).sub(p[o])));
622 let v = p[a].sub(p[b]);
623 let lateral = v.sub(n.scale(n.dot(v)));
624 out.extend([lateral.x, lateral.y, lateral.z]);
625 }
626 Atom::OnBezier(pt, ref controls) => {
627 let positions: Vec<Vec3> = controls.iter().map(|&i| p[i]).collect();
628 let d = p[pt].sub(nearest_on_bezier(&positions, p[pt]));
629 out.extend([d.x, d.y, d.z]);
630 }
631 }
632 }
633}
634
635struct Prepared {
640 coords: Vec<Vec3>,
642 free: Vec<usize>,
644 atoms: Vec<Atom>,
645 atom_owner: Vec<Option<usize>>,
647 m: usize,
648 effective_rows: usize,
649 scale: f64,
650 tight: f64,
651}
652
653fn prepare(doc: &Sketch3dDoc, options: &Sketch3dSolveOptions) -> Result<Prepared, String> {
654 let mut index: HashMap<String, usize> = HashMap::with_capacity(doc.points.len());
655 let mut coords: Vec<Vec3> = Vec::with_capacity(doc.points.len());
656 let mut fixed: Vec<bool> = Vec::with_capacity(doc.points.len());
657 for (i, point) in doc.points.iter().enumerate() {
658 if !(point.x.is_finite() && point.y.is_finite() && point.z.is_finite()) {
659 return Err(format!("sketch3d: point {} has a non-finite coordinate", id_key(&point.id)));
660 }
661 index.insert(id_key(&point.id), i);
663 coords.push(point.position());
664 fixed.push(point.fixed);
665 }
666 let lookup = |what: &str, id: &Value| -> Result<usize, String> {
667 index
668 .get(&id_key(id))
669 .copied()
670 .ok_or_else(|| format!("sketch3d: {what} references unknown point {}", id_key(id)))
671 };
672
673 let mut atoms: Vec<Atom> = Vec::new();
674 let mut atom_owner: Vec<Option<usize>> = Vec::new();
675
676 for (ci, constraint) in doc.constraints.iter().enumerate() {
678 let Some(ctype) = constraint.ctype() else {
679 return Err(format!("sketch3d: constraint {ci} has no type"));
680 };
681 let spec = sketch3d_constraint_type(ctype)
682 .ok_or_else(|| format!("sketch3d: unknown constraint type '{ctype}'"))?;
683 let ids = constraint.points();
684 if ids.len() < spec.arity {
685 return Err(format!(
686 "sketch3d: constraint '{ctype}' needs {} points, got {}",
687 spec.arity,
688 ids.len()
689 ));
690 }
691 let what = format!("constraint '{ctype}'");
692 let taken = if ctype == "onCurve" { ids.len() } else { spec.arity };
694 if ctype == "onCurve" && (taken - 2) % 3 != 0 {
695 return Err(format!("sketch3d: constraint 'onCurve' needs a point and 3n + 1 control points, got {taken} ids"));
696 }
697 let mut idx = Vec::with_capacity(taken);
698 for id in &ids[..taken] {
699 idx.push(lookup(&what, id)?);
700 }
701 let value = || -> Result<f64, String> {
702 constraint
703 .value()
704 .filter(|v| v.is_finite())
705 .ok_or_else(|| format!("sketch3d: constraint '{ctype}' needs a finite value"))
706 };
707 let mut push = |atom: Atom| {
708 atoms.push(atom);
709 atom_owner.push(Some(ci));
710 };
711 match ctype {
712 "fixed" => fixed[idx[0]] = true,
713 "coincident" => push(Atom::Coincide(idx[0], idx[1])),
714 "distance" => {
715 let target = value()?;
716 if target.abs() <= 0.0 {
717 push(Atom::Coincide(idx[0], idx[1]));
718 } else {
719 push(Atom::Distance(idx[0], idx[1], target.abs()));
720 }
721 }
722 "onLine" => push(Atom::Lateral(idx[0], idx[1], idx[2])),
723 "lineDistance" => {
724 let target = value()?;
725 if target.abs() <= 0.0 {
726 push(Atom::Lateral(idx[0], idx[1], idx[2]));
727 } else {
728 push(Atom::LineDistance(idx[0], idx[1], idx[2], target.abs()));
729 }
730 }
731 "onPlane" => push(Atom::PlaneOffset(idx[0], idx[1], idx[2], 0.0)),
732 "planeDistance" => push(Atom::PlaneOffset(idx[0], idx[1], idx[2], value()?)),
733 "parallel" => push(Atom::DirCross(idx[0], idx[1], idx[2], idx[3])),
734 "perpendicular" => push(Atom::DirDot(idx[0], idx[1], idx[2], idx[3], 0.0)),
735 "angle" => {
736 let target = value()?.to_radians().cos();
737 push(Atom::DirDot(idx[0], idx[1], idx[2], idx[3], target));
738 }
739 "alongX" => push(Atom::AxisCross(idx[0], idx[1], Vec3::new(1.0, 0.0, 0.0))),
740 "alongY" => push(Atom::AxisCross(idx[0], idx[1], Vec3::new(0.0, 1.0, 0.0))),
741 "alongZ" => push(Atom::AxisCross(idx[0], idx[1], Vec3::new(0.0, 0.0, 1.0))),
742 "equal" => push(Atom::EqualLength(idx[0], idx[1], idx[2], idx[3])),
743 "tangent" => {
744 let (a0, a1, c, s, e) = (idx[0], idx[1], idx[2], idx[3], idx[4]);
747 let touch = if s == a0 || s == a1 {
748 s
749 } else if e == a0 || e == a1 {
750 e
751 } else {
752 let near = |t: usize| {
753 coords[t]
754 .sub(coords[a0])
755 .length()
756 .min(coords[t].sub(coords[a1]).length())
757 };
758 if near(s) <= near(e) {
759 s
760 } else {
761 e
762 }
763 };
764 push(Atom::TangentTouch(a0, a1, c, touch));
765 push(Atom::InArcPlane(a0, a1, c, s, e));
766 }
767 "midpoint" => push(Atom::Midpoint(idx[0], idx[1], idx[2])),
768 "onArc" => {
769 let (pt, c, s, e) = (idx[0], idx[1], idx[2], idx[3]);
770 push(Atom::EqualLength(c, pt, c, s));
771 push(Atom::InArcPlane(c, pt, c, s, e));
772 }
773 "onCircle" => {
774 let (pt, c, tip) = (idx[0], idx[1], idx[2]);
775 push(Atom::PlaneOffset(pt, c, tip, 0.0));
776 push(Atom::EqualLength(c, pt, c, tip));
777 }
778 "symmetric" => push(Atom::Symmetric(idx[0], idx[1], idx[2], idx[3])),
779 "onCurve" => push(Atom::OnBezier(idx[0], idx[1..].to_vec().into_boxed_slice())),
780 other => return Err(format!("sketch3d: unknown constraint type '{other}'")),
781 }
782 }
783
784 for geometry in &doc.geometries {
786 match geometry.geom_type.as_str() {
787 "arc" if geometry.points.len() >= 3 => {
788 let what = "arc geometry";
789 let c = lookup(what, &geometry.points[0])?;
790 let s = lookup(what, &geometry.points[1])?;
791 let e = lookup(what, &geometry.points[2])?;
792 atoms.push(Atom::EqualLength(c, s, c, e));
793 atom_owner.push(None);
794 }
795 "bezier" if geometry.points.len() >= 7 => {
796 let what = "bezier geometry";
797 let ids = &geometry.points;
798 let span_count = (ids.len() - 1) / 3;
799 let last_anchor = span_count * 3;
800 let mut i = 3usize;
801 while i < last_anchor {
802 let prev = lookup(what, &ids[i - 1])?;
803 let anchor = lookup(what, &ids[i])?;
804 let next = lookup(what, &ids[i + 1])?;
805 if prev != anchor && next != anchor && prev != next {
806 atoms.push(Atom::Lateral(anchor, prev, next));
807 atom_owner.push(None);
808 }
809 i += 3;
810 }
811 }
812 _ => {}
813 }
814 }
815
816 let free: Vec<usize> = (0..coords.len()).filter(|&i| !fixed[i]).collect();
817 let m: usize = atoms.iter().map(Atom::rows).sum();
818 let effective_rows: usize = atoms.iter().map(Atom::eff_rows).sum();
819
820 let mut scale = 1.0f64;
821 for c in &coords {
822 scale = scale.max(c.x.abs()).max(c.y.abs()).max(c.z.abs());
823 }
824 let tight = options.tolerance * scale;
825 Ok(Prepared {
826 coords,
827 free,
828 atoms,
829 atom_owner,
830 m,
831 effective_rows,
832 scale,
833 tight,
834 })
835}
836
837fn apply_delta(base: &[Vec3], free: &[usize], delta: &[f64], out: &mut Vec<Vec3>) {
842 out.clear();
843 out.extend_from_slice(base);
844 for (k, &i) in free.iter().enumerate() {
845 out[i] = base[i].add(Vec3::new(delta[3 * k], delta[3 * k + 1], delta[3 * k + 2]));
846 }
847}
848
849fn eval_residuals(
850 atoms: &[Atom],
851 base: &[Vec3],
852 free: &[usize],
853 delta: &[f64],
854 dir_eps: f64,
855 scratch: &mut Vec<Vec3>,
856 out: &mut Vec<f64>,
857) {
858 apply_delta(base, free, delta, scratch);
859 out.clear();
860 for atom in atoms {
861 atom.eval(scratch, dir_eps, out);
862 }
863}
864
865fn finite_diff_jacobian(
866 atoms: &[Atom],
867 base: &[Vec3],
868 free: &[usize],
869 h: f64,
870 dir_eps: f64,
871 m: usize,
872) -> Vec<Vec<f64>> {
873 let n = 3 * free.len();
874 let mut jac = vec![vec![0.0f64; n]; m];
875 if n == 0 || m == 0 {
876 return jac;
877 }
878 let mut delta = vec![0.0f64; n];
879 let mut scratch: Vec<Vec3> = Vec::with_capacity(base.len());
880 let mut rp: Vec<f64> = Vec::with_capacity(m);
881 let mut rm: Vec<f64> = Vec::with_capacity(m);
882 let inv = 1.0 / (2.0 * h);
883 for col in 0..n {
884 delta[col] = h;
885 eval_residuals(atoms, base, free, &delta, dir_eps, &mut scratch, &mut rp);
886 delta[col] = -h;
887 eval_residuals(atoms, base, free, &delta, dir_eps, &mut scratch, &mut rm);
888 delta[col] = 0.0;
889 for i in 0..m {
890 jac[i][col] = (rp[i] - rm[i]) * inv;
891 }
892 }
893 jac
894}
895
896fn lm_core(prep: &Prepared, coords: &mut Vec<Vec3>, max_iterations: usize) -> Result<usize, String> {
900 let atoms = &prep.atoms;
901 let free = &prep.free;
902 let n = 3 * free.len();
903 let m = prep.m;
904 let scale = prep.scale;
905 let dir_eps = DIR_EPS_REL * scale;
906 let h = FD_STEP * scale;
907 let step_tol = STEP_REL_TOL * scale;
908 let tight = prep.tight;
909
910 let max_abs = |v: &[f64]| v.iter().fold(0.0f64, |acc, &x| acc.max(x.abs()));
911 let sum_sq = |v: &[f64]| v.iter().fold(0.0f64, |acc, &x| acc + x * x);
912
913 let zeros = vec![0.0f64; n];
914 let mut scratch: Vec<Vec3> = Vec::with_capacity(coords.len());
915 let mut r: Vec<f64> = Vec::with_capacity(m);
916 eval_residuals(atoms, coords, free, &zeros, dir_eps, &mut scratch, &mut r);
917 if !max_abs(&r).is_finite() {
918 return Err("sketch3d: non-finite residual at the initial coordinates".into());
919 }
920
921 let mut iterations = 0usize;
922 let mut s = sum_sq(&r);
923 let mut best_coords = coords.clone();
924 let mut best_max = max_abs(&r);
925 let mut lambda = -1.0f64;
926
927 if n > 0 && m > 0 {
928 'outer: for _iter in 0..max_iterations {
929 if best_max <= tight {
930 break;
931 }
932 let jac = finite_diff_jacobian(atoms, coords, free, h, dir_eps, m);
933 let mut hm = vec![vec![0.0f64; n]; n];
934 let mut g = vec![0.0f64; n];
935 for i in 0..m {
936 let row = &jac[i];
937 let ri = r[i];
938 for a in 0..n {
939 let ja = row[a];
940 if ja == 0.0 {
941 continue;
942 }
943 g[a] += ja * ri;
944 for b in a..n {
945 hm[a][b] += ja * row[b];
946 }
947 }
948 }
949 for a in 0..n {
950 for b in (a + 1)..n {
951 hm[b][a] = hm[a][b];
952 }
953 }
954 let mut max_diag = 0.0f64;
955 for a in 0..n {
956 max_diag = max_diag.max(hm[a][a]);
957 }
958 if !(max_diag > 0.0) {
959 break;
960 }
961 if lambda < 0.0 {
962 lambda = 1e-9 * max_diag;
963 }
964 let lambda_ceiling = 1e12 * max_diag;
965
966 let mut stepped = false;
967 let mut step_small = false;
968 for _try in 0..MAX_DAMPING {
969 let mut a_mat = hm.clone();
970 for d in 0..n {
971 a_mat[d][d] += lambda;
972 }
973 let mut delta: Vec<f64> = g.iter().map(|v| -v).collect();
974 if !solve_spd(&mut a_mat, &mut delta) {
975 lambda *= LAMBDA_UP;
976 if lambda > lambda_ceiling {
977 break;
978 }
979 continue;
980 }
981 let mut r_trial: Vec<f64> = Vec::with_capacity(m);
982 eval_residuals(atoms, coords, free, &delta, dir_eps, &mut scratch, &mut r_trial);
983 let s_trial = sum_sq(&r_trial);
984 if s_trial.is_finite() && s_trial < s {
985 let step_norm = sum_sq(&delta).sqrt();
986 apply_delta(coords, free, &delta, &mut scratch);
987 coords.copy_from_slice(&scratch);
988 eval_residuals(atoms, coords, free, &zeros, dir_eps, &mut scratch, &mut r);
989 s = sum_sq(&r);
990 let m_now = max_abs(&r);
991 if m_now < best_max {
992 best_max = m_now;
993 best_coords.copy_from_slice(coords);
994 }
995 lambda = (lambda * LAMBDA_DOWN).max(1e-12 * max_diag);
996 iterations += 1;
997 stepped = true;
998 step_small = step_norm <= step_tol;
999 break;
1000 }
1001 lambda *= LAMBDA_UP;
1002 if lambda > lambda_ceiling {
1003 break;
1004 }
1005 }
1006 if !stepped || step_small {
1007 break 'outer;
1008 }
1009 }
1010 }
1011 coords.copy_from_slice(&best_coords);
1012 Ok(iterations)
1013}
1014
1015fn rank_and_nullspace(mut a: Vec<Vec<f64>>, m: usize, n: usize) -> (usize, Vec<Vec<f64>>) {
1018 if n == 0 {
1019 return (0, Vec::new());
1020 }
1021 let mut maxabs = 0.0f64;
1022 for row in &a {
1023 for &v in row {
1024 maxabs = maxabs.max(v.abs());
1025 }
1026 }
1027 let tol = if maxabs > 0.0 {
1028 RANK_REL_TOL * maxabs
1029 } else {
1030 RANK_REL_TOL
1031 };
1032 let mut pivot_cols: Vec<usize> = Vec::new();
1033 row_reduce(&mut a, m, n, tol, |col| pivot_cols.push(col));
1034 let rank = pivot_cols.len();
1035 let mut is_pivot = vec![false; n];
1036 for &pc in &pivot_cols {
1037 is_pivot[pc] = true;
1038 }
1039 let mut basis: Vec<Vec<f64>> = Vec::new();
1040 for free_col in 0..n {
1041 if is_pivot[free_col] {
1042 continue;
1043 }
1044 let mut v = vec![0.0f64; n];
1045 v[free_col] = 1.0;
1046 for (i, &pc) in pivot_cols.iter().enumerate() {
1047 v[pc] = -a[i][free_col];
1048 }
1049 basis.push(v);
1050 }
1051 (rank, basis)
1052}
1053
1054pub fn solve_sketch3d_doc(
1061 doc: &mut Sketch3dDoc,
1062 options: &Sketch3dSolveOptions,
1063) -> Result<Sketch3dDiagnostics, String> {
1064 let prep = prepare(doc, options)?;
1065 let mut coords = prep.coords.clone();
1066 let iterations = lm_core(&prep, &mut coords, options.max_iterations)?;
1067
1068 let n = 3 * prep.free.len();
1069 let m = prep.m;
1070 let dir_eps = DIR_EPS_REL * prep.scale;
1071 let zeros = vec![0.0f64; n];
1072 let mut scratch: Vec<Vec3> = Vec::with_capacity(coords.len());
1073 let mut r: Vec<f64> = Vec::with_capacity(m);
1074 eval_residuals(&prep.atoms, &coords, &prep.free, &zeros, dir_eps, &mut scratch, &mut r);
1075
1076 let jac = finite_diff_jacobian(&prep.atoms, &coords, &prep.free, FD_STEP * prep.scale, dir_eps, m);
1077 let row_norms: Vec<f64> = jac
1081 .iter()
1082 .map(|row| row.iter().fold(0.0f64, |acc, &v| acc + v * v).sqrt())
1083 .collect();
1084 let max_row_norm = row_norms.iter().fold(0.0f64, |a, &v| a.max(v));
1085 let norm_floor = if max_row_norm > 0.0 { 1e-9 * max_row_norm } else { 1.0 };
1086 let scaled: Vec<f64> = r
1087 .iter()
1088 .zip(row_norms.iter())
1089 .map(|(&v, &norm)| v / norm.max(norm_floor))
1090 .collect();
1091 let max_residual = scaled.iter().fold(0.0f64, |acc, &v| acc.max(v.abs()));
1092
1093 let (rank, basis) = rank_and_nullspace(jac, m, n);
1094 let dof = n - rank;
1095 let redundant = prep.effective_rows.saturating_sub(rank);
1096
1097 let conflict_tol = CONFLICT_REL_TOL * prep.scale;
1100 let mut flagged = vec![false; doc.constraints.len()];
1101 let mut row = 0usize;
1102 for (ai, atom) in prep.atoms.iter().enumerate() {
1103 let rows = atom.rows();
1104 let violated = scaled[row..row + rows].iter().any(|v| v.abs() > conflict_tol);
1105 if violated {
1106 if let Some(owner) = prep.atom_owner[ai] {
1107 flagged[owner] = true;
1108 }
1109 }
1110 row += rows;
1111 }
1112 let mut conflicting_constraints: Vec<String> = Vec::new();
1113 for (ci, &hit) in flagged.iter().enumerate() {
1114 if hit {
1115 if let Some(id) = doc.constraints[ci].id() {
1116 conflicting_constraints.push(id_key(id));
1117 }
1118 }
1119 }
1120 if max_residual > conflict_tol && conflicting_constraints.is_empty() {
1125 let mut row = 0usize;
1126 for (ai, atom) in prep.atoms.iter().enumerate() {
1127 let rows = atom.rows();
1128 let violated = scaled[row..row + rows].iter().any(|v| v.abs() > conflict_tol);
1129 row += rows;
1130 if !violated || prep.atom_owner[ai].is_some() {
1131 continue;
1132 }
1133 for (ci, constraint) in doc.constraints.iter().enumerate() {
1134 if flagged[ci] {
1135 continue;
1136 }
1137 if let Some(id) = constraint.id() {
1138 flagged[ci] = true;
1139 conflicting_constraints.push(id_key(id));
1140 }
1141 }
1142 }
1143 }
1144 let conflicting = !conflicting_constraints.is_empty();
1145
1146 let status = if redundant > 0 {
1147 "over"
1148 } else if dof > 0 {
1149 "under"
1150 } else {
1151 "well"
1152 };
1153
1154 let basis_max: Vec<f64> = basis
1157 .iter()
1158 .map(|v| v.iter().fold(0.0f64, |a, &x| a.max(x.abs())).max(1e-300))
1159 .collect();
1160 let mut col_of: HashMap<usize, usize> = HashMap::with_capacity(prep.free.len());
1161 for (k, &i) in prep.free.iter().enumerate() {
1162 col_of.insert(i, k);
1163 }
1164 let mut point_mobility = std::collections::BTreeMap::new();
1165 for (i, point) in doc.points.iter().enumerate() {
1166 let movable = match col_of.get(&i) {
1167 None => false,
1168 Some(&k) => basis.iter().enumerate().any(|(b, v)| {
1169 let c = 3 * k;
1170 let comp = ((v[c] / basis_max[b]).powi(2)
1171 + (v[c + 1] / basis_max[b]).powi(2)
1172 + (v[c + 2] / basis_max[b]).powi(2))
1173 .sqrt();
1174 comp > MOBILITY_TOL
1175 }),
1176 };
1177 point_mobility.insert(
1178 id_key(&point.id),
1179 (if movable { "movable" } else { "locked" }).to_string(),
1180 );
1181 }
1182 let mut geometry_mobility = std::collections::BTreeMap::new();
1183 for geometry in &doc.geometries {
1184 let movable = geometry
1185 .points
1186 .iter()
1187 .any(|pid| point_mobility.get(&id_key(pid)).map(String::as_str) == Some("movable"));
1188 geometry_mobility.insert(
1189 id_key(&geometry.id),
1190 (if movable { "movable" } else { "locked" }).to_string(),
1191 );
1192 }
1193
1194 for (i, point) in doc.points.iter_mut().enumerate() {
1196 point.x = coords[i].x;
1197 point.y = coords[i].y;
1198 point.z = coords[i].z;
1199 }
1200
1201 Ok(Sketch3dDiagnostics {
1202 dof: dof as i64,
1203 rank: rank as i64,
1204 unknowns: n as i64,
1205 equations: m as i64,
1206 redundant: redundant as i64,
1207 status: status.into(),
1208 conflicting,
1209 conflicting_constraints,
1210 point_mobility,
1211 geometry_mobility,
1212 iterations: iterations as i64,
1213 max_residual,
1214 })
1215}
1216
1217pub fn solve_sketch3d(request: &SolveSketch3dRequest) -> Result<Value, String> {
1220 let mut doc: Sketch3dDoc = serde_json::from_value(request.sketch.clone())
1221 .map_err(|e| format!("sketch3d: invalid sketch document: {e}"))?;
1222 let options = Sketch3dSolveOptions {
1223 tolerance: request.tolerance.unwrap_or_else(default_tolerance),
1224 max_iterations: request.max_iterations.unwrap_or_else(default_max_iterations),
1225 };
1226 let diagnostics = solve_sketch3d_doc(&mut doc, &options)?;
1227 let mut sketch = serde_json::to_value(&doc).map_err(|e| format!("sketch3d: {e}"))?;
1228 if let Some(map) = sketch.as_object_mut() {
1229 map.insert(
1230 "diagnostics".into(),
1231 serde_json::to_value(&diagnostics).map_err(|e| format!("sketch3d: {e}"))?,
1232 );
1233 }
1234 Ok(serde_json::json!({ "sketch": sketch }))
1235}
1236
1237pub fn solve_sketch3d_from_json(request: &str) -> Result<String, String> {
1239 let request: SolveSketch3dRequest =
1240 serde_json::from_str(request).map_err(|e| format!("sketch3d: invalid request: {e}"))?;
1241 let response = solve_sketch3d(&request)?;
1242 serde_json::to_string(&response).map_err(|e| format!("sketch3d: {e}"))
1243}
1244