Skip to main content

brep_kernel/solvers/sketch_solver/
api.rs

1use super::*;
2
3// ---------------------------------------------------------------------------
4// Implied-duplicate constraint removal (ConstraintSolver private helpers)
5// ---------------------------------------------------------------------------
6
7fn ordered_pair_key(a: i64, b: i64) -> String {
8    if a <= b {
9        format!("{a},{b}")
10    } else {
11        format!("{b},{a}")
12    }
13}
14
15fn constraint_signature(
16    constraint_type: &str,
17    points: &[Value],
18    canonical: &HashMap<i64, i64>,
19) -> Option<String> {
20    let canon = |value: &Value| -> Option<i64> {
21        let pid = js_parse_int(Some(value))?;
22        Some(*canonical.get(&pid).unwrap_or(&pid))
23    };
24    if constraint_type == "⇌" && points.len() >= 4 {
25        let p0 = canon(&points[0])?;
26        let p1 = canon(&points[1])?;
27        let p2 = canon(&points[2])?;
28        let p3 = canon(&points[3])?;
29        let a = ordered_pair_key(p0, p1);
30        let b = ordered_pair_key(p2, p3);
31        return Some(if a <= b {
32            format!("⇌:{a}|{b}")
33        } else {
34            format!("⇌:{b}|{a}")
35        });
36    }
37    if constraint_type == "⏛" && points.len() >= 3 {
38        let p0 = canon(&points[0])?;
39        let p1 = canon(&points[1])?;
40        let p2 = canon(&points[2])?;
41        return Some(format!("⏛:{}|{}", ordered_pair_key(p0, p1), p2));
42    }
43    None
44}
45
46fn build_coincident_canonical_map(constraints: &[Value]) -> HashMap<i64, i64> {
47    fn find(parent: &mut HashMap<i64, i64>, x: i64) -> i64 {
48        let p = *parent.entry(x).or_insert(x);
49        if p == x {
50            return x;
51        }
52        let root = find(parent, p);
53        parent.insert(x, root);
54        root
55    }
56
57    let mut parent: HashMap<i64, i64> = HashMap::default();
58    for constraint in constraints {
59        let Some(obj) = constraint.as_object() else {
60            continue;
61        };
62        if truthy(obj.get("temporary")) {
63            continue;
64        }
65        if obj.get("type").and_then(Value::as_str) != Some("≡") {
66            continue;
67        }
68        let Some(points) = obj.get("points").and_then(Value::as_array) else {
69            continue;
70        };
71        if points.len() < 2 {
72            continue;
73        }
74        let (Some(p0), Some(p1)) = (js_parse_int(points.first()), js_parse_int(points.get(1)))
75        else {
76            continue;
77        };
78        let ra = find(&mut parent, p0);
79        let rb = find(&mut parent, p1);
80        if ra != rb {
81            if ra < rb {
82                parent.insert(rb, ra);
83            } else {
84                parent.insert(ra, rb);
85            }
86        }
87    }
88    let keys: Vec<i64> = parent.keys().copied().collect();
89    let mut out = HashMap::default();
90    for key in keys {
91        let root = find(&mut parent, key);
92        out.insert(key, root);
93    }
94    out
95}
96
97fn remove_constraints_duplicated_by_implied_geometry(sketch: &mut Value) {
98    let Some(obj) = sketch.as_object_mut() else {
99        return;
100    };
101    let Some(geometries) = obj.get("geometries").and_then(Value::as_array).cloned() else {
102        return;
103    };
104    let Some(constraints) = obj.get("constraints").and_then(Value::as_array).cloned() else {
105        return;
106    };
107
108    let canonical = build_coincident_canonical_map(&constraints);
109
110    let mut implied: std::collections::HashSet<String> = std::collections::HashSet::new();
111    for geometry in &geometries {
112        let Some(geo) = geometry.as_object() else {
113            continue;
114        };
115        let Some(points) = geo.get("points").and_then(Value::as_array) else {
116            continue;
117        };
118        let geometry_type = geo.get("type").and_then(Value::as_str).unwrap_or("");
119        if geometry_type == "arc" && points.len() >= 3 {
120            let candidate = vec![
121                points[0].clone(),
122                points[1].clone(),
123                points[0].clone(),
124                points[2].clone(),
125            ];
126            if let Some(key) = constraint_signature("⇌", &candidate, &canonical) {
127                implied.insert(key);
128            }
129        } else if is_spline_geometry_type(geometry_type) && points.len() >= 4 {
130            let seg_count = (points.len() - 1) / 3;
131            let last_anchor_index = seg_count * 3;
132            let mut i = 3usize;
133            while i < last_anchor_index {
134                let prev_handle = &points[i - 1];
135                let anchor = &points[i];
136                let next_handle = &points[i + 1];
137                let nullish = |v: &Value| matches!(v, Value::Null);
138                if !nullish(prev_handle) && !nullish(anchor) && !nullish(next_handle) {
139                    let same = |a: &Value, b: &Value| point_key(a) == point_key(b);
140                    if !same(prev_handle, anchor)
141                        && !same(next_handle, anchor)
142                        && !same(prev_handle, next_handle)
143                    {
144                        let candidate =
145                            vec![prev_handle.clone(), next_handle.clone(), anchor.clone()];
146                        if let Some(key) = constraint_signature("⏛", &candidate, &canonical) {
147                            implied.insert(key);
148                        }
149                    }
150                }
151                i += 3;
152            }
153        }
154    }
155    if implied.is_empty() {
156        return;
157    }
158
159    let filtered: Vec<Value> = constraints
160        .into_iter()
161        .filter(|constraint| {
162            let Some(c) = constraint.as_object() else {
163                return true;
164            };
165            if truthy(c.get("temporary")) {
166                return true;
167            }
168            let constraint_type = c.get("type").and_then(Value::as_str).unwrap_or("");
169            let Some(points) = c.get("points").and_then(Value::as_array) else {
170                return true;
171            };
172            match constraint_signature(constraint_type, points, &canonical) {
173                Some(key) => !implied.contains(&key),
174                None => true,
175            }
176        })
177        .collect();
178    obj.insert("constraints".into(), Value::Array(filtered));
179}
180
181// ---------------------------------------------------------------------------
182// Public entry point
183// ---------------------------------------------------------------------------
184
185pub fn solve_sketch(request: &SolveSketchRequest) -> Result<Value, String> {
186    let mut settings = SketchSolverSettings::default();
187    if let Some(tolerance) = request.tolerance {
188        settings.tolerance = tolerance;
189    }
190    if let Some(value) = request.distance_slide_threshold_ratio {
191        if value.is_finite() && value >= 0.0 {
192            settings.distance_slide_threshold_ratio = value;
193        }
194    }
195    if let Some(value) = request.distance_slide_step_ratio {
196        if value.is_finite() && value >= 0.0 {
197            settings.distance_slide_step_ratio = value;
198        }
199    }
200    if let Some(value) = request.distance_slide_min_step {
201        if value.is_finite() && value >= 0.0 {
202            settings.distance_slide_min_step = value;
203        }
204    }
205    if let Some(polish) = request.polish {
206        settings.newton_polish = polish;
207    }
208
209    let mut sketch = request.sketch.clone();
210    if request.remove_implied_duplicates {
211        remove_constraints_duplicated_by_implied_geometry(&mut sketch);
212    }
213
214    let iterations = request.iterations.unwrap_or(1500);
215    let mut engine = Engine::new(&sketch, settings)?;
216    let solved = engine.solve(iterations)?;
217
218    let mut response = Map::new();
219    response.insert("sketch".into(), solved);
220    Ok(Value::Object(response))
221}
222
223pub fn solve_sketch_from_json(request_json: &str) -> Result<String, String> {
224    let request: SolveSketchRequest =
225        serde_json::from_str(request_json).map_err(|error| error.to_string())?;
226    let response = solve_sketch(&request)?;
227    serde_json::to_string(&response).map_err(|error| error.to_string())
228}