use crate::solver_linear_algebra::{row_reduce, solve_spd};
use crate::Vec3;
use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};
use std::collections::HashMap;
const MAX_DAMPING: usize = 16;
const LAMBDA_UP: f64 = 8.0;
const LAMBDA_DOWN: f64 = 0.25;
const STEP_REL_TOL: f64 = 1e-13;
const FD_STEP: f64 = 1e-6;
const RANK_REL_TOL: f64 = 1e-7;
const MOBILITY_TOL: f64 = 1e-6;
const CONFLICT_REL_TOL: f64 = 1e-6;
const DIR_EPS_REL: f64 = 1e-12;
fn default_tolerance() -> f64 {
1e-10
}
fn default_max_iterations() -> usize {
200
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct Sketch3dPoint {
pub id: Value,
pub x: f64,
pub y: f64,
pub z: f64,
#[serde(default)]
pub fixed: bool,
#[serde(default)]
pub construction: bool,
#[serde(default, rename = "externalReference")]
pub external_reference: bool,
}
impl Sketch3dPoint {
pub fn position(&self) -> Vec3 {
Vec3::new(self.x, self.y, self.z)
}
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct Sketch3dGeometry {
pub id: Value,
#[serde(rename = "type")]
pub geom_type: String,
#[serde(default)]
pub points: Vec<Value>,
#[serde(flatten)]
pub extra: Map<String, Value>,
}
impl Sketch3dGeometry {
pub fn construction(&self) -> bool {
self.geom_type == "plane"
|| self
.extra
.get("construction")
.and_then(Value::as_bool)
.unwrap_or(false)
}
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(transparent)]
pub struct Sketch3dConstraint {
pub raw: Map<String, Value>,
}
impl Sketch3dConstraint {
pub fn ctype(&self) -> Option<&str> {
self.raw.get("type").and_then(Value::as_str)
}
pub fn id(&self) -> Option<&Value> {
self.raw.get("id")
}
pub fn points(&self) -> &[Value] {
self.raw
.get("points")
.and_then(Value::as_array)
.map(Vec::as_slice)
.unwrap_or(&[])
}
pub fn value(&self) -> Option<f64> {
self.raw.get("value").and_then(Value::as_f64)
}
}
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
pub struct Sketch3dDoc {
#[serde(default)]
pub points: Vec<Sketch3dPoint>,
#[serde(default)]
pub geometries: Vec<Sketch3dGeometry>,
#[serde(default)]
pub constraints: Vec<Sketch3dConstraint>,
}
impl Sketch3dDoc {
pub fn point(&self, id: &Value) -> Option<&Sketch3dPoint> {
let key = id_key(id);
self.points.iter().find(|p| id_key(&p.id) == key)
}
pub fn point_mut(&mut self, id: &Value) -> Option<&mut Sketch3dPoint> {
let key = id_key(id);
self.points.iter_mut().find(|p| id_key(&p.id) == key)
}
pub fn geometry(&self, id: &Value) -> Option<&Sketch3dGeometry> {
let key = id_key(id);
self.geometries.iter().find(|g| id_key(&g.id) == key)
}
pub fn geometry_mut(&mut self, id: &Value) -> Option<&mut Sketch3dGeometry> {
let key = id_key(id);
self.geometries.iter_mut().find(|g| id_key(&g.id) == key)
}
pub fn constraint(&self, id: &Value) -> Option<&Sketch3dConstraint> {
let key = id_key(id);
self.constraints
.iter()
.find(|c| c.id().map(id_key) == Some(key.clone()))
}
pub fn next_point_id(&self) -> Value {
mint_next_id(self.points.iter().map(|p| &p.id))
}
pub fn next_geometry_id(&self) -> Value {
mint_next_id(self.geometries.iter().map(|g| &g.id))
}
pub fn next_constraint_id(&self) -> Value {
mint_next_id(self.constraints.iter().filter_map(|c| c.id()))
}
pub fn add_point(&mut self, p: Vec3) -> Value {
let id = self.next_point_id();
self.points.push(Sketch3dPoint {
id: id.clone(),
x: p.x,
y: p.y,
z: p.z,
fixed: false,
construction: false,
external_reference: false,
});
id
}
pub fn snap_or_add_point(&mut self, p: Vec3, radius: f64) -> Value {
let mut best: Option<(f64, Value)> = None;
for point in &self.points {
let d = point.position().sub(p).length();
if d <= radius && best.as_ref().map_or(true, |(bd, _)| d < *bd) {
best = Some((d, point.id.clone()));
}
}
match best {
Some((_, id)) => id,
None => self.add_point(p),
}
}
pub fn add_geometry(&mut self, geom_type: &str, points: Vec<Value>, construction: bool) -> Value {
let id = self.next_geometry_id();
let mut extra = Map::new();
if construction {
extra.insert("construction".into(), Value::Bool(true));
}
self.geometries.push(Sketch3dGeometry {
id: id.clone(),
geom_type: geom_type.to_string(),
points,
extra,
});
id
}
pub fn add_constraint(&mut self, ctype: &str, points: Vec<Value>, value: Option<f64>) -> Value {
let id = self.next_constraint_id();
let mut raw = Map::new();
raw.insert("id".into(), id.clone());
raw.insert("type".into(), Value::String(ctype.to_string()));
raw.insert("points".into(), Value::Array(points));
if let Some(value) = value {
raw.insert("value".into(), Value::from(value));
}
self.constraints.push(Sketch3dConstraint { raw });
id
}
pub fn geometry_positions(&self, geometry: &Sketch3dGeometry) -> Option<Vec<Vec3>> {
geometry
.points
.iter()
.map(|id| self.point(id).map(Sketch3dPoint::position))
.collect()
}
}
pub use crate::sketch_solver::sketch_id_key as id_key;
fn mint_next_id<'a>(ids: impl Iterator<Item = &'a Value>) -> Value {
crate::mint_sketch_id(ids, |_| false)
}
#[derive(Clone, Copy, Debug)]
pub struct Sketch3dConstraintType {
pub name: &'static str,
pub arity: usize,
pub dimensional: bool,
pub meaning: &'static str,
}
pub const SKETCH3D_CONSTRAINT_TYPES: &[Sketch3dConstraintType] = &[
Sketch3dConstraintType { name: "fixed", arity: 1, dimensional: false, meaning: "ground a point where it is" },
Sketch3dConstraintType { name: "coincident", arity: 2, dimensional: false, meaning: "two points touch" },
Sketch3dConstraintType { name: "distance", arity: 2, dimensional: true, meaning: "distance between two points (a line's length)" },
Sketch3dConstraintType { name: "onLine", arity: 3, dimensional: false, meaning: "[p, a0, a1]: point on the infinite line a0→a1" },
Sketch3dConstraintType { name: "lineDistance", arity: 3, dimensional: true, meaning: "[p, a0, a1]: distance from the point to the infinite line" },
Sketch3dConstraintType { name: "onPlane", arity: 3, dimensional: false, meaning: "[p, origin, normalTip]: point on the plane" },
Sketch3dConstraintType { name: "planeDistance", arity: 3, dimensional: true, meaning: "[p, origin, normalTip]: signed height of the point above the plane" },
Sketch3dConstraintType { name: "parallel", arity: 4, dimensional: false, meaning: "[a0, a1, b0, b1]: the two directions are parallel (either sense)" },
Sketch3dConstraintType { name: "perpendicular", arity: 4, dimensional: false, meaning: "[a0, a1, b0, b1]: the two directions are perpendicular" },
Sketch3dConstraintType { name: "angle", arity: 4, dimensional: true, meaning: "[a0, a1, b0, b1]: angle in degrees between the two directions" },
Sketch3dConstraintType { name: "alongX", arity: 2, dimensional: false, meaning: "the line runs along world X" },
Sketch3dConstraintType { name: "alongY", arity: 2, dimensional: false, meaning: "the line runs along world Y" },
Sketch3dConstraintType { name: "alongZ", arity: 2, dimensional: false, meaning: "the line runs along world Z" },
Sketch3dConstraintType { name: "equal", arity: 4, dimensional: false, meaning: "[a0, a1, b0, b1]: equal lengths (an arc's radius is center→start)" },
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" },
Sketch3dConstraintType { name: "midpoint", arity: 3, dimensional: false, meaning: "[p, a0, a1]: the point is halfway between a0 and a1" },
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" },
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" },
Sketch3dConstraintType { name: "symmetric", arity: 4, dimensional: false, meaning: "[a, b, origin, normalTip]: a and b mirror each other in the plane" },
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" },
];
pub fn sketch3d_constraint_type(name: &str) -> Option<&'static Sketch3dConstraintType> {
SKETCH3D_CONSTRAINT_TYPES.iter().find(|t| t.name == name)
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
pub struct Sketch3dSolveOptions {
#[serde(default = "default_tolerance")]
pub tolerance: f64,
#[serde(default = "default_max_iterations")]
pub max_iterations: usize,
}
impl Default for Sketch3dSolveOptions {
fn default() -> Self {
Self {
tolerance: default_tolerance(),
max_iterations: default_max_iterations(),
}
}
}
#[derive(Debug, Deserialize)]
pub struct SolveSketch3dRequest {
pub sketch: Value,
#[serde(default)]
pub tolerance: Option<f64>,
#[serde(default)]
pub max_iterations: Option<usize>,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
pub struct Sketch3dDiagnostics {
#[serde(default)]
pub dof: i64,
#[serde(default)]
pub rank: i64,
#[serde(default)]
pub unknowns: i64,
#[serde(default)]
pub equations: i64,
#[serde(default)]
pub redundant: i64,
#[serde(default)]
pub status: String,
#[serde(default)]
pub conflicting: bool,
#[serde(default, rename = "conflictingConstraints")]
pub conflicting_constraints: Vec<String>,
#[serde(default, rename = "pointMobility")]
pub point_mobility: std::collections::BTreeMap<String, String>,
#[serde(default, rename = "geometryMobility")]
pub geometry_mobility: std::collections::BTreeMap<String, String>,
#[serde(default)]
pub iterations: i64,
#[serde(default, rename = "maxResidual")]
pub max_residual: f64,
}
impl Sketch3dDiagnostics {
pub fn point_movable(&self, id: &Value) -> Option<bool> {
self.point_mobility.get(&id_key(id)).map(|v| v == "movable")
}
pub fn geometry_movable(&self, id: &Value) -> Option<bool> {
self.geometry_mobility.get(&id_key(id)).map(|v| v == "movable")
}
pub fn constraint_conflicting(&self, id: &Value) -> bool {
if self.conflicting_constraints.is_empty() {
return false;
}
let key = id_key(id);
self.conflicting_constraints.iter().any(|c| *c == key)
}
}
#[derive(Clone, Debug)]
enum Atom {
Coincide(usize, usize),
Distance(usize, usize, f64),
Lateral(usize, usize, usize),
LineDistance(usize, usize, usize, f64),
PlaneOffset(usize, usize, usize, f64),
DirCross(usize, usize, usize, usize),
DirDot(usize, usize, usize, usize, f64),
AxisCross(usize, usize, Vec3),
EqualLength(usize, usize, usize, usize),
TangentTouch(usize, usize, usize, usize),
InArcPlane(usize, usize, usize, usize, usize),
Midpoint(usize, usize, usize),
Symmetric(usize, usize, usize, usize),
OnBezier(usize, Box<[usize]>),
}
fn nearest_on_bezier(controls: &[Vec3], target: Vec3) -> Vec3 {
const SCAN: usize = 16;
let value = |c: &[Vec3], t: f64| {
let u = 1.0 - t;
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))
};
let first = |c: &[Vec3], t: f64| {
let u = 1.0 - t;
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))
};
let second = |c: &[Vec3], t: f64| {
let d1 = c[2].sub(c[1].scale(2.0)).add(c[0]);
let d2 = c[3].sub(c[2].scale(2.0)).add(c[1]);
d1.scale(6.0 * (1.0 - t)).add(d2.scale(6.0 * t))
};
let mut best: Option<(f64, usize, f64)> = None;
for span in 0..(controls.len() - 1) / 3 {
let c = &controls[span * 3..span * 3 + 4];
for i in 0..=SCAN {
let t = i as f64 / SCAN as f64;
let d = value(c, t).sub(target).length();
if best.map_or(true, |(bd, _, _)| d < bd) {
best = Some((d, span, t));
}
}
}
let Some((_, span, mut t)) = best else { return target };
let c = &controls[span * 3..span * 3 + 4];
for _ in 0..8 {
let offset = value(c, t).sub(target);
let d1 = first(c, t);
let denominator = d1.dot(d1) + offset.dot(second(c, t));
if denominator.abs() <= 1e-14 {
break;
}
t = (t - offset.dot(d1) / denominator).clamp(0.0, 1.0);
}
value(c, t)
}
impl Atom {
fn rows(&self) -> usize {
match self {
Atom::Coincide(..) | Atom::Lateral(..) | Atom::DirCross(..) | Atom::AxisCross(..) | Atom::Midpoint(..) | Atom::OnBezier(..) => 3,
Atom::Symmetric(..) => 4,
Atom::Distance(..)
| Atom::LineDistance(..)
| Atom::PlaneOffset(..)
| Atom::DirDot(..)
| Atom::EqualLength(..)
| Atom::TangentTouch(..)
| Atom::InArcPlane(..) => 1,
}
}
fn eff_rows(&self) -> usize {
match self {
Atom::Coincide(..) | Atom::Midpoint(..) | Atom::Symmetric(..) => 3,
Atom::Lateral(..) | Atom::DirCross(..) | Atom::AxisCross(..) | Atom::OnBezier(..) => 2,
_ => 1,
}
}
fn eval(&self, p: &[Vec3], dir_eps: f64, out: &mut Vec<f64>) {
let unit = |v: Vec3| {
let len = v.length();
if len > dir_eps {
v.scale(1.0 / len)
} else {
v.scale(1.0 / dir_eps)
}
};
match *self {
Atom::Coincide(a, b) => {
let d = p[a].sub(p[b]);
out.extend([d.x, d.y, d.z]);
}
Atom::Distance(a, b, target) => out.push(p[a].sub(p[b]).length() - target),
Atom::Lateral(pt, o, q) => {
let d = unit(p[q].sub(p[o]));
let v = p[pt].sub(p[o]);
let lateral = v.sub(d.scale(d.dot(v)));
out.extend([lateral.x, lateral.y, lateral.z]);
}
Atom::LineDistance(pt, o, q, target) => {
let d = unit(p[q].sub(p[o]));
let v = p[pt].sub(p[o]);
let lateral = v.sub(d.scale(d.dot(v)));
out.push(lateral.length() - target);
}
Atom::PlaneOffset(pt, o, t, target) => {
let n = unit(p[t].sub(p[o]));
out.push(n.dot(p[pt].sub(p[o])) - target);
}
Atom::DirCross(a0, a1, b0, b1) => {
let c = unit(p[a1].sub(p[a0])).cross(unit(p[b1].sub(p[b0])));
out.extend([c.x, c.y, c.z]);
}
Atom::DirDot(a0, a1, b0, b1, target) => {
out.push(unit(p[a1].sub(p[a0])).dot(unit(p[b1].sub(p[b0]))) - target);
}
Atom::AxisCross(a0, a1, axis) => {
let c = unit(p[a1].sub(p[a0])).cross(axis);
out.extend([c.x, c.y, c.z]);
}
Atom::EqualLength(a0, a1, b0, b1) => {
out.push(p[a1].sub(p[a0]).length() - p[b1].sub(p[b0]).length());
}
Atom::TangentTouch(a0, a1, c, t) => {
out.push(unit(p[t].sub(p[c])).dot(unit(p[a1].sub(p[a0]))));
}
Atom::InArcPlane(a0, a1, c, s, e) => {
let n = unit(p[s].sub(p[c]).cross(p[e].sub(p[c])));
out.push(unit(p[a1].sub(p[a0])).dot(n));
}
Atom::Midpoint(pt, a, b) => {
let d = p[pt].sub(p[a].add(p[b]).scale(0.5));
out.extend([d.x, d.y, d.z]);
}
Atom::Symmetric(a, b, o, t) => {
let n = unit(p[t].sub(p[o]));
out.push(n.dot(p[a].add(p[b]).scale(0.5).sub(p[o])));
let v = p[a].sub(p[b]);
let lateral = v.sub(n.scale(n.dot(v)));
out.extend([lateral.x, lateral.y, lateral.z]);
}
Atom::OnBezier(pt, ref controls) => {
let positions: Vec<Vec3> = controls.iter().map(|&i| p[i]).collect();
let d = p[pt].sub(nearest_on_bezier(&positions, p[pt]));
out.extend([d.x, d.y, d.z]);
}
}
}
}
struct Prepared {
coords: Vec<Vec3>,
free: Vec<usize>,
atoms: Vec<Atom>,
atom_owner: Vec<Option<usize>>,
m: usize,
effective_rows: usize,
scale: f64,
tight: f64,
}
fn prepare(doc: &Sketch3dDoc, options: &Sketch3dSolveOptions) -> Result<Prepared, String> {
let mut index: HashMap<String, usize> = HashMap::with_capacity(doc.points.len());
let mut coords: Vec<Vec3> = Vec::with_capacity(doc.points.len());
let mut fixed: Vec<bool> = Vec::with_capacity(doc.points.len());
for (i, point) in doc.points.iter().enumerate() {
if !(point.x.is_finite() && point.y.is_finite() && point.z.is_finite()) {
return Err(format!("sketch3d: point {} has a non-finite coordinate", id_key(&point.id)));
}
index.insert(id_key(&point.id), i);
coords.push(point.position());
fixed.push(point.fixed);
}
let lookup = |what: &str, id: &Value| -> Result<usize, String> {
index
.get(&id_key(id))
.copied()
.ok_or_else(|| format!("sketch3d: {what} references unknown point {}", id_key(id)))
};
let mut atoms: Vec<Atom> = Vec::new();
let mut atom_owner: Vec<Option<usize>> = Vec::new();
for (ci, constraint) in doc.constraints.iter().enumerate() {
let Some(ctype) = constraint.ctype() else {
return Err(format!("sketch3d: constraint {ci} has no type"));
};
let spec = sketch3d_constraint_type(ctype)
.ok_or_else(|| format!("sketch3d: unknown constraint type '{ctype}'"))?;
let ids = constraint.points();
if ids.len() < spec.arity {
return Err(format!(
"sketch3d: constraint '{ctype}' needs {} points, got {}",
spec.arity,
ids.len()
));
}
let what = format!("constraint '{ctype}'");
let taken = if ctype == "onCurve" { ids.len() } else { spec.arity };
if ctype == "onCurve" && (taken - 2) % 3 != 0 {
return Err(format!("sketch3d: constraint 'onCurve' needs a point and 3n + 1 control points, got {taken} ids"));
}
let mut idx = Vec::with_capacity(taken);
for id in &ids[..taken] {
idx.push(lookup(&what, id)?);
}
let value = || -> Result<f64, String> {
constraint
.value()
.filter(|v| v.is_finite())
.ok_or_else(|| format!("sketch3d: constraint '{ctype}' needs a finite value"))
};
let mut push = |atom: Atom| {
atoms.push(atom);
atom_owner.push(Some(ci));
};
match ctype {
"fixed" => fixed[idx[0]] = true,
"coincident" => push(Atom::Coincide(idx[0], idx[1])),
"distance" => {
let target = value()?;
if target.abs() <= 0.0 {
push(Atom::Coincide(idx[0], idx[1]));
} else {
push(Atom::Distance(idx[0], idx[1], target.abs()));
}
}
"onLine" => push(Atom::Lateral(idx[0], idx[1], idx[2])),
"lineDistance" => {
let target = value()?;
if target.abs() <= 0.0 {
push(Atom::Lateral(idx[0], idx[1], idx[2]));
} else {
push(Atom::LineDistance(idx[0], idx[1], idx[2], target.abs()));
}
}
"onPlane" => push(Atom::PlaneOffset(idx[0], idx[1], idx[2], 0.0)),
"planeDistance" => push(Atom::PlaneOffset(idx[0], idx[1], idx[2], value()?)),
"parallel" => push(Atom::DirCross(idx[0], idx[1], idx[2], idx[3])),
"perpendicular" => push(Atom::DirDot(idx[0], idx[1], idx[2], idx[3], 0.0)),
"angle" => {
let target = value()?.to_radians().cos();
push(Atom::DirDot(idx[0], idx[1], idx[2], idx[3], target));
}
"alongX" => push(Atom::AxisCross(idx[0], idx[1], Vec3::new(1.0, 0.0, 0.0))),
"alongY" => push(Atom::AxisCross(idx[0], idx[1], Vec3::new(0.0, 1.0, 0.0))),
"alongZ" => push(Atom::AxisCross(idx[0], idx[1], Vec3::new(0.0, 0.0, 1.0))),
"equal" => push(Atom::EqualLength(idx[0], idx[1], idx[2], idx[3])),
"tangent" => {
let (a0, a1, c, s, e) = (idx[0], idx[1], idx[2], idx[3], idx[4]);
let touch = if s == a0 || s == a1 {
s
} else if e == a0 || e == a1 {
e
} else {
let near = |t: usize| {
coords[t]
.sub(coords[a0])
.length()
.min(coords[t].sub(coords[a1]).length())
};
if near(s) <= near(e) {
s
} else {
e
}
};
push(Atom::TangentTouch(a0, a1, c, touch));
push(Atom::InArcPlane(a0, a1, c, s, e));
}
"midpoint" => push(Atom::Midpoint(idx[0], idx[1], idx[2])),
"onArc" => {
let (pt, c, s, e) = (idx[0], idx[1], idx[2], idx[3]);
push(Atom::EqualLength(c, pt, c, s));
push(Atom::InArcPlane(c, pt, c, s, e));
}
"onCircle" => {
let (pt, c, tip) = (idx[0], idx[1], idx[2]);
push(Atom::PlaneOffset(pt, c, tip, 0.0));
push(Atom::EqualLength(c, pt, c, tip));
}
"symmetric" => push(Atom::Symmetric(idx[0], idx[1], idx[2], idx[3])),
"onCurve" => push(Atom::OnBezier(idx[0], idx[1..].to_vec().into_boxed_slice())),
other => return Err(format!("sketch3d: unknown constraint type '{other}'")),
}
}
for geometry in &doc.geometries {
match geometry.geom_type.as_str() {
"arc" if geometry.points.len() >= 3 => {
let what = "arc geometry";
let c = lookup(what, &geometry.points[0])?;
let s = lookup(what, &geometry.points[1])?;
let e = lookup(what, &geometry.points[2])?;
atoms.push(Atom::EqualLength(c, s, c, e));
atom_owner.push(None);
}
"bezier" if geometry.points.len() >= 7 => {
let what = "bezier geometry";
let ids = &geometry.points;
let span_count = (ids.len() - 1) / 3;
let last_anchor = span_count * 3;
let mut i = 3usize;
while i < last_anchor {
let prev = lookup(what, &ids[i - 1])?;
let anchor = lookup(what, &ids[i])?;
let next = lookup(what, &ids[i + 1])?;
if prev != anchor && next != anchor && prev != next {
atoms.push(Atom::Lateral(anchor, prev, next));
atom_owner.push(None);
}
i += 3;
}
}
_ => {}
}
}
let free: Vec<usize> = (0..coords.len()).filter(|&i| !fixed[i]).collect();
let m: usize = atoms.iter().map(Atom::rows).sum();
let effective_rows: usize = atoms.iter().map(Atom::eff_rows).sum();
let mut scale = 1.0f64;
for c in &coords {
scale = scale.max(c.x.abs()).max(c.y.abs()).max(c.z.abs());
}
let tight = options.tolerance * scale;
Ok(Prepared {
coords,
free,
atoms,
atom_owner,
m,
effective_rows,
scale,
tight,
})
}
fn apply_delta(base: &[Vec3], free: &[usize], delta: &[f64], out: &mut Vec<Vec3>) {
out.clear();
out.extend_from_slice(base);
for (k, &i) in free.iter().enumerate() {
out[i] = base[i].add(Vec3::new(delta[3 * k], delta[3 * k + 1], delta[3 * k + 2]));
}
}
fn eval_residuals(
atoms: &[Atom],
base: &[Vec3],
free: &[usize],
delta: &[f64],
dir_eps: f64,
scratch: &mut Vec<Vec3>,
out: &mut Vec<f64>,
) {
apply_delta(base, free, delta, scratch);
out.clear();
for atom in atoms {
atom.eval(scratch, dir_eps, out);
}
}
fn finite_diff_jacobian(
atoms: &[Atom],
base: &[Vec3],
free: &[usize],
h: f64,
dir_eps: f64,
m: usize,
) -> Vec<Vec<f64>> {
let n = 3 * free.len();
let mut jac = vec![vec![0.0f64; n]; m];
if n == 0 || m == 0 {
return jac;
}
let mut delta = vec![0.0f64; n];
let mut scratch: Vec<Vec3> = Vec::with_capacity(base.len());
let mut rp: Vec<f64> = Vec::with_capacity(m);
let mut rm: Vec<f64> = Vec::with_capacity(m);
let inv = 1.0 / (2.0 * h);
for col in 0..n {
delta[col] = h;
eval_residuals(atoms, base, free, &delta, dir_eps, &mut scratch, &mut rp);
delta[col] = -h;
eval_residuals(atoms, base, free, &delta, dir_eps, &mut scratch, &mut rm);
delta[col] = 0.0;
for i in 0..m {
jac[i][col] = (rp[i] - rm[i]) * inv;
}
}
jac
}
fn lm_core(prep: &Prepared, coords: &mut Vec<Vec3>, max_iterations: usize) -> Result<usize, String> {
let atoms = &prep.atoms;
let free = &prep.free;
let n = 3 * free.len();
let m = prep.m;
let scale = prep.scale;
let dir_eps = DIR_EPS_REL * scale;
let h = FD_STEP * scale;
let step_tol = STEP_REL_TOL * scale;
let tight = prep.tight;
let max_abs = |v: &[f64]| v.iter().fold(0.0f64, |acc, &x| acc.max(x.abs()));
let sum_sq = |v: &[f64]| v.iter().fold(0.0f64, |acc, &x| acc + x * x);
let zeros = vec![0.0f64; n];
let mut scratch: Vec<Vec3> = Vec::with_capacity(coords.len());
let mut r: Vec<f64> = Vec::with_capacity(m);
eval_residuals(atoms, coords, free, &zeros, dir_eps, &mut scratch, &mut r);
if !max_abs(&r).is_finite() {
return Err("sketch3d: non-finite residual at the initial coordinates".into());
}
let mut iterations = 0usize;
let mut s = sum_sq(&r);
let mut best_coords = coords.clone();
let mut best_max = max_abs(&r);
let mut lambda = -1.0f64;
if n > 0 && m > 0 {
'outer: for _iter in 0..max_iterations {
if best_max <= tight {
break;
}
let jac = finite_diff_jacobian(atoms, coords, free, h, dir_eps, m);
let mut hm = vec![vec![0.0f64; n]; n];
let mut g = vec![0.0f64; n];
for i in 0..m {
let row = &jac[i];
let ri = r[i];
for a in 0..n {
let ja = row[a];
if ja == 0.0 {
continue;
}
g[a] += ja * ri;
for b in a..n {
hm[a][b] += ja * row[b];
}
}
}
for a in 0..n {
for b in (a + 1)..n {
hm[b][a] = hm[a][b];
}
}
let mut max_diag = 0.0f64;
for a in 0..n {
max_diag = max_diag.max(hm[a][a]);
}
if !(max_diag > 0.0) {
break;
}
if lambda < 0.0 {
lambda = 1e-9 * max_diag;
}
let lambda_ceiling = 1e12 * max_diag;
let mut stepped = false;
let mut step_small = false;
for _try in 0..MAX_DAMPING {
let mut a_mat = hm.clone();
for d in 0..n {
a_mat[d][d] += lambda;
}
let mut delta: Vec<f64> = g.iter().map(|v| -v).collect();
if !solve_spd(&mut a_mat, &mut delta) {
lambda *= LAMBDA_UP;
if lambda > lambda_ceiling {
break;
}
continue;
}
let mut r_trial: Vec<f64> = Vec::with_capacity(m);
eval_residuals(atoms, coords, free, &delta, dir_eps, &mut scratch, &mut r_trial);
let s_trial = sum_sq(&r_trial);
if s_trial.is_finite() && s_trial < s {
let step_norm = sum_sq(&delta).sqrt();
apply_delta(coords, free, &delta, &mut scratch);
coords.copy_from_slice(&scratch);
eval_residuals(atoms, coords, free, &zeros, dir_eps, &mut scratch, &mut r);
s = sum_sq(&r);
let m_now = max_abs(&r);
if m_now < best_max {
best_max = m_now;
best_coords.copy_from_slice(coords);
}
lambda = (lambda * LAMBDA_DOWN).max(1e-12 * max_diag);
iterations += 1;
stepped = true;
step_small = step_norm <= step_tol;
break;
}
lambda *= LAMBDA_UP;
if lambda > lambda_ceiling {
break;
}
}
if !stepped || step_small {
break 'outer;
}
}
}
coords.copy_from_slice(&best_coords);
Ok(iterations)
}
fn rank_and_nullspace(mut a: Vec<Vec<f64>>, m: usize, n: usize) -> (usize, Vec<Vec<f64>>) {
if n == 0 {
return (0, Vec::new());
}
let mut maxabs = 0.0f64;
for row in &a {
for &v in row {
maxabs = maxabs.max(v.abs());
}
}
let tol = if maxabs > 0.0 {
RANK_REL_TOL * maxabs
} else {
RANK_REL_TOL
};
let mut pivot_cols: Vec<usize> = Vec::new();
row_reduce(&mut a, m, n, tol, |col| pivot_cols.push(col));
let rank = pivot_cols.len();
let mut is_pivot = vec![false; n];
for &pc in &pivot_cols {
is_pivot[pc] = true;
}
let mut basis: Vec<Vec<f64>> = Vec::new();
for free_col in 0..n {
if is_pivot[free_col] {
continue;
}
let mut v = vec![0.0f64; n];
v[free_col] = 1.0;
for (i, &pc) in pivot_cols.iter().enumerate() {
v[pc] = -a[i][free_col];
}
basis.push(v);
}
(rank, basis)
}
pub fn solve_sketch3d_doc(
doc: &mut Sketch3dDoc,
options: &Sketch3dSolveOptions,
) -> Result<Sketch3dDiagnostics, String> {
let prep = prepare(doc, options)?;
let mut coords = prep.coords.clone();
let iterations = lm_core(&prep, &mut coords, options.max_iterations)?;
let n = 3 * prep.free.len();
let m = prep.m;
let dir_eps = DIR_EPS_REL * prep.scale;
let zeros = vec![0.0f64; n];
let mut scratch: Vec<Vec3> = Vec::with_capacity(coords.len());
let mut r: Vec<f64> = Vec::with_capacity(m);
eval_residuals(&prep.atoms, &coords, &prep.free, &zeros, dir_eps, &mut scratch, &mut r);
let jac = finite_diff_jacobian(&prep.atoms, &coords, &prep.free, FD_STEP * prep.scale, dir_eps, m);
let row_norms: Vec<f64> = jac
.iter()
.map(|row| row.iter().fold(0.0f64, |acc, &v| acc + v * v).sqrt())
.collect();
let max_row_norm = row_norms.iter().fold(0.0f64, |a, &v| a.max(v));
let norm_floor = if max_row_norm > 0.0 { 1e-9 * max_row_norm } else { 1.0 };
let scaled: Vec<f64> = r
.iter()
.zip(row_norms.iter())
.map(|(&v, &norm)| v / norm.max(norm_floor))
.collect();
let max_residual = scaled.iter().fold(0.0f64, |acc, &v| acc.max(v.abs()));
let (rank, basis) = rank_and_nullspace(jac, m, n);
let dof = n - rank;
let redundant = prep.effective_rows.saturating_sub(rank);
let conflict_tol = CONFLICT_REL_TOL * prep.scale;
let mut flagged = vec![false; doc.constraints.len()];
let mut row = 0usize;
for (ai, atom) in prep.atoms.iter().enumerate() {
let rows = atom.rows();
let violated = scaled[row..row + rows].iter().any(|v| v.abs() > conflict_tol);
if violated {
if let Some(owner) = prep.atom_owner[ai] {
flagged[owner] = true;
}
}
row += rows;
}
let mut conflicting_constraints: Vec<String> = Vec::new();
for (ci, &hit) in flagged.iter().enumerate() {
if hit {
if let Some(id) = doc.constraints[ci].id() {
conflicting_constraints.push(id_key(id));
}
}
}
if max_residual > conflict_tol && conflicting_constraints.is_empty() {
let mut row = 0usize;
for (ai, atom) in prep.atoms.iter().enumerate() {
let rows = atom.rows();
let violated = scaled[row..row + rows].iter().any(|v| v.abs() > conflict_tol);
row += rows;
if !violated || prep.atom_owner[ai].is_some() {
continue;
}
for (ci, constraint) in doc.constraints.iter().enumerate() {
if flagged[ci] {
continue;
}
if let Some(id) = constraint.id() {
flagged[ci] = true;
conflicting_constraints.push(id_key(id));
}
}
}
}
let conflicting = !conflicting_constraints.is_empty();
let status = if redundant > 0 {
"over"
} else if dof > 0 {
"under"
} else {
"well"
};
let basis_max: Vec<f64> = basis
.iter()
.map(|v| v.iter().fold(0.0f64, |a, &x| a.max(x.abs())).max(1e-300))
.collect();
let mut col_of: HashMap<usize, usize> = HashMap::with_capacity(prep.free.len());
for (k, &i) in prep.free.iter().enumerate() {
col_of.insert(i, k);
}
let mut point_mobility = std::collections::BTreeMap::new();
for (i, point) in doc.points.iter().enumerate() {
let movable = match col_of.get(&i) {
None => false,
Some(&k) => basis.iter().enumerate().any(|(b, v)| {
let c = 3 * k;
let comp = ((v[c] / basis_max[b]).powi(2)
+ (v[c + 1] / basis_max[b]).powi(2)
+ (v[c + 2] / basis_max[b]).powi(2))
.sqrt();
comp > MOBILITY_TOL
}),
};
point_mobility.insert(
id_key(&point.id),
(if movable { "movable" } else { "locked" }).to_string(),
);
}
let mut geometry_mobility = std::collections::BTreeMap::new();
for geometry in &doc.geometries {
let movable = geometry
.points
.iter()
.any(|pid| point_mobility.get(&id_key(pid)).map(String::as_str) == Some("movable"));
geometry_mobility.insert(
id_key(&geometry.id),
(if movable { "movable" } else { "locked" }).to_string(),
);
}
for (i, point) in doc.points.iter_mut().enumerate() {
point.x = coords[i].x;
point.y = coords[i].y;
point.z = coords[i].z;
}
Ok(Sketch3dDiagnostics {
dof: dof as i64,
rank: rank as i64,
unknowns: n as i64,
equations: m as i64,
redundant: redundant as i64,
status: status.into(),
conflicting,
conflicting_constraints,
point_mobility,
geometry_mobility,
iterations: iterations as i64,
max_residual,
})
}
pub fn solve_sketch3d(request: &SolveSketch3dRequest) -> Result<Value, String> {
let mut doc: Sketch3dDoc = serde_json::from_value(request.sketch.clone())
.map_err(|e| format!("sketch3d: invalid sketch document: {e}"))?;
let options = Sketch3dSolveOptions {
tolerance: request.tolerance.unwrap_or_else(default_tolerance),
max_iterations: request.max_iterations.unwrap_or_else(default_max_iterations),
};
let diagnostics = solve_sketch3d_doc(&mut doc, &options)?;
let mut sketch = serde_json::to_value(&doc).map_err(|e| format!("sketch3d: {e}"))?;
if let Some(map) = sketch.as_object_mut() {
map.insert(
"diagnostics".into(),
serde_json::to_value(&diagnostics).map_err(|e| format!("sketch3d: {e}"))?,
);
}
Ok(serde_json::json!({ "sketch": sketch }))
}
pub fn solve_sketch3d_from_json(request: &str) -> Result<String, String> {
let request: SolveSketch3dRequest =
serde_json::from_str(request).map_err(|e| format!("sketch3d: invalid request: {e}"))?;
let response = solve_sketch3d(&request)?;
serde_json::to_string(&response).map_err(|e| format!("sketch3d: {e}"))
}