Skip to main content

brepkit_wasm/bindings/
sketch.rs

1//! 2D sketch constraint solver bindings.
2
3#![allow(clippy::missing_errors_doc)]
4
5use wasm_bindgen::prelude::*;
6
7use crate::error::WasmError;
8use crate::helpers::{json_f64, json_usize, parse_sketch_constraint};
9use crate::kernel::BrepKernel;
10use crate::state::SketchState;
11
12/// Build a `GcsSystem` from a `SketchState`, returning the system along with
13/// the `PointId` handles needed for result readback.
14///
15/// Shared between `sketch_solve` (when arcs are present) and `sketch_dof`.
16/// Result of building a GCS from sketch state.
17#[allow(dead_code)]
18struct GcsBuildResult {
19    sys: brepkit_operations::sketch::GcsSystem,
20    point_ids: Vec<brepkit_operations::sketch::PointId>,
21    arc_ids: Vec<brepkit_operations::sketch::ArcId>,
22    circle_ids: Vec<brepkit_operations::sketch::CircleId>,
23}
24
25fn build_gcs_from_state(sk: &SketchState) -> Result<GcsBuildResult, JsError> {
26    use brepkit_operations::sketch::GcsConstraint;
27
28    let mut sys = brepkit_operations::sketch::GcsSystem::new();
29
30    // Add points
31    let point_ids: Vec<brepkit_operations::sketch::PointId> = sk
32        .points
33        .iter()
34        .map(|p| {
35            sys.add_point(brepkit_operations::sketch::PointData {
36                x: p.x,
37                y: p.y,
38                fixed: p.fixed,
39            })
40        })
41        .collect();
42
43    // Add arcs
44    let mut arc_ids = Vec::with_capacity(sk.arcs.len());
45    for &(center, start, end) in &sk.arcs {
46        let aid = sys
47            .add_arc(point_ids[center], point_ids[start], point_ids[end])
48            .map_err(|e| WasmError::InvalidInput {
49                reason: format!("failed to add arc: {e}"),
50            })?;
51        arc_ids.push(aid);
52    }
53
54    // Add circles
55    let mut circle_ids = Vec::with_capacity(sk.circles.len());
56    for &(center, radius) in &sk.circles {
57        let cid =
58            sys.add_circle(point_ids[center], radius)
59                .map_err(|e| WasmError::InvalidInput {
60                    reason: format!("failed to add circle: {e}"),
61                })?;
62        circle_ids.push(cid);
63    }
64
65    // Implicit line cache for point-pair-based constraints
66    let mut line_cache: std::collections::HashMap<
67        (usize, usize),
68        brepkit_operations::sketch::LineId,
69    > = std::collections::HashMap::new();
70
71    let mut get_or_create_line = |sys: &mut brepkit_operations::sketch::GcsSystem,
72                                  ids: &[brepkit_operations::sketch::PointId],
73                                  a: usize,
74                                  b: usize|
75     -> Option<brepkit_operations::sketch::LineId> {
76        if let std::collections::hash_map::Entry::Vacant(e) = line_cache.entry((a, b))
77            && let Ok(lid) = sys.add_line(ids[a], ids[b])
78        {
79            e.insert(lid);
80        }
81        line_cache.get(&(a, b)).copied()
82    };
83
84    // Convert legacy constraints
85    for c in &sk.constraints {
86        let _ = match c {
87            brepkit_operations::sketch::Constraint::Coincident(a, b) => {
88                sys.add_constraint(GcsConstraint::Coincident(point_ids[*a], point_ids[*b]))
89            }
90            brepkit_operations::sketch::Constraint::Distance(a, b, d) => {
91                sys.add_constraint(GcsConstraint::Distance(point_ids[*a], point_ids[*b], *d))
92            }
93            brepkit_operations::sketch::Constraint::FixX(p, v) => {
94                sys.add_constraint(GcsConstraint::FixX(point_ids[*p], *v))
95            }
96            brepkit_operations::sketch::Constraint::FixY(p, v) => {
97                sys.add_constraint(GcsConstraint::FixY(point_ids[*p], *v))
98            }
99            brepkit_operations::sketch::Constraint::Horizontal(a, b) => {
100                if let Some(l) = get_or_create_line(&mut sys, &point_ids, *a, *b) {
101                    sys.add_constraint(GcsConstraint::Horizontal(l))
102                } else {
103                    continue;
104                }
105            }
106            brepkit_operations::sketch::Constraint::Vertical(a, b) => {
107                if let Some(l) = get_or_create_line(&mut sys, &point_ids, *a, *b) {
108                    sys.add_constraint(GcsConstraint::Vertical(l))
109                } else {
110                    continue;
111                }
112            }
113            brepkit_operations::sketch::Constraint::Angle(a, b, c, d, theta) => {
114                let l1 = get_or_create_line(&mut sys, &point_ids, *a, *b);
115                let l2 = get_or_create_line(&mut sys, &point_ids, *c, *d);
116                if let (Some(l1), Some(l2)) = (l1, l2) {
117                    sys.add_constraint(GcsConstraint::Angle(l1, l2, *theta))
118                } else {
119                    continue;
120                }
121            }
122            brepkit_operations::sketch::Constraint::Perpendicular(a, b, c, d) => {
123                let l1 = get_or_create_line(&mut sys, &point_ids, *a, *b);
124                let l2 = get_or_create_line(&mut sys, &point_ids, *c, *d);
125                if let (Some(l1), Some(l2)) = (l1, l2) {
126                    sys.add_constraint(GcsConstraint::Perpendicular(l1, l2))
127                } else {
128                    continue;
129                }
130            }
131            brepkit_operations::sketch::Constraint::Parallel(a, b, c, d) => {
132                let l1 = get_or_create_line(&mut sys, &point_ids, *a, *b);
133                let l2 = get_or_create_line(&mut sys, &point_ids, *c, *d);
134                if let (Some(l1), Some(l2)) = (l1, l2) {
135                    sys.add_constraint(GcsConstraint::Parallel(l1, l2))
136                } else {
137                    continue;
138                }
139            }
140        };
141    }
142
143    // Resolve deferred (arc-referencing) constraints now that we have real IDs
144    for val in &sk.deferred_constraints {
145        let gc = resolve_deferred_constraint(
146            val,
147            &point_ids,
148            &arc_ids,
149            &circle_ids,
150            &mut line_cache,
151            &mut sys,
152        )?;
153        let _ = sys
154            .add_constraint(gc)
155            .map_err(|e| WasmError::InvalidInput {
156                reason: format!("failed to add deferred constraint: {e}"),
157            })?;
158    }
159
160    Ok(GcsBuildResult {
161        sys,
162        point_ids,
163        arc_ids,
164        circle_ids,
165    })
166}
167
168/// Resolve a deferred constraint JSON value into a real `GcsConstraint`
169/// using the entity IDs created during `build_gcs_from_state`.
170fn resolve_deferred_constraint(
171    val: &serde_json::Value,
172    point_ids: &[brepkit_operations::sketch::PointId],
173    arc_ids: &[brepkit_operations::sketch::ArcId],
174    circle_ids: &[brepkit_operations::sketch::CircleId],
175    line_cache: &mut std::collections::HashMap<(usize, usize), brepkit_operations::sketch::LineId>,
176    sys: &mut brepkit_operations::sketch::GcsSystem,
177) -> Result<brepkit_operations::sketch::GcsConstraint, JsError> {
178    use brepkit_operations::sketch::GcsConstraint;
179
180    let ty = val["type"].as_str().unwrap_or("");
181
182    let get_point = |key: &str| -> Result<brepkit_operations::sketch::PointId, JsError> {
183        let idx = json_usize(val, key)?;
184        point_ids.get(idx).copied().ok_or_else(|| {
185            WasmError::InvalidInput {
186                reason: format!("point index {idx} out of range"),
187            }
188            .into()
189        })
190    };
191
192    let get_arc = |key: &str| -> Result<brepkit_operations::sketch::ArcId, JsError> {
193        let idx = json_usize(val, key)?;
194        arc_ids.get(idx).copied().ok_or_else(|| {
195            WasmError::InvalidInput {
196                reason: format!("arc index {idx} out of range"),
197            }
198            .into()
199        })
200    };
201
202    let get_circle = |key: &str| -> Result<brepkit_operations::sketch::CircleId, JsError> {
203        let idx = json_usize(val, key)?;
204        circle_ids.get(idx).copied().ok_or_else(|| {
205            WasmError::InvalidInput {
206                reason: format!("circle index {idx} out of range"),
207            }
208            .into()
209        })
210    };
211
212    let get_or_create_line = |p1: usize,
213                              p2: usize,
214                              cache: &mut std::collections::HashMap<
215        (usize, usize),
216        brepkit_operations::sketch::LineId,
217    >,
218                              s: &mut brepkit_operations::sketch::GcsSystem|
219     -> Result<brepkit_operations::sketch::LineId, JsError> {
220        if p1 >= point_ids.len() || p2 >= point_ids.len() {
221            return Err(WasmError::InvalidInput {
222                reason: "point index out of range in deferred constraint".to_string(),
223            }
224            .into());
225        }
226        let key = (p1, p2);
227        if let Some(&lid) = cache.get(&key) {
228            return Ok(lid);
229        }
230        let lid =
231            s.add_line(point_ids[p1], point_ids[p2])
232                .map_err(|e| WasmError::InvalidInput {
233                    reason: format!("failed to create line: {e}"),
234                })?;
235        cache.insert(key, lid);
236        Ok(lid)
237    };
238
239    match ty {
240        "pointOnArc" => {
241            let pt = get_point("point")?;
242            let arc = get_arc("arc")?;
243            Ok(GcsConstraint::PointOnArc(pt, arc))
244        }
245        "tangentLineArc" => {
246            let line_arr = val["line"]
247                .as_array()
248                .ok_or_else(|| WasmError::InvalidInput {
249                    reason: "tangentLineArc requires 'line' as [p1, p2]".into(),
250                })?;
251            if line_arr.len() != 2 {
252                return Err(WasmError::InvalidInput {
253                    reason: "tangentLineArc 'line' must have exactly 2 point indices".into(),
254                }
255                .into());
256            }
257            #[allow(clippy::cast_possible_truncation)]
258            let lp1 = line_arr[0]
259                .as_u64()
260                .ok_or_else(|| WasmError::InvalidInput {
261                    reason: "tangentLineArc line[0] must be an integer".into(),
262                })? as usize;
263            #[allow(clippy::cast_possible_truncation)]
264            let lp2 = line_arr[1]
265                .as_u64()
266                .ok_or_else(|| WasmError::InvalidInput {
267                    reason: "tangentLineArc line[1] must be an integer".into(),
268                })? as usize;
269            let lid = get_or_create_line(lp1, lp2, line_cache, sys)?;
270            let arc = get_arc("arc")?;
271            let pt = get_point("point")?;
272            Ok(GcsConstraint::TangentLineArc(lid, arc, pt))
273        }
274        "tangentArcArc" => {
275            let arc1 = get_arc("arc1")?;
276            let arc2 = get_arc("arc2")?;
277            let pt = get_point("point")?;
278            Ok(GcsConstraint::TangentArcArc(arc1, arc2, pt))
279        }
280        "equalRadiusArcArc" => {
281            let arc1 = get_arc("arc1")?;
282            let arc2 = get_arc("arc2")?;
283            Ok(GcsConstraint::EqualRadiusArcArc(arc1, arc2))
284        }
285        "equalRadiusArcCircle" => {
286            let arc = get_arc("arc")?;
287            let circle = get_circle("circle")?;
288            Ok(GcsConstraint::EqualRadiusArcCircle(arc, circle))
289        }
290        "arcLength" => {
291            let arc = get_arc("arc")?;
292            let v = json_f64(val, "value")?;
293            Ok(GcsConstraint::ArcLength(arc, v))
294        }
295        "concentricArcArc" => {
296            let arc1 = get_arc("arc1")?;
297            let arc2 = get_arc("arc2")?;
298            Ok(GcsConstraint::ConcentricArcArc(arc1, arc2))
299        }
300        "concentricArcCircle" => {
301            let arc = get_arc("arc")?;
302            let circle = get_circle("circle")?;
303            Ok(GcsConstraint::ConcentricArcCircle(arc, circle))
304        }
305        "pointOnCircle" => {
306            let pt = get_point("point")?;
307            let circle = get_circle("circle")?;
308            Ok(GcsConstraint::PointOnCircle(pt, circle))
309        }
310        _ => Err(WasmError::InvalidInput {
311            reason: format!("unknown constraint type: {ty}"),
312        }
313        .into()),
314    }
315}
316
317#[wasm_bindgen]
318impl BrepKernel {
319    /// Create a new empty sketch. Returns a sketch index.
320    #[wasm_bindgen(js_name = "sketchNew")]
321    pub fn sketch_new(&mut self) -> u32 {
322        self.sketches.push(SketchState::default());
323        #[allow(clippy::cast_possible_truncation)]
324        let idx = (self.sketches.len() - 1) as u32;
325        idx
326    }
327
328    /// Add a point to a sketch. Returns the point index.
329    #[wasm_bindgen(js_name = "sketchAddPoint")]
330    pub fn sketch_add_point(
331        &mut self,
332        sketch: u32,
333        x: f64,
334        y: f64,
335        fixed: bool,
336    ) -> Result<u32, JsError> {
337        let sk = self
338            .sketches
339            .get_mut(sketch as usize)
340            .ok_or(WasmError::InvalidHandle {
341                entity: "sketch",
342                index: sketch as usize,
343            })?;
344        let pt = if fixed {
345            brepkit_operations::sketch::SketchPoint::fixed(x, y)
346        } else {
347            brepkit_operations::sketch::SketchPoint::new(x, y)
348        };
349        sk.points.push(pt);
350        #[allow(clippy::cast_possible_truncation)]
351        Ok((sk.points.len() - 1) as u32)
352    }
353
354    /// Add an arc to a sketch (defined by center, start, end point indices).
355    /// Returns the arc index.
356    #[wasm_bindgen(js_name = "sketchAddArc")]
357    pub fn sketch_add_arc(
358        &mut self,
359        sketch: u32,
360        center_idx: u32,
361        start_idx: u32,
362        end_idx: u32,
363    ) -> Result<u32, JsError> {
364        let sk = self
365            .sketches
366            .get_mut(sketch as usize)
367            .ok_or(WasmError::InvalidHandle {
368                entity: "sketch",
369                index: sketch as usize,
370            })?;
371        let center = center_idx as usize;
372        let start = start_idx as usize;
373        let end = end_idx as usize;
374        if center >= sk.points.len() || start >= sk.points.len() || end >= sk.points.len() {
375            return Err(WasmError::InvalidInput {
376                reason: format!(
377                    "arc point index out of range (center={center}, start={start}, \
378                     end={end}, points={})",
379                    sk.points.len()
380                ),
381            }
382            .into());
383        }
384        sk.arcs.push((center, start, end));
385        #[allow(clippy::cast_possible_truncation)]
386        Ok((sk.arcs.len() - 1) as u32)
387    }
388
389    /// Add a circle to a sketch.
390    ///
391    /// `center_idx` must be a valid point index. Returns the circle index
392    /// (0-based) for use in circle-referencing constraints.
393    #[wasm_bindgen(js_name = "sketchAddCircle")]
394    pub fn sketch_add_circle(
395        &mut self,
396        sketch: u32,
397        center_idx: u32,
398        radius: f64,
399    ) -> Result<u32, JsError> {
400        let sk = self
401            .sketches
402            .get_mut(sketch as usize)
403            .ok_or(WasmError::InvalidHandle {
404                entity: "sketch",
405                index: sketch as usize,
406            })?;
407        let center = center_idx as usize;
408        if center >= sk.points.len() {
409            return Err(WasmError::InvalidInput {
410                reason: format!(
411                    "circle center index out of range (center={center}, points={})",
412                    sk.points.len()
413                ),
414            }
415            .into());
416        }
417        if radius <= 0.0 || !radius.is_finite() {
418            return Err(WasmError::InvalidInput {
419                reason: format!("circle radius must be positive and finite, got {radius}"),
420            }
421            .into());
422        }
423        sk.circles.push((center, radius));
424        #[allow(clippy::cast_possible_truncation)]
425        Ok((sk.circles.len() - 1) as u32)
426    }
427
428    /// Add a constraint to a sketch from a JSON string.
429    ///
430    /// Supports all legacy constraint types plus arc-referencing constraints:
431    /// `tangentLineArc`, `tangentArcArc`, `pointOnArc`, `equalRadiusArcArc`,
432    /// `arcLength`, `concentricArcArc`.
433    #[wasm_bindgen(js_name = "sketchAddConstraint")]
434    pub fn sketch_add_constraint(&mut self, sketch: u32, json: &str) -> Result<(), JsError> {
435        let sk = self
436            .sketches
437            .get_mut(sketch as usize)
438            .ok_or(WasmError::InvalidHandle {
439                entity: "sketch",
440                index: sketch as usize,
441            })?;
442        let val: serde_json::Value =
443            serde_json::from_str(json).map_err(|e| WasmError::InvalidInput {
444                reason: format!("invalid constraint JSON: {e}"),
445            })?;
446
447        // Try legacy constraint first; fall back to deferred (arc-aware) storage
448        match parse_sketch_constraint(&val) {
449            Ok(constraint) => {
450                sk.constraints.push(constraint);
451            }
452            Err(e) => {
453                // Only defer known arc constraint types — don't silently swallow parse errors
454                let ty = val.get("type").and_then(|v| v.as_str()).unwrap_or("");
455                let arc_types: &[&str] = &[
456                    "tangentLineArc",
457                    "tangentArcArc",
458                    "pointOnArc",
459                    "pointOnCircle",
460                    "equalRadiusArcArc",
461                    "equalRadiusArcCircle",
462                    "arcLength",
463                    "concentricArcArc",
464                    "concentricArcCircle",
465                ];
466                if arc_types.contains(&ty) {
467                    sk.deferred_constraints.push(val);
468                } else {
469                    return Err(WasmError::InvalidInput {
470                        reason: format!("failed to parse constraint: {e:?}"),
471                    }
472                    .into());
473                }
474            }
475        }
476        Ok(())
477    }
478
479    /// Solve the sketch constraints.
480    ///
481    /// Returns a JSON string with converged status, iteration count, point
482    /// positions, and arc definitions.
483    #[wasm_bindgen(js_name = "sketchSolve")]
484    pub fn sketch_solve(
485        &mut self,
486        sketch: u32,
487        max_iterations: u32,
488        tolerance: f64,
489    ) -> Result<String, JsError> {
490        let sk = self
491            .sketches
492            .get_mut(sketch as usize)
493            .ok_or(WasmError::InvalidHandle {
494                entity: "sketch",
495                index: sketch as usize,
496            })?;
497
498        // If no arcs, circles, or deferred constraints, use the fast legacy path
499        if sk.arcs.is_empty() && sk.circles.is_empty() && sk.deferred_constraints.is_empty() {
500            let mut sketch_obj = brepkit_operations::sketch::Sketch {
501                points: std::mem::take(&mut sk.points),
502                constraints: std::mem::take(&mut sk.constraints),
503            };
504            let result = sketch_obj.solve(max_iterations as usize, tolerance);
505            sk.points = sketch_obj.points;
506            sk.constraints = sketch_obj.constraints;
507            let (converged, iterations, max_residual) = match &result {
508                Ok(r) => (r.converged, r.iterations, Some(r.max_residual)),
509                Err(_) => (false, max_iterations as usize, None),
510            };
511            let pts: Vec<serde_json::Value> = sk
512                .points
513                .iter()
514                .map(|p| serde_json::json!([p.x, p.y]))
515                .collect();
516            return Ok(serde_json::json!({
517                "converged": converged,
518                "iterations": iterations,
519                "maxResidual": max_residual,
520                "points": pts,
521                "arcs": [],
522            })
523            .to_string());
524        }
525
526        // Full GcsSystem path (supports arcs + deferred constraints)
527        let gcs = build_gcs_from_state(sk)?;
528        let mut sys = gcs.sys;
529        let result = sys.solve(max_iterations as usize, tolerance);
530        let (converged, iterations, max_residual) = match &result {
531            Ok(r) => (r.converged, r.iterations, Some(r.max_residual)),
532            Err(_) => (false, max_iterations as usize, None),
533        };
534
535        // Write solved positions back
536        for (i, pid) in gcs.point_ids.iter().enumerate() {
537            if let Some(data) = sys.point(*pid) {
538                sk.points[i].x = data.x;
539                sk.points[i].y = data.y;
540            }
541        }
542
543        let pts: Vec<serde_json::Value> = sk
544            .points
545            .iter()
546            .map(|p| serde_json::json!([p.x, p.y]))
547            .collect();
548        let arcs: Vec<serde_json::Value> = sk
549            .arcs
550            .iter()
551            .map(|(c, s, e)| serde_json::json!({"center": c, "start": s, "end": e}))
552            .collect();
553        let circles: Vec<serde_json::Value> = sk
554            .circles
555            .iter()
556            .enumerate()
557            .map(|(i, &(center, _))| {
558                // Read solved radius from GCS
559                let radius = gcs
560                    .circle_ids
561                    .get(i)
562                    .and_then(|cid| sys.circle(*cid))
563                    .map_or(0.0, |c| c.radius);
564                serde_json::json!({"center": center, "radius": radius})
565            })
566            .collect();
567        Ok(serde_json::json!({
568            "converged": converged,
569            "iterations": iterations,
570            "maxResidual": max_residual,
571            "points": pts,
572            "arcs": arcs,
573            "circles": circles,
574        })
575        .to_string())
576    }
577
578    /// Compute degrees of freedom for a sketch.
579    ///
580    /// Returns a JSON string: `{"dof": n, "rank": n, "numParams": n, "numEquations": n}`.
581    #[wasm_bindgen(js_name = "sketchDof")]
582    pub fn sketch_dof(&mut self, sketch: u32) -> Result<String, JsError> {
583        let sk = self
584            .sketches
585            .get_mut(sketch as usize)
586            .ok_or(WasmError::InvalidHandle {
587                entity: "sketch",
588                index: sketch as usize,
589            })?;
590        let GcsBuildResult { mut sys, .. } = build_gcs_from_state(sk)?;
591        let dof = sys.dof();
592        Ok(serde_json::json!({
593            "dof": dof.dof,
594            "rank": dof.rank,
595            "numParams": dof.num_params,
596            "numEquations": dof.num_equations,
597        })
598        .to_string())
599    }
600}