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