use rustc_hash::FxHashMap as HashMap;
use serde::Deserialize;
use serde_json::{Map, Value};
use std::sync::atomic::{AtomicU64, Ordering};
static GLOBAL_DISTANCE_SOLVE_CYCLE_ID: AtomicU64 = AtomicU64::new(0);
const POINT_LINE_DISTANCE_TYPE: &str = "↥";
#[derive(Clone, Copy, Debug)]
pub struct SketchSolverSettings {
pub tolerance: f64,
pub distance_slide_threshold_ratio: f64,
pub distance_slide_step_ratio: f64,
pub distance_slide_min_step: f64,
pub newton_polish: bool,
}
impl Default for SketchSolverSettings {
fn default() -> Self {
Self {
tolerance: 0.00001,
distance_slide_threshold_ratio: 0.10,
distance_slide_step_ratio: 0.10,
distance_slide_min_step: 0.001,
newton_polish: true,
}
}
}
pub const SKETCH_ORIGIN_POINT_ID: &str = "origin";
pub fn is_sketch_origin_point_id(id: &Value) -> bool {
id.as_str() == Some(SKETCH_ORIGIN_POINT_ID)
}
#[derive(Debug, Deserialize)]
pub struct SolveSketchRequest {
pub sketch: Value,
#[serde(default)]
pub iterations: Option<u32>,
#[serde(default)]
pub remove_implied_duplicates: bool,
#[serde(default)]
pub tolerance: Option<f64>,
#[serde(default)]
pub distance_slide_threshold_ratio: Option<f64>,
#[serde(default)]
pub distance_slide_step_ratio: Option<f64>,
#[serde(default)]
pub distance_slide_min_step: Option<f64>,
#[serde(default)]
pub polish: Option<bool>,
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
enum PointKey {
Num(u64),
Str(String),
Bool(bool),
Null,
}
fn point_key(value: &Value) -> PointKey {
match value {
Value::Number(number) => match number.as_f64() {
Some(float) => PointKey::Num(sig_bits(float)),
None => PointKey::Null,
},
Value::String(text) => PointKey::Str(text.clone()),
Value::Bool(flag) => PointKey::Bool(*flag),
_ => PointKey::Null,
}
}
const TANGENT_TYPE: &str = "⌒";
const CONCENTRIC_TYPE: &str = "◎";
const EQUAL_RADIUS_TYPE: &str = "⊜";
const COLLINEAR_TYPE: &str = "⋰";
const SYMMETRIC_TYPE: &str = "⋈";
const SPLINE_CURVATURE_TYPE: &str = "ϰ";
const SPLINE_FOOT_TANGENT_TYPE: &str = "∿";
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
enum CType {
Horizontal,
Vertical,
Distance,
PointLine,
EqualDistance,
Parallel,
Perpendicular,
Angle,
Coincident,
PointOnLine,
Midpoint,
Ground,
Tangent,
SplineCurvature,
SplineFootTangent,
Concentric,
EqualRadius,
Collinear,
Symmetric,
#[default]
Other,
}
fn ctype_from(type_str: &str) -> CType {
match type_str {
"━" => CType::Horizontal,
"│" => CType::Vertical,
"⟺" => CType::Distance,
POINT_LINE_DISTANCE_TYPE => CType::PointLine,
"⇌" => CType::EqualDistance,
"∥" => CType::Parallel,
"⟂" => CType::Perpendicular,
"∠" => CType::Angle,
"≡" => CType::Coincident,
"⏛" => CType::PointOnLine,
"⋯" => CType::Midpoint,
"⏚" => CType::Ground,
TANGENT_TYPE => CType::Tangent,
SPLINE_CURVATURE_TYPE => CType::SplineCurvature,
SPLINE_FOOT_TANGENT_TYPE => CType::SplineFootTangent,
CONCENTRIC_TYPE => CType::Concentric,
EQUAL_RADIUS_TYPE => CType::EqualRadius,
COLLINEAR_TYPE => CType::Collinear,
SYMMETRIC_TYPE => CType::Symmetric,
_ => CType::Other,
}
}
fn is_distance_ctype(ctype: CType) -> bool {
matches!(ctype, CType::Distance | CType::PointLine)
}
fn is_spline_geometry_type(type_str: &str) -> bool {
type_str == "bezier" || type_str == "spline"
}
type SigEntry = Option<(u64, u64, bool)>;
#[derive(Clone, Copy, Debug, Default)]
enum PrevSolve {
#[default]
Missing,
Num(f64),
OtherType,
}
#[derive(Clone, Debug, Default)]
enum PrevPoints {
#[default]
Missing,
PresentNonString,
Str(String),
Bits(Vec<SigEntry>, String),
}
#[derive(Debug, Default)]
struct HotConstraint {
raw: Map<String, Value>,
ctype: CType,
type_str: Option<String>,
point_ids: Vec<Value>,
point_idx: Vec<Option<usize>>,
points_written: bool,
temporary: bool,
value_num: Option<f64>,
value_parsed: f64,
value_nullish: bool,
value_cv: f64,
value_written: bool,
status_solved: bool,
status_written: bool,
error: Option<Value>,
error_written: bool,
prev_solve: PrevSolve,
prev_solve_written: bool,
prev_points: PrevPoints,
prev_points_written: bool,
req_target: Option<f64>,
req_target_written: bool,
app_target: Option<f64>,
app_target_written: bool,
throttle_true: bool,
throttle_truthy: bool,
throttle_written: bool,
pass_token: Option<String>,
pass_token_written: bool,
line_sign: f64,
line_sign_written: bool,
foot_t: Option<f64>,
foot_t_written: bool,
}
enum Filter<'a> {
All,
Type(CType, &'a str),
}
#[derive(Clone, Debug)]
struct EnginePoint {
id: Value,
x: f64,
y: f64,
fixed: bool,
construction: bool,
external_reference: bool,
}
type CResult = Result<Value, String>;
struct Engine {
points: Vec<EnginePoint>,
point_index: HashMap<PointKey, usize>,
geometries: Vec<Value>,
constraints: Vec<HotConstraint>,
settings: SketchSolverSettings,
pass_token: String,
sig_scratch_before: Vec<SigEntry>,
sig_scratch_after: Vec<SigEntry>,
}
#[path = "sketch_solver/jsnum.rs"]
mod jsnum;
pub use jsnum::fmt_id as sketch_id_key;
#[path = "sketch_solver/hot_constraint.rs"]
mod hot_constraint;
#[path = "sketch_solver/engine.rs"]
mod engine;
#[path = "sketch_solver/constraints_basic.rs"]
mod constraints_basic;
#[path = "sketch_solver/constraints_shape.rs"]
mod constraints_shape;
#[path = "sketch_solver/constraints_foot.rs"]
mod constraints_foot;
#[path = "sketch_solver/dof_polish.rs"]
mod dof_polish;
#[path = "sketch_solver/api.rs"]
mod api;
use constraints_foot::{
foot_in_span, solve_spline_foot, FootTarget, SpanControls, FOOT_EVAL_MARGIN,
};
use constraints_shape::{
circle_curvature_residual, circle_curvature_sign, joint_curvature_residual,
spline_end_curvature,
};
use jsnum::*;
pub use api::{solve_sketch, solve_sketch_from_json};
pub fn mint_sketch_id<'a>(ids: impl Iterator<Item = &'a Value>, skip: impl Fn(&Value) -> bool) -> Value {
let ids: Vec<&Value> = ids.filter(|id| !skip(id)).collect();
let mut max: Option<i64> = None;
for &id in &ids {
if let Some(n) = id_num(id) {
if n.is_finite() {
let i = n.floor() as i64;
max = Some(max.map_or(i, |m| m.max(i)));
}
}
}
let mut candidate = max.map(|m| m + 1).unwrap_or(ids.len() as i64);
let existing: std::collections::HashSet<String> = ids.iter().map(|&id| sketch_id_key(id)).collect();
while existing.contains(&sketch_id_key(&Value::from(candidate))) {
candidate += 1;
}
Value::from(candidate)
}
fn id_num(value: &Value) -> Option<f64> {
match value {
Value::Number(number) => number.as_f64(),
Value::String(text) => text.trim().parse::<f64>().ok(),
_ => None,
}
}