1#![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#[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 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 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 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 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 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 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
168fn 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 #[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 #[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 #[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 #[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 #[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 match parse_sketch_constraint(&val) {
449 Ok(constraint) => {
450 sk.constraints.push(constraint);
451 }
452 Err(e) => {
453 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 #[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 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 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 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 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 #[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}