Skip to main content

brep_render/sketch/
solve.rs

1//! Solve wiring — call the kernel's 2D constraint solver in-process.
2//!
3//! `brep-render` links the kernel crate directly (a plain Rust path dependency,
4//! for BOTH native and the wasm canvas build), so the solver is reached by a
5//! direct `brep_kernel::solve_sketch(&SolveSketchRequest)` call on every
6//! target — there is NO wasm-bindgen JSON hop (that boundary only existed for the
7//! previous app calling INTO the wasm module from the browser; here the solver is
8//! compiled into the same binary/module). The solver is pure numeric Rust with no `std::time`,
9//! so it is wasm-clean.
10//!
11//! The solver returns `{sketch: {points, geometries, constraints, diagnostics}}`.
12//! We split that into the solved [`SketchDoc`] (the three editable arrays) and the
13//! read-only [`SketchDiagnostics`] the overlay coloring + DOF readout consume.
14
15use super::doc::{SketchDiagnostics, SketchDoc};
16use brep_kernel::{solve_sketch, SolveSketchRequest};
17
18/// User-tunable 2D solver knobs (the "Solver Settings" section), all optional
19/// — `None` keeps the kernel's default. Held on the [`super::SketchSession`] and
20/// read by every `resolve`, so a change re-solves with the new settings without
21/// threading params through each edit path.
22#[derive(Clone, Debug, PartialEq)]
23pub struct SketchSolverSettings {
24    /// Convergence tolerance (kernel default when `None`).
25    pub tolerance: Option<f64>,
26    /// Max solver iterations (defaults to 1000 when `None`).
27    pub iterations: Option<u32>,
28    /// Distance-slide threshold ratio (kernel default when `None`).
29    pub distance_slide_threshold_ratio: Option<f64>,
30    /// Distance-slide step ratio (kernel default when `None`).
31    pub distance_slide_step_ratio: Option<f64>,
32    /// Distance-slide minimum step (kernel default when `None`).
33    pub distance_slide_min_step: Option<f64>,
34}
35
36impl Default for SketchSolverSettings {
37    fn default() -> Self {
38        // All `None`/default → byte-identical to the historical hard-coded request
39        // (iterations 1000, everything else the kernel default), so an untouched
40        // solver behaves exactly as before this knob existed.
41        Self {
42            tolerance: None,
43            iterations: Some(1000),
44            distance_slide_threshold_ratio: None,
45            distance_slide_step_ratio: None,
46            distance_slide_min_step: None,
47        }
48    }
49}
50
51/// Solve `doc` and return the solved coordinates plus the constraint diagnostics.
52///
53/// `remove_implied_duplicates` is left off (the display path never edits
54/// constraints, so we keep every authored constraint) and the Newton polish is
55/// left on (`polish: None` → default true) so the displayed coordinates match
56/// what a committed feature-pipeline solve would produce.
57pub fn solve(doc: &SketchDoc) -> Result<(SketchDoc, SketchDiagnostics), String> {
58    solve_with(doc, &SketchSolverSettings::default())
59}
60
61/// Like [`solve`], but with user-tunable [`SketchSolverSettings`] (the Solver
62/// Settings panel). Default settings reproduce [`solve`] byte-for-byte.
63pub fn solve_with(
64    doc: &SketchDoc,
65    settings: &SketchSolverSettings,
66) -> Result<(SketchDoc, SketchDiagnostics), String> {
67    let sketch = serde_json::to_value(doc).map_err(|e| format!("sketch serialize: {e}"))?;
68    let request = SolveSketchRequest {
69        sketch,
70        iterations: settings.iterations,
71        remove_implied_duplicates: false,
72        tolerance: settings.tolerance,
73        distance_slide_threshold_ratio: settings.distance_slide_threshold_ratio,
74        distance_slide_step_ratio: settings.distance_slide_step_ratio,
75        distance_slide_min_step: settings.distance_slide_min_step,
76        polish: None,
77    };
78
79    let response = solve_sketch(&request)?;
80    let solved = response
81        .get("sketch")
82        .cloned()
83        .ok_or_else(|| "solve_sketch: response missing `sketch`".to_string())?;
84
85    let diagnostics = match solved.get("diagnostics").cloned() {
86        Some(value) => {
87            serde_json::from_value(value).map_err(|e| format!("diagnostics parse: {e}"))?
88        }
89        None => SketchDiagnostics::default(),
90    };
91    let solved_doc: SketchDoc =
92        serde_json::from_value(solved).map_err(|e| format!("solved sketch parse: {e}"))?;
93
94    Ok((solved_doc, diagnostics))
95}