Skip to main content

brepkit_wasm/bindings/
query.rs

1//! Topology query, edge/surface evaluation, and BREP introspection bindings.
2
3#![allow(clippy::missing_errors_doc, clippy::too_many_lines)]
4
5use std::f64::consts::PI;
6
7use wasm_bindgen::prelude::*;
8
9use brepkit_math::vec::{Point3, Vec3};
10use brepkit_topology::edge::EdgeCurve;
11use brepkit_topology::face::{Face, FaceSurface};
12
13use crate::error::{WasmError, validate_finite};
14use crate::handles::{
15    edge_id_to_u32, face_id_to_u32, shell_id_to_u32, solid_id_to_u32, vertex_id_to_u32,
16    wire_id_to_u32,
17};
18use brepkit_geometry::convert::{DetectedCurveKind, detect_curve_kind, detect_surface_kind};
19
20use crate::helpers::sample_full_period_curve;
21use crate::kernel::BrepKernel;
22
23#[wasm_bindgen]
24impl BrepKernel {
25    // ── Topology queries ──────────────────────────────────────────
26
27    /// Get all face handles of a solid.
28    ///
29    /// Returns an array of face handles (`u32[]`).
30    ///
31    /// # Errors
32    ///
33    /// Returns an error if the solid handle is invalid.
34    #[wasm_bindgen(js_name = "getSolidFaces")]
35    pub fn get_solid_faces(&self, solid: u32) -> Result<Vec<u32>, JsError> {
36        let solid_id = self.resolve_solid(solid)?;
37        let faces = brepkit_topology::explorer::solid_faces(&self.topo, solid_id)?;
38        #[allow(clippy::cast_possible_truncation)]
39        Ok(faces.iter().map(|f| f.index() as u32).collect())
40    }
41
42    /// Get all edge handles of a solid.
43    ///
44    /// Returns an array of unique edge handles (`u32[]`).
45    ///
46    /// # Errors
47    ///
48    /// Returns an error if the solid handle is invalid.
49    #[wasm_bindgen(js_name = "getSolidEdges")]
50    pub fn get_solid_edges(&self, solid: u32) -> Result<Vec<u32>, JsError> {
51        let solid_id = self.resolve_solid(solid)?;
52        let edges = brepkit_topology::explorer::solid_edges(&self.topo, solid_id)?;
53        #[allow(clippy::cast_possible_truncation)]
54        Ok(edges.iter().map(|e| e.index() as u32).collect())
55    }
56
57    /// Get all vertex handles of a solid.
58    ///
59    /// Returns an array of unique vertex handles (`u32[]`).
60    ///
61    /// # Errors
62    ///
63    /// Returns an error if the solid handle is invalid.
64    #[wasm_bindgen(js_name = "getSolidVertices")]
65    pub fn get_solid_vertices(&self, solid: u32) -> Result<Vec<u32>, JsError> {
66        let solid_id = self.resolve_solid(solid)?;
67        let verts = brepkit_topology::explorer::solid_vertices(&self.topo, solid_id)?;
68        #[allow(clippy::cast_possible_truncation)]
69        Ok(verts.iter().map(|v| v.index() as u32).collect())
70    }
71
72    /// Get all shell handles of a solid.
73    ///
74    /// Returns the outer shell first, followed by any inner void shells
75    /// (cavities produced by `shell`/hollow operations or boolean cuts).
76    /// A simple solid such as a box reports exactly one shell.
77    ///
78    /// # Errors
79    ///
80    /// Returns an error if the solid handle is invalid.
81    #[wasm_bindgen(js_name = "getSolidShells")]
82    pub fn get_solid_shells(&self, solid: u32) -> Result<Vec<u32>, JsError> {
83        let solid_id = self.resolve_solid(solid)?;
84        let solid_data = self.topo.solid(solid_id)?;
85        let mut shells = Vec::with_capacity(1 + solid_data.inner_shells().len());
86        shells.push(shell_id_to_u32(solid_data.outer_shell()));
87        shells.extend(
88            solid_data
89                .inner_shells()
90                .iter()
91                .map(|s| shell_id_to_u32(*s)),
92        );
93        Ok(shells)
94    }
95
96    /// Get the vertex positions of an edge.
97    ///
98    /// Returns `[start_x, start_y, start_z, end_x, end_y, end_z]`.
99    ///
100    /// # Errors
101    ///
102    /// Returns an error if the edge handle is invalid.
103    #[wasm_bindgen(js_name = "getEdgeVertices")]
104    pub fn get_edge_vertices(&self, edge: u32) -> Result<Vec<f64>, JsError> {
105        let edge_id = self.resolve_edge(edge)?;
106        let edge_data = self.topo.edge(edge_id)?;
107        let start = self.topo.vertex(edge_data.start())?.point();
108        let end = self.topo.vertex(edge_data.end())?.point();
109        Ok(vec![
110            start.x(),
111            start.y(),
112            start.z(),
113            end.x(),
114            end.y(),
115            end.z(),
116        ])
117    }
118
119    /// Get the vertex *handles* (not positions) of an edge.
120    ///
121    /// Returns `[start_vertex_handle, end_vertex_handle]`.
122    ///
123    /// # Errors
124    ///
125    /// Returns an error if the edge handle is invalid.
126    #[wasm_bindgen(js_name = "getEdgeVertexHandles")]
127    pub fn get_edge_vertex_handles(&self, edge: u32) -> Result<Vec<u32>, JsError> {
128        let edge_id = self.resolve_edge(edge)?;
129        let edge_data = self.topo.edge(edge_id)?;
130        Ok(vec![
131            vertex_id_to_u32(edge_data.start()),
132            vertex_id_to_u32(edge_data.end()),
133        ])
134    }
135
136    /// Get the position of a vertex.
137    ///
138    /// Returns `[x, y, z]`.
139    ///
140    /// # Errors
141    ///
142    /// Returns an error if the vertex handle is invalid.
143    #[wasm_bindgen(js_name = "getVertexPosition")]
144    pub fn get_vertex_position(&self, vertex: u32) -> Result<Vec<f64>, JsError> {
145        let vertex_id = self.resolve_vertex(vertex)?;
146        let point = self.topo.vertex(vertex_id)?.point();
147        Ok(vec![point.x(), point.y(), point.z()])
148    }
149
150    /// Export a solid as a BREP string (STEP format).
151    ///
152    /// Returns a STEP-formatted string containing the solid's B-Rep data.
153    /// Use `fromBREP` to reconstruct the solid from this string.
154    ///
155    /// # Errors
156    ///
157    /// Returns an error if the solid handle is invalid.
158    #[wasm_bindgen(js_name = "toBREP")]
159    pub fn to_brep(&self, solid: u32) -> Result<JsValue, JsError> {
160        let solid_id = self.resolve_solid(solid)?;
161        let step_str = brepkit_io::step::writer::write_step(&self.topo, &[solid_id])
162            .map_err(|e| JsError::new(&e.to_string()))?;
163        Ok(step_str.into())
164    }
165
166    /// Export a solid as a JSON-encoded BREP representation.
167    ///
168    /// Returns a JSON string with vertices, edges (with curve parameters),
169    /// and faces (with surface parameters). This is a brepkit-specific format
170    /// that preserves all analytic geometry types.
171    #[wasm_bindgen(js_name = "toBrepJson")]
172    #[allow(clippy::too_many_lines)]
173    pub fn to_brep_json(&self, solid: u32) -> Result<JsValue, JsError> {
174        let solid_id = self.resolve_solid(solid)?;
175        let faces = brepkit_topology::explorer::solid_faces(&self.topo, solid_id)?;
176        let edges = brepkit_topology::explorer::solid_edges(&self.topo, solid_id)?;
177        let verts = brepkit_topology::explorer::solid_vertices(&self.topo, solid_id)?;
178
179        let vert_json: Vec<serde_json::Value> = verts
180            .iter()
181            .map(|&vid| -> Result<serde_json::Value, JsError> {
182                let v = self.topo.vertex(vid)?;
183                let p = v.point();
184                Ok(serde_json::json!({
185                    "id": vertex_id_to_u32(vid),
186                    "position": [p.x(), p.y(), p.z()],
187                }))
188            })
189            .collect::<Result<_, _>>()?;
190
191        let edge_json: Vec<serde_json::Value> = edges
192            .iter()
193            .map(|&eid| -> Result<serde_json::Value, JsError> {
194                let e = self.topo.edge(eid)?;
195                let curve_type = match e.curve() {
196                    EdgeCurve::Line => "line",
197                    EdgeCurve::Circle(_) => "circle",
198                    EdgeCurve::Ellipse(_) => "ellipse",
199                    EdgeCurve::NurbsCurve(_) => "nurbs",
200                };
201                let curve_params = match e.curve() {
202                    EdgeCurve::Line => serde_json::json!(null),
203                    EdgeCurve::Circle(c) => serde_json::json!({
204                        "center": [c.center().x(), c.center().y(), c.center().z()],
205                        "axis": [c.normal().x(), c.normal().y(), c.normal().z()],
206                        "xAxis": [c.u_axis().x(), c.u_axis().y(), c.u_axis().z()],
207                        "radius": c.radius(),
208                    }),
209                    EdgeCurve::Ellipse(el) => serde_json::json!({
210                        "center": [el.center().x(), el.center().y(), el.center().z()],
211                        "axis": [el.normal().x(), el.normal().y(), el.normal().z()],
212                        "majorAxis": [el.u_axis().x(), el.u_axis().y(), el.u_axis().z()],
213                        "majorRadius": el.semi_major(),
214                        "minorRadius": el.semi_minor(),
215                    }),
216                    EdgeCurve::NurbsCurve(n) => serde_json::json!({
217                        "degree": n.degree(),
218                        "controlPoints": n.control_points().iter()
219                            .map(|p| [p.x(), p.y(), p.z()])
220                            .collect::<Vec<_>>(),
221                        "weights": n.weights().to_vec(),
222                        "knots": n.knots().to_vec(),
223                    }),
224                };
225                Ok(serde_json::json!({
226                    "id": edge_id_to_u32(eid),
227                    "curveType": curve_type,
228                    "curveParams": curve_params,
229                    "startVertex": vertex_id_to_u32(e.start()),
230                    "endVertex": vertex_id_to_u32(e.end()),
231                }))
232            })
233            .collect::<Result<_, _>>()?;
234
235        let face_json: Vec<serde_json::Value> = faces
236            .iter()
237            .map(|&fid| -> Result<serde_json::Value, JsError> {
238                let f = self.topo.face(fid)?;
239                let surface_type = match f.surface() {
240                    brepkit_topology::face::FaceSurface::Plane { .. } => "plane",
241                    brepkit_topology::face::FaceSurface::Nurbs(_) => "nurbs",
242                    brepkit_topology::face::FaceSurface::Cylinder(_) => "cylinder",
243                    brepkit_topology::face::FaceSurface::Cone(_) => "cone",
244                    brepkit_topology::face::FaceSurface::Sphere(_) => "sphere",
245                    brepkit_topology::face::FaceSurface::Torus(_) => "torus",
246                };
247                let surface_params = match f.surface() {
248                    FaceSurface::Plane { normal, d } => serde_json::json!({
249                        "normal": [normal.x(), normal.y(), normal.z()],
250                        "d": d,
251                    }),
252                    FaceSurface::Cylinder(c) => serde_json::json!({
253                        "origin": [c.origin().x(), c.origin().y(), c.origin().z()],
254                        "axis": [c.axis().x(), c.axis().y(), c.axis().z()],
255                        "refDir": [c.x_axis().x(), c.x_axis().y(), c.x_axis().z()],
256                        "radius": c.radius(),
257                    }),
258                    FaceSurface::Cone(c) => serde_json::json!({
259                        "apex": [c.apex().x(), c.apex().y(), c.apex().z()],
260                        "axis": [c.axis().x(), c.axis().y(), c.axis().z()],
261                        "refDir": [c.x_axis().x(), c.x_axis().y(), c.x_axis().z()],
262                        "halfAngle": c.half_angle(),
263                    }),
264                    FaceSurface::Sphere(s) => serde_json::json!({
265                        "center": [s.center().x(), s.center().y(), s.center().z()],
266                        "axis": [s.z_axis().x(), s.z_axis().y(), s.z_axis().z()],
267                        "radius": s.radius(),
268                    }),
269                    FaceSurface::Torus(t) => serde_json::json!({
270                        "center": [t.center().x(), t.center().y(), t.center().z()],
271                        "axis": [t.z_axis().x(), t.z_axis().y(), t.z_axis().z()],
272                        "majorRadius": t.major_radius(),
273                        "minorRadius": t.minor_radius(),
274                    }),
275                    FaceSurface::Nurbs(n) => {
276                        let cps: Vec<Vec<serde_json::Value>> = n
277                            .control_points()
278                            .iter()
279                            .map(|row| {
280                                row.iter()
281                                    .map(|p| serde_json::json!([p.x(), p.y(), p.z()]))
282                                    .collect()
283                            })
284                            .collect();
285                        serde_json::json!({
286                            "degreeU": n.degree_u(),
287                            "degreeV": n.degree_v(),
288                            "controlPoints": cps,
289                            "weights": n.weights(),
290                            "knotsU": n.knots_u(),
291                            "knotsV": n.knots_v(),
292                        })
293                    }
294                };
295                let outer_wire = self.topo.wire(f.outer_wire())?;
296                let outer_edges: Vec<u32> = outer_wire
297                    .edges()
298                    .iter()
299                    .map(|e| edge_id_to_u32(e.edge()))
300                    .collect();
301                let outer_edge_orientations: Vec<bool> = outer_wire
302                    .edges()
303                    .iter()
304                    .map(brepkit_topology::wire::OrientedEdge::is_forward)
305                    .collect();
306                let inner_wires: Vec<serde_json::Value> = f
307                    .inner_wires()
308                    .iter()
309                    .filter_map(|&wid| {
310                        self.topo.wire(wid).ok().map(|w| {
311                            let edges: Vec<u32> =
312                                w.edges().iter().map(|e| edge_id_to_u32(e.edge())).collect();
313                            let orientations: Vec<bool> = w
314                                .edges()
315                                .iter()
316                                .map(brepkit_topology::wire::OrientedEdge::is_forward)
317                                .collect();
318                            serde_json::json!({
319                                "edges": edges,
320                                "orientations": orientations,
321                            })
322                        })
323                    })
324                    .collect();
325                Ok(serde_json::json!({
326                    "id": face_id_to_u32(fid),
327                    "surfaceType": surface_type,
328                    "surfaceParams": surface_params,
329                    "reversed": f.is_reversed(),
330                    "outerWireEdges": outer_edges,
331                    "outerWireOrientations": outer_edge_orientations,
332                    "innerWires": inner_wires,
333                }))
334            })
335            .collect::<Result<_, _>>()?;
336
337        Ok(serde_json::to_string(&serde_json::json!({
338            "type": "solid",
339            "solidId": solid_id_to_u32(solid_id),
340            "vertices": vert_json,
341            "edges": edge_json,
342            "faces": face_json,
343        }))
344        .map_err(|e| JsError::new(&e.to_string()))?
345        .into())
346    }
347
348    /// Reconstruct a solid from a BREP string.
349    ///
350    /// Accepts both STEP format (from `toBREP`) and JSON format (from
351    /// `toBrepJson`). Auto-detects the format: strings starting with `{`
352    /// are parsed as JSON, otherwise as STEP.
353    ///
354    /// Only single-solid STEP files are supported. Multi-solid files will
355    /// return only the first solid.
356    ///
357    /// # Errors
358    ///
359    /// Returns an error if the data is invalid or reconstruction fails.
360    #[wasm_bindgen(js_name = "fromBREP")]
361    #[allow(clippy::wrong_self_convention)]
362    pub fn from_brep(&mut self, data: &str) -> Result<u32, JsError> {
363        let trimmed = data.trim_start();
364        if trimmed.starts_with('{') {
365            // JSON BREP format
366            Ok(self.from_brep_impl(data)?)
367        } else {
368            // STEP format — delegate to STEP import
369            let solids = brepkit_io::step::reader::read_step(data, self.topo_mut())
370                .map_err(|e| JsError::new(&e.to_string()))?;
371            let first = solids
372                .first()
373                .ok_or_else(|| JsError::new("fromBREP: STEP data produced no solids"))?;
374            #[allow(clippy::cast_possible_truncation)]
375            Ok(first.index() as u32)
376        }
377    }
378
379    /// Get the face normal of a planar face.
380    ///
381    /// Returns `[nx, ny, nz]`.
382    ///
383    /// # Errors
384    ///
385    /// Returns an error if the face is invalid or NURBS.
386    #[wasm_bindgen(js_name = "getFaceNormal")]
387    pub fn get_face_normal(&self, face: u32) -> Result<Vec<f64>, JsError> {
388        let face_id = self.resolve_face(face)?;
389        let face_data = self.topo.face(face_id)?;
390        match face_data.surface() {
391            brepkit_topology::face::FaceSurface::Plane { normal, .. } => {
392                Ok(vec![normal.x(), normal.y(), normal.z()])
393            }
394            _ => Err(WasmError::InvalidInput {
395                reason: "getFaceNormal only works on planar faces".into(),
396            }
397            .into()),
398        }
399    }
400
401    /// Get entity counts of a solid: `[faces, edges, vertices]`.
402    ///
403    /// # Errors
404    ///
405    /// Returns an error if the solid handle is invalid.
406    #[wasm_bindgen(js_name = "getEntityCounts")]
407    pub fn get_entity_counts(&self, solid: u32) -> Result<Vec<u32>, JsError> {
408        let solid_id = self.resolve_solid(solid)?;
409        let (f, e, v) = brepkit_topology::explorer::solid_entity_counts(&self.topo, solid_id)?;
410        #[allow(clippy::cast_possible_truncation)]
411        Ok(vec![f as u32, e as u32, v as u32])
412    }
413
414    // ── Topology queries (extended) ──────────────────────────────
415
416    /// Get the edge handles of a face.
417    ///
418    /// Returns an array of edge handles (`u32[]`).
419    #[wasm_bindgen(js_name = "getFaceEdges")]
420    pub fn get_face_edges(&self, face: u32) -> Result<Vec<u32>, JsError> {
421        let face_id = self.resolve_face(face)?;
422        let edges = brepkit_topology::explorer::face_edges(&self.topo, face_id)?;
423        #[allow(clippy::cast_possible_truncation)]
424        Ok(edges.iter().map(|e| e.index() as u32).collect())
425    }
426
427    /// Get the vertex handles of a face.
428    ///
429    /// Returns an array of vertex handles (`u32[]`).
430    #[wasm_bindgen(js_name = "getFaceVertices")]
431    pub fn get_face_vertices(&self, face: u32) -> Result<Vec<u32>, JsError> {
432        let face_id = self.resolve_face(face)?;
433        let verts = brepkit_topology::explorer::face_vertices(&self.topo, face_id)?;
434        #[allow(clippy::cast_possible_truncation)]
435        Ok(verts.iter().map(|v| v.index() as u32).collect())
436    }
437
438    /// Get the outer wire handle of a face.
439    ///
440    /// Returns a wire handle (`u32`).
441    #[wasm_bindgen(js_name = "getFaceOuterWire")]
442    pub fn get_face_outer_wire(&self, face: u32) -> Result<u32, JsError> {
443        let face_id = self.resolve_face(face)?;
444        let face_data = self.topo.face(face_id)?;
445        Ok(wire_id_to_u32(face_data.outer_wire()))
446    }
447
448    /// Get all wires of a face (outer wire first, then inner/hole wires).
449    ///
450    /// # Errors
451    /// Returns an error if the face handle is invalid.
452    #[wasm_bindgen(js_name = "getFaceWires")]
453    pub fn get_face_wires(&self, face: u32) -> Result<Vec<u32>, JsError> {
454        let face_id = self.resolve_face(face)?;
455        let face_data = self.topo.face(face_id)?;
456        let mut wires = vec![wire_id_to_u32(face_data.outer_wire())];
457        for &iw in face_data.inner_wires() {
458            wires.push(wire_id_to_u32(iw));
459        }
460        Ok(wires)
461    }
462
463    /// Get the surface type of a face.
464    ///
465    /// Returns one of: `"plane"`, `"cylinder"`, `"cone"`, `"sphere"`,
466    /// `"torus"`, `"bspline"`.
467    ///
468    /// For NURBS surfaces that exactly represent analytic shapes, this
469    /// returns the underlying analytic type (e.g. `"sphere"` for a NURBS
470    /// sphere patch).
471    #[wasm_bindgen(js_name = "getSurfaceType")]
472    pub fn get_surface_type(&self, face: u32) -> Result<String, JsError> {
473        let face_id = self.resolve_face(face)?;
474        let face_data = self.topo.face(face_id)?;
475        Ok(match face_data.surface() {
476            FaceSurface::Plane { .. } => "plane",
477            FaceSurface::Nurbs(ns) => detect_surface_kind(ns).as_str(),
478            FaceSurface::Cylinder(_) => "cylinder",
479            FaceSurface::Cone(_) => "cone",
480            FaceSurface::Sphere(_) => "sphere",
481            FaceSurface::Torus(_) => "torus",
482        }
483        .into())
484    }
485
486    /// Get the curve type of an edge.
487    ///
488    /// Returns `"LINE"`, `"BSPLINE_CURVE"`, `"CIRCLE"`, or `"ELLIPSE"`.
489    ///
490    /// For NURBS curves that exactly represent analytic curves, this
491    /// returns the underlying analytic type (e.g. `"CIRCLE"` for a
492    /// rational NURBS circle).
493    #[wasm_bindgen(js_name = "getEdgeCurveType")]
494    pub fn get_edge_curve_type(&self, edge: u32) -> Result<String, JsError> {
495        let edge_id = self.resolve_edge(edge)?;
496        let edge_data = self.topo.edge(edge_id)?;
497        Ok(match edge_data.curve() {
498            EdgeCurve::Line => "LINE",
499            EdgeCurve::NurbsCurve(nc) => match detect_curve_kind(nc) {
500                DetectedCurveKind::Line => "LINE",
501                DetectedCurveKind::Circle => "CIRCLE",
502                DetectedCurveKind::BSpline => "BSPLINE_CURVE",
503            },
504            EdgeCurve::Circle(_) => "CIRCLE",
505            EdgeCurve::Ellipse(_) => "ELLIPSE",
506        }
507        .into())
508    }
509
510    /// Get the parameter domain of an edge curve.
511    ///
512    /// Returns `[t_start, t_end]`.
513    /// For line edges: `[0.0, length]`.
514    /// For NURBS edges: knot domain.
515    #[wasm_bindgen(js_name = "getEdgeCurveParameters")]
516    pub fn get_edge_curve_parameters(&self, edge: u32) -> Result<Vec<f64>, JsError> {
517        let edge_id = self.resolve_edge(edge)?;
518        let edge_data = self.topo.edge(edge_id)?;
519        match edge_data.curve() {
520            EdgeCurve::Line => {
521                let start = self.topo.vertex(edge_data.start())?.point();
522                let end = self.topo.vertex(edge_data.end())?.point();
523                let len = (end - start).length();
524                Ok(vec![0.0, len])
525            }
526            EdgeCurve::NurbsCurve(curve) => {
527                let (u_start, u_end) = curve.domain();
528                Ok(vec![u_start, u_end])
529            }
530            EdgeCurve::Circle(_) | EdgeCurve::Ellipse(_) => Ok(vec![0.0, std::f64::consts::TAU]),
531        }
532    }
533
534    /// Evaluate a point on an edge curve at parameter `t`.
535    ///
536    /// Returns `[x, y, z]`.
537    #[wasm_bindgen(js_name = "evaluateEdgeCurve")]
538    pub fn evaluate_edge_curve(&self, edge: u32, t: f64) -> Result<Vec<f64>, JsError> {
539        validate_finite(t, "t")?;
540        let edge_id = self.resolve_edge(edge)?;
541        let edge_data = self.topo.edge(edge_id)?;
542        let point = match edge_data.curve() {
543            EdgeCurve::Line => {
544                let start = self.topo.vertex(edge_data.start())?.point();
545                let end = self.topo.vertex(edge_data.end())?.point();
546                let len = (end - start).length();
547                if len < 1e-15 {
548                    start
549                } else {
550                    let frac = t / len;
551                    let dir = end - start;
552                    Point3::new(
553                        start.x() + dir.x() * frac,
554                        start.y() + dir.y() * frac,
555                        start.z() + dir.z() * frac,
556                    )
557                }
558            }
559            EdgeCurve::NurbsCurve(curve) => curve.evaluate(t),
560            EdgeCurve::Circle(circle) => circle.evaluate(t),
561            EdgeCurve::Ellipse(ellipse) => ellipse.evaluate(t),
562        };
563        Ok(vec![point.x(), point.y(), point.z()])
564    }
565
566    /// Evaluate a point and tangent on an edge curve at parameter `t`.
567    ///
568    /// Returns `[px, py, pz, tx, ty, tz]`.
569    #[wasm_bindgen(js_name = "evaluateEdgeCurveD1")]
570    pub fn evaluate_edge_curve_d1(&self, edge: u32, t: f64) -> Result<Vec<f64>, JsError> {
571        validate_finite(t, "t")?;
572        let edge_id = self.resolve_edge(edge)?;
573        let edge_data = self.topo.edge(edge_id)?;
574        match edge_data.curve() {
575            EdgeCurve::Line => {
576                let start = self.topo.vertex(edge_data.start())?.point();
577                let end = self.topo.vertex(edge_data.end())?.point();
578                let dir = end - start;
579                let len = dir.length();
580                let frac = if len < 1e-15 { 0.0 } else { t / len };
581                let point = Point3::new(
582                    start.x() + dir.x() * frac,
583                    start.y() + dir.y() * frac,
584                    start.z() + dir.z() * frac,
585                );
586                let tangent = if len < 1e-15 {
587                    Vec3::new(1.0, 0.0, 0.0)
588                } else {
589                    Vec3::new(dir.x() / len, dir.y() / len, dir.z() / len)
590                };
591                Ok(vec![
592                    point.x(),
593                    point.y(),
594                    point.z(),
595                    tangent.x(),
596                    tangent.y(),
597                    tangent.z(),
598                ])
599            }
600            EdgeCurve::NurbsCurve(curve) => {
601                let point = curve.evaluate(t);
602                let derivs = curve.derivatives(t, 1);
603                let tangent = if derivs.len() > 1 {
604                    derivs[1]
605                } else {
606                    Vec3::new(1.0, 0.0, 0.0)
607                };
608                Ok(vec![
609                    point.x(),
610                    point.y(),
611                    point.z(),
612                    tangent.x(),
613                    tangent.y(),
614                    tangent.z(),
615                ])
616            }
617            EdgeCurve::Circle(circle) => {
618                let point = circle.evaluate(t);
619                let tangent = circle.tangent(t);
620                Ok(vec![
621                    point.x(),
622                    point.y(),
623                    point.z(),
624                    tangent.x(),
625                    tangent.y(),
626                    tangent.z(),
627                ])
628            }
629            EdgeCurve::Ellipse(ellipse) => {
630                let point = ellipse.evaluate(t);
631                let tangent = ellipse.tangent(t);
632                Ok(vec![
633                    point.x(),
634                    point.y(),
635                    point.z(),
636                    tangent.x(),
637                    tangent.y(),
638                    tangent.z(),
639                ])
640            }
641        }
642    }
643
644    /// Measure curvature of an edge curve at parameter `t`.
645    ///
646    /// Returns `[curvature, tangent_x, tangent_y, tangent_z, normal_x, normal_y, normal_z]`.
647    /// Curvature is 1/radius. For lines, curvature is 0.
648    #[wasm_bindgen(js_name = "measureCurvatureAtEdge")]
649    pub fn measure_curvature_at_edge(&self, edge: u32, t: f64) -> Result<Vec<f64>, JsError> {
650        validate_finite(t, "t")?;
651        let edge_id = self.resolve_edge(edge)?;
652        let edge_data = self.topo.edge(edge_id)?;
653        match edge_data.curve() {
654            EdgeCurve::Line => {
655                let start = self.topo.vertex(edge_data.start())?.point();
656                let end = self.topo.vertex(edge_data.end())?.point();
657                let dir = end - start;
658                let len = dir.length();
659                let tangent = if len < 1e-15 {
660                    Vec3::new(1.0, 0.0, 0.0)
661                } else {
662                    Vec3::new(dir.x() / len, dir.y() / len, dir.z() / len)
663                };
664                Ok(vec![
665                    0.0,
666                    tangent.x(),
667                    tangent.y(),
668                    tangent.z(),
669                    0.0,
670                    0.0,
671                    0.0,
672                ])
673            }
674            EdgeCurve::NurbsCurve(curve) => {
675                let curvature = curve.curvature(t).unwrap_or(0.0);
676                let derivs = curve.derivatives(t, 2);
677                let tangent = if derivs.len() > 1 {
678                    derivs[1].normalize().unwrap_or(Vec3::new(1.0, 0.0, 0.0))
679                } else {
680                    Vec3::new(1.0, 0.0, 0.0)
681                };
682                let normal = if derivs.len() > 2 {
683                    let d1 = derivs[1];
684                    let d2 = derivs[2];
685                    let cross = d1.cross(d2);
686                    let binormal = cross.normalize().unwrap_or(Vec3::new(0.0, 0.0, 1.0));
687                    binormal
688                        .cross(tangent)
689                        .normalize()
690                        .unwrap_or(Vec3::new(0.0, 1.0, 0.0))
691                } else {
692                    Vec3::new(0.0, 1.0, 0.0)
693                };
694                Ok(vec![
695                    curvature,
696                    tangent.x(),
697                    tangent.y(),
698                    tangent.z(),
699                    normal.x(),
700                    normal.y(),
701                    normal.z(),
702                ])
703            }
704            EdgeCurve::Circle(circle) => {
705                let curvature = 1.0 / circle.radius();
706                let tangent = circle
707                    .tangent(t)
708                    .normalize()
709                    .unwrap_or(Vec3::new(1.0, 0.0, 0.0));
710                let point = circle.evaluate(t);
711                let to_center = Vec3::new(
712                    circle.center().x() - point.x(),
713                    circle.center().y() - point.y(),
714                    circle.center().z() - point.z(),
715                );
716                let normal = to_center.normalize().unwrap_or(Vec3::new(0.0, 1.0, 0.0));
717                Ok(vec![
718                    curvature,
719                    tangent.x(),
720                    tangent.y(),
721                    tangent.z(),
722                    normal.x(),
723                    normal.y(),
724                    normal.z(),
725                ])
726            }
727            EdgeCurve::Ellipse(ellipse) => {
728                let point = ellipse.evaluate(t);
729                let tangent = ellipse
730                    .tangent(t)
731                    .normalize()
732                    .unwrap_or(Vec3::new(1.0, 0.0, 0.0));
733                // Approximate curvature from finite differences
734                let dt = 1e-6;
735                let p0 = ellipse.evaluate(t - dt);
736                let p1 = ellipse.evaluate(t + dt);
737                let d1 = p1 - p0;
738                let d2 = (p1 - point) - (point - p0);
739                let speed = d1.length() / (2.0 * dt);
740                let curvature = if speed > 1e-15 {
741                    d1.cross(d2).length() / ((2.0 * dt) * speed * speed * speed)
742                } else {
743                    0.0
744                };
745                let normal = Vec3::new(
746                    ellipse.center().x() - point.x(),
747                    ellipse.center().y() - point.y(),
748                    ellipse.center().z() - point.z(),
749                )
750                .normalize()
751                .unwrap_or(Vec3::new(0.0, 1.0, 0.0));
752                Ok(vec![
753                    curvature,
754                    tangent.x(),
755                    tangent.y(),
756                    tangent.z(),
757                    normal.x(),
758                    normal.y(),
759                    normal.z(),
760                ])
761            }
762        }
763    }
764
765    /// Evaluate a surface normal at (u, v) on a face.
766    ///
767    /// Returns `[nx, ny, nz]`.
768    #[wasm_bindgen(js_name = "evaluateSurfaceNormal")]
769    pub fn evaluate_surface_normal(&self, face: u32, u: f64, v: f64) -> Result<Vec<f64>, JsError> {
770        let face_id = self.resolve_face(face)?;
771        let face_data = self.topo.face(face_id)?;
772        match face_data.surface() {
773            FaceSurface::Plane { normal, .. } => Ok(vec![normal.x(), normal.y(), normal.z()]),
774            FaceSurface::Nurbs(surface) => {
775                let derivs = surface.derivatives(u, v, 1);
776                let du = if derivs.len() > 1 && !derivs[1].is_empty() {
777                    derivs[1][0]
778                } else {
779                    Vec3::new(1.0, 0.0, 0.0)
780                };
781                let dv = if !derivs.is_empty() && derivs[0].len() > 1 {
782                    derivs[0][1]
783                } else {
784                    Vec3::new(0.0, 1.0, 0.0)
785                };
786                let n = du.cross(dv);
787                match n.normalize() {
788                    Ok(normal) => Ok(vec![normal.x(), normal.y(), normal.z()]),
789                    Err(_) => Ok(vec![0.0, 0.0, 1.0]),
790                }
791            }
792            FaceSurface::Cylinder(cyl) => {
793                let n = cyl.normal(u, v);
794                Ok(vec![n.x(), n.y(), n.z()])
795            }
796            FaceSurface::Cone(cone) => {
797                let n = cone.normal(u, v);
798                Ok(vec![n.x(), n.y(), n.z()])
799            }
800            FaceSurface::Sphere(sph) => {
801                let n = sph.normal(u, v);
802                Ok(vec![n.x(), n.y(), n.z()])
803            }
804            FaceSurface::Torus(tor) => {
805                let n = tor.normal(u, v);
806                Ok(vec![n.x(), n.y(), n.z()])
807            }
808        }
809    }
810
811    /// Evaluate a point on a face surface at (u, v).
812    ///
813    /// Returns `[x, y, z]`.
814    #[wasm_bindgen(js_name = "evaluateSurface")]
815    pub fn evaluate_surface(&self, face: u32, u: f64, v: f64) -> Result<Vec<f64>, JsError> {
816        let face_id = self.resolve_face(face)?;
817        let face_data = self.topo.face(face_id)?;
818        let point = match face_data.surface() {
819            FaceSurface::Plane { normal, d } => {
820                // Build a point on the plane: p = d * normal + u * x_axis + v * y_axis
821                // Choose arbitrary axes perpendicular to normal
822                let up = if normal.x().abs() < 0.9 {
823                    Vec3::new(1.0, 0.0, 0.0)
824                } else {
825                    Vec3::new(0.0, 1.0, 0.0)
826                };
827                let x_axis = normal.cross(up);
828                let y_axis = normal.cross(x_axis);
829                Point3::new(
830                    normal.x() * d + x_axis.x() * u + y_axis.x() * v,
831                    normal.y() * d + x_axis.y() * u + y_axis.y() * v,
832                    normal.z() * d + x_axis.z() * u + y_axis.z() * v,
833                )
834            }
835            FaceSurface::Nurbs(surface) => surface.evaluate(u, v),
836            FaceSurface::Cylinder(cyl) => cyl.evaluate(u, v),
837            FaceSurface::Cone(cone) => cone.evaluate(u, v),
838            FaceSurface::Sphere(sph) => sph.evaluate(u, v),
839            FaceSurface::Torus(tor) => tor.evaluate(u, v),
840        };
841        Ok(vec![point.x(), point.y(), point.z()])
842    }
843
844    /// Measure principal curvatures at (u, v) on a face surface.
845    ///
846    /// Returns `[k1, k2, d1x, d1y, d1z, d2x, d2y, d2z]` where k1/k2 are
847    /// principal curvatures and d1/d2 are the corresponding direction vectors.
848    #[wasm_bindgen(js_name = "measureCurvatureAtSurface")]
849    #[allow(clippy::too_many_lines)]
850    pub fn measure_curvature_at_surface(
851        &self,
852        face: u32,
853        u: f64,
854        v: f64,
855    ) -> Result<Vec<f64>, JsError> {
856        let face_id = self.resolve_face(face)?;
857        let face_data = self.topo.face(face_id)?;
858        match face_data.surface() {
859            FaceSurface::Plane { .. } => Ok(vec![0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0]),
860            FaceSurface::Nurbs(surface) => {
861                let derivs = surface.derivatives(u, v, 2);
862                // derivs[i][j] = d^(i+j) S / du^i dv^j
863                let su = if derivs.len() > 1 && !derivs[1].is_empty() {
864                    derivs[1][0]
865                } else {
866                    return Ok(vec![0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0]);
867                };
868                let sv = if !derivs.is_empty() && derivs[0].len() > 1 {
869                    derivs[0][1]
870                } else {
871                    return Ok(vec![0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0]);
872                };
873                let suu = if derivs.len() > 2 && !derivs[2].is_empty() {
874                    derivs[2][0]
875                } else {
876                    Vec3::new(0.0, 0.0, 0.0)
877                };
878                let suv = if derivs.len() > 1 && derivs[1].len() > 1 {
879                    derivs[1][1]
880                } else {
881                    Vec3::new(0.0, 0.0, 0.0)
882                };
883                let svv = if !derivs.is_empty() && derivs[0].len() > 2 {
884                    derivs[0][2]
885                } else {
886                    Vec3::new(0.0, 0.0, 0.0)
887                };
888
889                let normal = su.cross(sv);
890                let normal = match normal.normalize() {
891                    Ok(n) => n,
892                    Err(_) => return Ok(vec![0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0]),
893                };
894
895                // First fundamental form coefficients
896                let ee = su.dot(su);
897                let ff = su.dot(sv);
898                let gg = sv.dot(sv);
899
900                // Second fundamental form coefficients
901                let ll = suu.dot(normal);
902                let mm = suv.dot(normal);
903                let nn = svv.dot(normal);
904
905                // Principal curvatures from shape operator eigenvalues
906                let denom = ee * gg - ff * ff;
907                if denom.abs() < 1e-30 {
908                    return Ok(vec![0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0]);
909                }
910                let h = 0.5 * (ee * nn - 2.0 * ff * mm + gg * ll) / denom; // mean curvature
911                let k = (ll * nn - mm * mm) / denom; // Gaussian curvature
912                let disc = (h * h - k).max(0.0).sqrt();
913                let k1 = h + disc;
914                let k2 = h - disc;
915
916                // Principal directions (approximate)
917                let su_norm = su.normalize().unwrap_or(Vec3::new(1.0, 0.0, 0.0));
918                let sv_norm = sv.normalize().unwrap_or(Vec3::new(0.0, 1.0, 0.0));
919
920                Ok(vec![
921                    k1,
922                    k2,
923                    su_norm.x(),
924                    su_norm.y(),
925                    su_norm.z(),
926                    sv_norm.x(),
927                    sv_norm.y(),
928                    sv_norm.z(),
929                ])
930            }
931            FaceSurface::Cylinder(cyl) => {
932                let r = cyl.radius();
933                let axis = cyl.axis().normalize().unwrap_or(Vec3::new(0.0, 0.0, 1.0));
934                let point = cyl.evaluate(u, v);
935                let to_axis = Vec3::new(
936                    cyl.origin().x() - point.x()
937                        + axis.x()
938                            * axis.dot(Vec3::new(
939                                point.x() - cyl.origin().x(),
940                                point.y() - cyl.origin().y(),
941                                point.z() - cyl.origin().z(),
942                            )),
943                    cyl.origin().y() - point.y()
944                        + axis.y()
945                            * axis.dot(Vec3::new(
946                                point.x() - cyl.origin().x(),
947                                point.y() - cyl.origin().y(),
948                                point.z() - cyl.origin().z(),
949                            )),
950                    cyl.origin().z() - point.z()
951                        + axis.z()
952                            * axis.dot(Vec3::new(
953                                point.x() - cyl.origin().x(),
954                                point.y() - cyl.origin().y(),
955                                point.z() - cyl.origin().z(),
956                            )),
957                );
958                let radial = to_axis.normalize().unwrap_or(Vec3::new(1.0, 0.0, 0.0));
959                Ok(vec![
960                    1.0 / r,
961                    0.0,
962                    radial.x(),
963                    radial.y(),
964                    radial.z(),
965                    axis.x(),
966                    axis.y(),
967                    axis.z(),
968                ])
969            }
970            FaceSurface::Sphere(sph) => {
971                let r = sph.radius();
972                let point = sph.evaluate(u, v);
973                let radial = Vec3::new(
974                    point.x() - sph.center().x(),
975                    point.y() - sph.center().y(),
976                    point.z() - sph.center().z(),
977                )
978                .normalize()
979                .unwrap_or(Vec3::new(0.0, 0.0, 1.0));
980                // Both principal curvatures are 1/r for a sphere
981                let d1 = Vec3::new(-radial.y(), radial.x(), 0.0)
982                    .normalize()
983                    .unwrap_or(Vec3::new(1.0, 0.0, 0.0));
984                let d2 = radial.cross(d1);
985                Ok(vec![
986                    1.0 / r,
987                    1.0 / r,
988                    d1.x(),
989                    d1.y(),
990                    d1.z(),
991                    d2.x(),
992                    d2.y(),
993                    d2.z(),
994                ])
995            }
996            FaceSurface::Cone(cone) => {
997                let half_angle = cone.half_angle();
998                let v_pos = v.abs().max(1e-10);
999                let local_r = v_pos * half_angle.sin();
1000                let k_parallel = if local_r > 1e-15 {
1001                    half_angle.cos() / local_r
1002                } else {
1003                    0.0
1004                };
1005                let axis = cone.axis().normalize().unwrap_or(Vec3::new(0.0, 0.0, 1.0));
1006                Ok(vec![
1007                    0.0,
1008                    k_parallel,
1009                    axis.x(),
1010                    axis.y(),
1011                    axis.z(),
1012                    1.0,
1013                    0.0,
1014                    0.0,
1015                ])
1016            }
1017            FaceSurface::Torus(torus) => {
1018                let r_major = torus.major_radius();
1019                let r_minor = torus.minor_radius();
1020                let k1 = 1.0 / r_minor;
1021                let k2 = u.cos() / (r_major + r_minor * u.cos());
1022                Ok(vec![k1, k2, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0])
1023            }
1024        }
1025    }
1026
1027    /// Tessellate an edge curve into polyline segments.
1028    ///
1029    /// For line edges, returns just start and end points.
1030    /// For NURBS edges, samples at `num_points` along the curve.
1031    ///
1032    /// Returns flattened `[x, y, z, x, y, z, ...]` array.
1033    #[wasm_bindgen(js_name = "tessellateEdge")]
1034    pub fn tessellate_edge(&self, edge: u32, num_points: u32) -> Result<Vec<f64>, JsError> {
1035        let edge_id = self.resolve_edge(edge)?;
1036        let edge_data = self.topo.edge(edge_id)?;
1037
1038        match edge_data.curve() {
1039            EdgeCurve::Line => {
1040                let start = self.topo.vertex(edge_data.start())?.point();
1041                let end = self.topo.vertex(edge_data.end())?.point();
1042                Ok(vec![
1043                    start.x(),
1044                    start.y(),
1045                    start.z(),
1046                    end.x(),
1047                    end.y(),
1048                    end.z(),
1049                ])
1050            }
1051            EdgeCurve::NurbsCurve(curve) => {
1052                let (u0, u1) = curve.domain();
1053                let n = std::cmp::max(2, num_points as usize);
1054                let mut result = Vec::with_capacity(n * 3);
1055                for i in 0..n {
1056                    #[allow(clippy::cast_precision_loss)]
1057                    let t = u0 + (u1 - u0) * (i as f64) / ((n - 1) as f64);
1058                    let p = curve.evaluate(t);
1059                    result.push(p.x());
1060                    result.push(p.y());
1061                    result.push(p.z());
1062                }
1063                Ok(result)
1064            }
1065            EdgeCurve::Circle(circle) => {
1066                let n = std::cmp::max(2, num_points as usize);
1067                Ok(sample_full_period_curve(n, |t| circle.evaluate(t)))
1068            }
1069            EdgeCurve::Ellipse(ellipse) => {
1070                let n = std::cmp::max(2, num_points as usize);
1071                Ok(sample_full_period_curve(n, |t| ellipse.evaluate(t)))
1072            }
1073        }
1074    }
1075
1076    /// Check if an edge is forward-oriented in a given wire.
1077    ///
1078    /// Returns `true` if the edge is forward in the wire, `false` if reversed.
1079    #[wasm_bindgen(js_name = "isEdgeForwardInWire")]
1080    pub fn is_edge_forward_in_wire(&self, edge: u32, wire: u32) -> Result<bool, JsError> {
1081        let edge_id = self.resolve_edge(edge)?;
1082        let wire_id = self.resolve_wire(wire)?;
1083        let wire_data = self.topo.wire(wire_id)?;
1084
1085        for oe in wire_data.edges() {
1086            if oe.edge() == edge_id {
1087                return Ok(oe.is_forward());
1088            }
1089        }
1090
1091        Err(WasmError::InvalidInput {
1092            reason: "edge not found in wire".into(),
1093        }
1094        .into())
1095    }
1096
1097    /// Get the UV parameter domain of a face's surface.
1098    ///
1099    /// Returns `[u_min, u_max, v_min, v_max]`.
1100    #[wasm_bindgen(js_name = "getSurfaceDomain")]
1101    pub fn get_surface_domain(&self, face: u32) -> Result<Vec<f64>, JsError> {
1102        let face_id = self.resolve_face(face)?;
1103        let face_data = self.topo.face(face_id)?;
1104        match face_data.surface() {
1105            FaceSurface::Plane { .. } => Ok(vec![-1e6, 1e6, -1e6, 1e6]),
1106            FaceSurface::Nurbs(surface) => {
1107                let (u0, u1) = surface.domain_u();
1108                let (v0, v1) = surface.domain_v();
1109                Ok(vec![u0, u1, v0, v1])
1110            }
1111            FaceSurface::Cylinder(cyl) => {
1112                let v_range = brepkit_check::properties::axial_v_range(
1113                    &self.topo,
1114                    face_id,
1115                    cyl.origin(),
1116                    cyl.axis(),
1117                )?;
1118                Ok(vec![0.0, 2.0 * PI, v_range.0, v_range.1])
1119            }
1120            FaceSurface::Cone(cone) => {
1121                let v_range = brepkit_check::properties::axial_v_range(
1122                    &self.topo,
1123                    face_id,
1124                    cone.apex(),
1125                    cone.axis(),
1126                )?;
1127                Ok(vec![0.0, 2.0 * PI, v_range.0, v_range.1])
1128            }
1129            FaceSurface::Sphere(_) => Ok(vec![0.0, 2.0 * PI, -PI / 2.0, PI / 2.0]),
1130            FaceSurface::Torus(_) => Ok(vec![0.0, 2.0 * PI, 0.0, 2.0 * PI]),
1131        }
1132    }
1133
1134    /// Project a 3D point onto a face surface using Newton iteration.
1135    ///
1136    /// Returns `[u, v, px, py, pz, distance]`.
1137    #[wasm_bindgen(js_name = "projectPointOnSurface")]
1138    pub fn project_point_on_surface(
1139        &self,
1140        face: u32,
1141        px: f64,
1142        py: f64,
1143        pz: f64,
1144    ) -> Result<Vec<f64>, JsError> {
1145        let face_id = self.resolve_face(face)?;
1146        let face_data = self.topo.face(face_id)?;
1147        let target = Point3::new(px, py, pz);
1148
1149        match face_data.surface() {
1150            FaceSurface::Plane { normal, d } => {
1151                // Project onto plane: p - ((p·n - d) * n)
1152                let dist_to_plane = normal.x() * px + normal.y() * py + normal.z() * pz - d;
1153                let proj = Point3::new(
1154                    px - dist_to_plane * normal.x(),
1155                    py - dist_to_plane * normal.y(),
1156                    pz - dist_to_plane * normal.z(),
1157                );
1158                let dist = (proj - target).length();
1159                // UV coordinates: project onto plane's local frame
1160                Ok(vec![proj.x(), proj.y(), proj.x(), proj.y(), proj.z(), dist])
1161            }
1162            FaceSurface::Nurbs(surface) => {
1163                // Newton iteration for closest point on NURBS surface
1164                let (u0, u1) = surface.domain_u();
1165                let (v0, v1) = surface.domain_v();
1166                let mut best_u = f64::midpoint(u0, u1);
1167                let mut best_v = f64::midpoint(v0, v1);
1168                let mut best_dist = f64::MAX;
1169
1170                // Grid search for initial guess
1171                let n_grid = 8;
1172                for iu in 0..=n_grid {
1173                    for iv in 0..=n_grid {
1174                        #[allow(clippy::cast_precision_loss)]
1175                        let u = u0 + (u1 - u0) * (iu as f64) / (n_grid as f64);
1176                        #[allow(clippy::cast_precision_loss)]
1177                        let v = v0 + (v1 - v0) * (iv as f64) / (n_grid as f64);
1178                        let p = surface.evaluate(u, v);
1179                        let d = (p - target).length();
1180                        if d < best_dist {
1181                            best_dist = d;
1182                            best_u = u;
1183                            best_v = v;
1184                        }
1185                    }
1186                }
1187
1188                // Newton refinement (5 iterations)
1189                for _ in 0..5 {
1190                    let p = surface.evaluate(best_u, best_v);
1191                    let derivs = surface.derivatives(best_u, best_v, 1);
1192                    if derivs.len() < 2 || derivs[0].len() < 2 || derivs[1].is_empty() {
1193                        break;
1194                    }
1195                    let du = derivs[1][0]; // dS/du
1196                    let dv = derivs[0][1]; // dS/dv
1197                    let diff = p - target;
1198
1199                    // Jacobian entries
1200                    let j00 = du.dot(du);
1201                    let j01 = du.dot(dv);
1202                    let j10 = j01;
1203                    let j11 = dv.dot(dv);
1204                    let r0 = diff.x() * du.x() + diff.y() * du.y() + diff.z() * du.z();
1205                    let r1 = diff.x() * dv.x() + diff.y() * dv.y() + diff.z() * dv.z();
1206
1207                    let det = j00 * j11 - j01 * j10;
1208                    if det.abs() < 1e-20 {
1209                        break;
1210                    }
1211                    let delta_u = -(j11 * r0 - j01 * r1) / det;
1212                    let delta_v = -(-j10 * r0 + j00 * r1) / det;
1213
1214                    best_u = (best_u + delta_u).clamp(u0, u1);
1215                    best_v = (best_v + delta_v).clamp(v0, v1);
1216                }
1217
1218                let proj = surface.evaluate(best_u, best_v);
1219                let dist = (proj - target).length();
1220                Ok(vec![best_u, best_v, proj.x(), proj.y(), proj.z(), dist])
1221            }
1222            _ => {
1223                // For analytic surfaces, use grid search (no Newton for now)
1224                let mut best_u = 0.0;
1225                let mut best_v = 0.0;
1226                let mut best_dist = f64::MAX;
1227                let n_grid = 16;
1228                for iu in 0..=n_grid {
1229                    for iv in 0..=n_grid {
1230                        #[allow(clippy::cast_precision_loss)]
1231                        let u = 2.0 * PI * (iu as f64) / (n_grid as f64);
1232                        #[allow(clippy::cast_precision_loss)]
1233                        let v = -PI + 2.0 * PI * (iv as f64) / (n_grid as f64);
1234                        let p = match face_data.surface() {
1235                            FaceSurface::Cylinder(cyl) => cyl.evaluate(u, v),
1236                            FaceSurface::Cone(cone) => cone.evaluate(u, v),
1237                            FaceSurface::Sphere(sph) => sph.evaluate(u, v),
1238                            FaceSurface::Torus(tor) => tor.evaluate(u, v),
1239                            _ => continue,
1240                        };
1241                        let d = (p - target).length();
1242                        if d < best_dist {
1243                            best_dist = d;
1244                            best_u = u;
1245                            best_v = v;
1246                        }
1247                    }
1248                }
1249                let proj = match face_data.surface() {
1250                    FaceSurface::Cylinder(cyl) => cyl.evaluate(best_u, best_v),
1251                    FaceSurface::Cone(cone) => cone.evaluate(best_u, best_v),
1252                    FaceSurface::Sphere(sph) => sph.evaluate(best_u, best_v),
1253                    FaceSurface::Torus(tor) => tor.evaluate(best_u, best_v),
1254                    _ => target,
1255                };
1256                Ok(vec![
1257                    best_u,
1258                    best_v,
1259                    proj.x(),
1260                    proj.y(),
1261                    proj.z(),
1262                    best_dist,
1263                ])
1264            }
1265        }
1266    }
1267
1268    /// Add hole wires to an existing face, creating a new face with the same
1269    /// surface but additional inner wires.
1270    ///
1271    /// Returns a new face handle (`u32`).
1272    #[wasm_bindgen(js_name = "addHolesToFace")]
1273    #[allow(clippy::needless_pass_by_value)]
1274    pub fn add_holes_to_face(
1275        &mut self,
1276        face: u32,
1277        hole_wire_handles: Vec<u32>,
1278    ) -> Result<u32, JsError> {
1279        let face_id = self.resolve_face(face)?;
1280        let face_data = self.topo.face(face_id)?;
1281        let outer_wire = face_data.outer_wire();
1282        let surface = face_data.surface().clone();
1283        let mut inner_wires: Vec<brepkit_topology::wire::WireId> = face_data.inner_wires().to_vec();
1284
1285        for &wh in &hole_wire_handles {
1286            let wid = self.resolve_wire(wh)?;
1287            inner_wires.push(wid);
1288        }
1289
1290        let new_face = Face::new(outer_wire, inner_wires, surface);
1291        let fid = self.topo_mut().add_face(new_face);
1292        Ok(face_id_to_u32(fid))
1293    }
1294
1295    /// Build an edge's NURBS curve data for JS consumption.
1296    ///
1297    /// Returns `null` for line edges, or a JSON string with
1298    /// `{degree, knots, controlPoints, weights}` for NURBS edges.
1299    #[wasm_bindgen(js_name = "getEdgeNurbsData")]
1300    pub fn get_edge_nurbs_data(&self, edge: u32) -> Result<JsValue, JsError> {
1301        let edge_id = self.resolve_edge(edge)?;
1302        let edge_data = self.topo.edge(edge_id)?;
1303        match edge_data.curve() {
1304            EdgeCurve::Line | EdgeCurve::Circle(_) | EdgeCurve::Ellipse(_) => Ok(JsValue::NULL),
1305            EdgeCurve::NurbsCurve(curve) => {
1306                let cp_flat: Vec<f64> = curve
1307                    .control_points()
1308                    .iter()
1309                    .flat_map(|p| [p.x(), p.y(), p.z()])
1310                    .collect();
1311                let data = serde_json::json!({
1312                    "degree": curve.degree(),
1313                    "knots": curve.knots(),
1314                    "controlPoints": cp_flat,
1315                    "weights": curve.weights(),
1316                });
1317                Ok(JsValue::from_str(&data.to_string()))
1318            }
1319        }
1320    }
1321
1322    /// Get the edge-to-face adjacency map for a solid.
1323    ///
1324    /// Returns a JSON string: `{"edgeId": [faceId, ...], ...}`.
1325    #[wasm_bindgen(js_name = "edgeToFaceMap")]
1326    pub fn edge_to_face_map(&self, solid: u32) -> Result<String, JsError> {
1327        let solid_id = self.resolve_solid(solid)?;
1328        let map = brepkit_topology::explorer::edge_to_face_map(&self.topo, solid_id)?;
1329        let json_map: std::collections::HashMap<String, Vec<u32>> = map
1330            .into_iter()
1331            .map(|(edge_idx, face_ids)| {
1332                let fids: Vec<u32> = face_ids.iter().map(|f| face_id_to_u32(*f)).collect();
1333                (edge_idx.to_string(), fids)
1334            })
1335            .collect();
1336        Ok(serde_json::json!(json_map).to_string())
1337    }
1338
1339    /// Get edges shared between two faces.
1340    ///
1341    /// Returns an array of edge handles.
1342    #[wasm_bindgen(js_name = "sharedEdges")]
1343    pub fn shared_edges(&self, face_a: u32, face_b: u32) -> Result<Vec<u32>, JsError> {
1344        let fa = self.resolve_face(face_a)?;
1345        let fb = self.resolve_face(face_b)?;
1346        let edges = brepkit_topology::explorer::shared_edges(&self.topo, fa, fb)?;
1347        Ok(edges.iter().map(|e| edge_id_to_u32(*e)).collect())
1348    }
1349
1350    /// Get faces adjacent to a given face within a solid.
1351    ///
1352    /// Returns an array of face handles.
1353    #[wasm_bindgen(js_name = "adjacentFaces")]
1354    pub fn adjacent_faces(&self, solid: u32, face: u32) -> Result<Vec<u32>, JsError> {
1355        let solid_id = self.resolve_solid(solid)?;
1356        let face_id = self.resolve_face(face)?;
1357        let map = brepkit_topology::explorer::edge_to_face_map(&self.topo, solid_id)?;
1358        let adj = brepkit_topology::explorer::adjacent_faces(&self.topo, face_id, &map)?;
1359        Ok(adj.iter().map(|f| face_id_to_u32(*f)).collect())
1360    }
1361
1362    /// Get the wires (outer + inner) of a face.
1363    ///
1364    /// Returns an array of wire handles.
1365    #[wasm_bindgen(js_name = "faceWires")]
1366    pub fn face_wires(&self, face: u32) -> Result<Vec<u32>, JsError> {
1367        let face_id = self.resolve_face(face)?;
1368        let wires = brepkit_topology::explorer::face_wires(&self.topo, face_id)?;
1369        Ok(wires.iter().map(|w| wire_id_to_u32(*w)).collect())
1370    }
1371
1372    /// Get the solid handles within a compound.
1373    ///
1374    /// Returns an array of solid handles (`u32[]`).
1375    ///
1376    /// # Errors
1377    ///
1378    /// Returns an error if the compound handle is invalid.
1379    #[wasm_bindgen(js_name = "getCompoundSolids")]
1380    pub fn get_compound_solids(&self, compound: u32) -> Result<Vec<u32>, JsError> {
1381        let compound_id = self.resolve_compound(compound)?;
1382        let compound_data = self.topo.compound(compound_id)?;
1383        Ok(compound_data
1384            .solids()
1385            .iter()
1386            .map(|s| solid_id_to_u32(*s))
1387            .collect())
1388    }
1389
1390    /// Get the face handles of a shell.
1391    ///
1392    /// Returns an array of face handles (`u32[]`).
1393    ///
1394    /// # Errors
1395    ///
1396    /// Returns an error if the shell handle is invalid.
1397    #[wasm_bindgen(js_name = "getShellFaces")]
1398    pub fn get_shell_faces(&self, shell: u32) -> Result<Vec<u32>, JsError> {
1399        let shell_id = self.resolve_shell(shell)?;
1400        let shell_data = self.topo.shell(shell_id)?;
1401        Ok(shell_data
1402            .faces()
1403            .iter()
1404            .map(|f| face_id_to_u32(*f))
1405            .collect())
1406    }
1407
1408    /// Get the edge handles of a wire.
1409    ///
1410    /// Returns an array of unique edge handles (`u32[]`).
1411    ///
1412    /// # Errors
1413    ///
1414    /// Returns an error if the wire handle is invalid.
1415    #[wasm_bindgen(js_name = "getWireEdges")]
1416    pub fn get_wire_edges(&self, wire: u32) -> Result<Vec<u32>, JsError> {
1417        let wire_id = self.resolve_wire(wire)?;
1418        let wire_data = self.topo.wire(wire_id)?;
1419        Ok(wire_data
1420            .edges()
1421            .iter()
1422            .map(|oe| edge_id_to_u32(oe.edge()))
1423            .collect())
1424    }
1425
1426    /// Check whether a wire is closed (last edge connects back to first).
1427    #[wasm_bindgen(js_name = "isWireClosed")]
1428    pub fn is_wire_closed(&self, wire: u32) -> Result<bool, JsError> {
1429        let wire_id = self.resolve_wire(wire)?;
1430        let wire_data = self.topo.wire(wire_id)?;
1431        Ok(wire_data.is_closed())
1432    }
1433
1434    /// Compute the total arc-length of a wire.
1435    #[wasm_bindgen(js_name = "wireLength")]
1436    pub fn wire_length(&self, wire: u32) -> Result<f64, JsError> {
1437        let wire_id = self.resolve_wire(wire)?;
1438        let wire_data = self.topo.wire(wire_id)?;
1439        let mut total = 0.0;
1440        for oe in wire_data.edges() {
1441            total += brepkit_operations::measure::edge_length(&self.topo, oe.edge())?;
1442        }
1443        Ok(total)
1444    }
1445
1446    /// Get the analytic surface parameters of a face.
1447    ///
1448    /// Returns a JSON string with surface-type-specific parameters.
1449    #[wasm_bindgen(js_name = "getAnalyticSurfaceParams")]
1450    pub fn get_analytic_surface_params(&self, face: u32) -> Result<String, JsError> {
1451        let face_id = self.resolve_face(face)?;
1452        let face_data = self.topo.face(face_id)?;
1453        let json = match face_data.surface() {
1454            FaceSurface::Plane { normal, d } => serde_json::json!({
1455                "type": "plane",
1456                "normal": [normal.x(), normal.y(), normal.z()],
1457                "d": d,
1458            }),
1459            FaceSurface::Nurbs(_) => serde_json::json!({
1460                "type": "nurbs",
1461            }),
1462            FaceSurface::Cylinder(cyl) => serde_json::json!({
1463                "type": "cylinder",
1464                "origin": [cyl.origin().x(), cyl.origin().y(), cyl.origin().z()],
1465                "axis": [cyl.axis().x(), cyl.axis().y(), cyl.axis().z()],
1466                "radius": cyl.radius(),
1467            }),
1468            FaceSurface::Cone(cone) => serde_json::json!({
1469                "type": "cone",
1470                "apex": [cone.apex().x(), cone.apex().y(), cone.apex().z()],
1471                "axis": [cone.axis().x(), cone.axis().y(), cone.axis().z()],
1472                "halfAngle": cone.half_angle(),
1473            }),
1474            FaceSurface::Sphere(sph) => serde_json::json!({
1475                "type": "sphere",
1476                "center": [sph.center().x(), sph.center().y(), sph.center().z()],
1477                "radius": sph.radius(),
1478            }),
1479            FaceSurface::Torus(tor) => serde_json::json!({
1480                "type": "torus",
1481                "center": [tor.center().x(), tor.center().y(), tor.center().z()],
1482                "majorRadius": tor.major_radius(),
1483                "minorRadius": tor.minor_radius(),
1484            }),
1485        };
1486        Ok(json.to_string())
1487    }
1488}
1489
1490#[cfg(test)]
1491mod tests;