Skip to main content

brep_kernel/
lib.rs

1use serde::{Deserialize, Serialize};
2use wasm_bindgen::prelude::*;
3
4#[path = "geometry/tolerance.rs"]
5mod tolerance;
6pub use tolerance::{solid_scale, KernelTolerances};
7#[path = "props/diagnostics.rs"]
8mod diagnostics;
9pub use diagnostics::{
10    DiagnosticEvent, DiagnosticSeverity, KernelDiagnostics, KernelOutcome, KernelStage,
11};
12#[path = "geometry/curve.rs"]
13mod curve;
14pub use curve::{
15    make_arc, make_circle, make_hyperbola, make_line, make_parabola, uniform_clamped_knots,
16    KnotVector, NurbsCurve, Vec4,
17};
18#[path = "blending/blend/mod.rs"]
19mod blend;
20pub use blend::{
21    blend_closed_edge, blend_edge_variable, blend_open_edge, blend_smooth_chain,
22    round_convex_corner,
23};
24#[path = "blending/fillet.rs"]
25mod fillet;
26pub use fillet::{
27    chamfer_edge, chamfer_edge_angle, chamfer_edge_asymmetric, chamfer_edges_angle,
28    chamfer_edges_asymmetric, fillet_edge, fillet_edges, fillet_edges_variable,
29};
30#[path = "geometry/fit.rs"]
31mod fit;
32pub use fit::{
33    fit_polyline, interpolate_curve, interpolate_curve_closed, interpolate_curve_local,
34    interpolate_curve_thinned, interpolate_curve_with_end_tangents, simplify_polyline,
35    solve_banded, solve_dense, PolylineFit,
36};
37#[path = "geometry/surface.rs"]
38mod surface;
39pub use surface::{
40    carrier_preview_patch, make_cone_surface, make_cylinder_surface, make_extrusion, make_plane,
41    make_revolution, make_sphere_surface, make_torus_surface, NurbsSurface,
42};
43#[path = "geometry/analytic_surface.rs"]
44mod analytic_surface;
45pub use analytic_surface::{
46    circle_angle_to_parameter, intersect_analytic_pair, revolution_structure, AnalyticSurface,
47    RevolutionFrame, RevolutionStructure,
48};
49#[path = "brep/solid_codec.rs"]
50mod solid_codec;
51pub use solid_codec::{decode_solid, encode_solid, SolidNames, SOLID_CODEC_VERSION};
52#[path = "brep/topology.rs"]
53mod topology;
54pub use topology::{
55    make_box_brep, make_cylinder_brep, make_pyramid_brep, BrepSolid, FaceRecord, ValidationReport,
56};
57#[path = "brep/topology_arena.rs"]
58mod topology_arena;
59pub use topology_arena::{
60    ArenaCoedge, ArenaEdge, ArenaFace, ArenaLoop, ArenaShell, ArenaVertex, CoedgeId, EdgeId,
61    FaceId, LoopId, ShellId, TopologyArena, VertexId,
62};
63#[path = "brep/analytic_topology.rs"]
64mod analytic_topology;
65pub use analytic_topology::{make_cone_brep, make_sphere_brep, make_torus_brep};
66#[path = "construction/sweep_topology.rs"]
67mod sweep_topology;
68pub use sweep_topology::{
69    extrude_profile_brep, extrude_profile_brep_draft, profile_anchor, rib_from_profile,
70    sweep_profile_along_path, sweep_profile_along_path_anchored, sweep_profile_helix,
71    sweep_profile_twisted, sweep_profile_twisted_anchored, ProfileAnchor,
72};
73#[path = "construction/revolve_topology.rs"]
74mod revolve_topology;
75pub use revolve_topology::{revolve_profile_brep, revolve_profile_brep_named};
76#[path = "construction/loft_topology.rs"]
77mod loft_topology;
78pub use loft_topology::{
79    loft_profile_brep, loft_profile_brep_closed, loft_profile_brep_guided,
80    loft_profile_brep_guided_frame, loft_profile_brep_tangent,
81};
82#[path = "brep/transform_topology.rs"]
83mod transform_topology;
84pub use transform_topology::{mirror_brep, transform_brep, AffineTransform};
85#[path = "edit/split.rs"]
86mod split;
87pub use split::{
88    split_solid_by_face_surface, split_solid_by_plane, split_solid_by_surface, SplitSurface,
89};
90#[path = "props/mass_properties.rs"]
91mod mass_properties;
92pub use mass_properties::{
93    curve_arc_length, edge_arc_length, face_area, face_boundary_length, face_volume_contribution,
94    parameter_space_area, solid_edge_length_total, solid_mass_properties,
95    solid_mass_properties_full, solid_signed_volume, trim_polygons, DensityMassProperties,
96    FullMassProperties, MassProperties,
97};
98#[path = "meshing/tessellation.rs"]
99mod tessellation;
100pub use tessellation::{tessellate_brep, tessellate_face, TessellationOptions};
101#[path = "meshing/watertight_tessellation/mod.rs"]
102mod watertight_tessellation;
103pub use watertight_tessellation::{
104    sample_edge_polylines, sample_edges_encoded, tessellate_brep_watertight,
105    tessellate_brep_watertight_face_stride, tessellate_brep_watertight_face_stride_with_samples,
106};
107// The Rust feature-history execution engine (migration-plan Stage 4/5 foundation).
108// Deserializes the serialized `{ type, inputParams, ... }` descriptor shape, runs
109// each feature against a live scene-map of resident handles, and returns per-feature
110// results (handles + face/edge names). See feature_pipeline/mod.rs for the contract.
111#[path = "feature_pipeline/mod.rs"]
112mod feature_pipeline;
113pub use feature_pipeline::execute_history_json;
114// Typed, in-process pipeline surface for native consumers (brep-render): run a
115// whole history and read the results without a JSON round trip.
116pub use feature_pipeline::{
117    clear_history_cache, execute_history, AddedSolid, Axis, FeatureDescriptor, FeatureResult,
118    Frame, HistoryRequest, HistoryResult, ProfileLoop, SketchProfile,
119};
120// The feature-schema catalogue (feature definitions: name + `inputParamsSchema`),
121// so a native UI can drive schema-driven feature dialogs without the JSON export.
122pub use feature_pipeline::feature_schema_catalogue;
123// The pipeline expression evaluator (the history's `expressions` + `configurator`
124// variable sheet), so a native UI (brep-render's engine-native sketcher) can
125// evaluate a LIVE dimension `valueExpr` against the same variables a committed
126// solve would (`features/sketch.rs` `pre_evaluate_expressions`). `Env::build`/
127// `eval` are pure arithmetic (no `std::time`), so this stays wasm-clean.
128pub use feature_pipeline::Env;
129
130/// One-shot: build an [`Env`] from a history's `expressions` source + its
131/// `configurator` JSON, then evaluate `source` to a scalar. The small surface the
132/// engine-native sketcher's live dimension-value edit uses (deliverable S5.0) —
133/// equivalent to `Env::build(expressions, configurator_json)?.eval(source)`.
134pub fn eval_expression(
135    expressions: &str,
136    configurator_json: &serde_json::Value,
137    source: &str,
138) -> Result<f64, String> {
139    Env::build(expressions, configurator_json).and_then(|env| env.eval(source))
140}
141
142// In the shared-memory-threaded wasm build, re-export wasm-bindgen-rayon's
143// thread-pool initializer as `initThreadPool(numThreads)`. The JS side must
144// await it once after wasm init (needs a cross-origin-isolated page —
145// COOP/COEP — for SharedArrayBuffer). With the pool live, the ordinary
146// `tessellate_watertight_buffers` parallelizes its per-face loop across threads
147// (see `tessellate_faces_stride` under the `parallel` feature).
148#[cfg(feature = "wasm-threads")]
149pub use wasm_bindgen_rayon::init_thread_pool;
150#[path = "geometry/projection.rs"]
151mod projection;
152pub use projection::{
153    project_point_to_curve, project_point_to_surface, project_point_to_surface_seeded,
154    CurveProjection, SurfaceProjection,
155};
156#[path = "intersect/curve_surface_intersection.rs"]
157mod curve_surface_intersection;
158pub use curve_surface_intersection::{intersect_curve_surface, CurveSurfaceIntersection};
159#[path = "intersect/curve_curve_intersection.rs"]
160mod curve_curve_intersection;
161pub use curve_curve_intersection::{intersect_curves, CurveCurveIntersection};
162#[path = "intersect/surface_surface_intersection.rs"]
163mod surface_surface_intersection;
164pub use surface_surface_intersection::{
165    intersect_surfaces, intersect_surfaces_supplemental, SurfaceIntersectionCurve,
166    SurfaceIntersectionOptions,
167};
168#[path = "brep/classification.rs"]
169mod classification;
170pub use classification::{
171    classify_point, parameter_point_in_face, PointClass, PointClassification, PolygonClass,
172    SolidClassifier,
173};
174#[path = "geometry/spatial.rs"]
175mod spatial;
176pub use spatial::{Aabb, Bvh};
177#[path = "brep/pair_classification.rs"]
178mod pair_classification;
179pub use pair_classification::{
180    classify_surface_pair, classify_surface_pair_cached, SurfaceClassifyData,
181    SurfacePairClassification, SurfacePairRelation,
182};
183#[path = "intersect/arrangement.rs"]
184mod arrangement;
185pub use arrangement::{
186    arrange_segments, point_in_polygon, segment_intersection, ArrangementPiece, ArrangementRegion,
187    CycleUse, Segment2, Vec2,
188};
189#[path = "geometry/pcurve.rs"]
190mod pcurve;
191pub use pcurve::{
192    build_pcurve_on_surface, build_pcurve_on_surface_marched, build_pcurve_on_surface_range,
193    build_pcurve_on_surface_range_dense,
194};
195#[path = "csg/imprint.rs"]
196mod imprint;
197pub use imprint::{
198    build_imprints, EdgeSplitRecord, FaceImprints, FaceKey, FacePcurve, ImprintOptions,
199    ImprintPieceRecord, ImprintResultRecord, ImprintVertex,
200};
201#[path = "csg/edge_split.rs"]
202mod edge_split;
203pub use edge_split::{apply_edge_splits, apply_edge_splits_with_map};
204#[path = "csg/fragment.rs"]
205mod fragment;
206pub use fragment::{
207    fragment_face, fragment_solid, FaceFragmentRecord, FragmentCoedge, FragmentEdgeSource,
208    FragmentLoop,
209};
210#[path = "csg/boolean/mod.rs"]
211mod boolean;
212pub use boolean::{
213    boolean_operation, boolean_operation_nary, boolean_operation_with_diagnostics,
214    BooleanOperation, BooleanOptions,
215};
216#[path = "csg/oracle.rs"]
217mod oracle;
218pub use oracle::{
219    boolean_residual_fusables, boolean_semantic_disagreement, boolean_semantic_disagreement_nary,
220    OracleReport, ResidualFusable, ResidualKind, SemanticDisagreement, DISAGREEMENT_THRESHOLD,
221};
222#[path = "healing/heal.rs"]
223mod heal;
224#[path = "offset/offset.rs"]
225mod offset;
226pub use offset::{offset_face_carrier, offset_surface, OffsetFaceCarrier};
227#[path = "offset/thicken.rs"]
228mod thicken;
229pub use thicken::{thicken_face_sheet, thicken_trimmed_sheet};
230#[path = "healing/coalesce.rs"]
231mod coalesce;
232pub use coalesce::{concatenate_exact_curve_pieces, merge_curve_continuation_edges};
233#[path = "healing/face_merge.rs"]
234mod face_merge;
235// Retained for potential mesh-import repair; no exact operation may fall
236// back to it (offset shell must produce real surfaces or fail loudly).
237#[allow(dead_code)]
238#[path = "healing/faceted_repair.rs"]
239mod faceted_repair;
240pub use face_merge::merge_same_surface_faces;
241pub use faceted_repair::mesh_to_faceted_brep;
242#[path = "offset/offset_shell.rs"]
243mod offset_shell;
244pub use offset_shell::{
245    offset_shell, offset_shell_with_diagnostics, OffsetFaceRole, OffsetShellFaceImageRecord,
246    OffsetShellResultRecord,
247};
248#[path = "io/step.rs"]
249mod step;
250pub use step::{audit_step_manifold, export_step};
251#[path = "io/step_import/mod.rs"]
252mod step_import;
253pub use step_import::{import_step, import_step_report};
254#[path = "io/mesh_io.rs"]
255mod mesh_io;
256pub use mesh_io::{
257    read_binary_stl, read_obj, write_binary_stl, write_obj, ObjReadResult, StlReadResult,
258};
259#[path = "solvers/sketch_solver.rs"]
260mod sketch_solver;
261pub use sketch_solver::{
262    solve_sketch, solve_sketch_from_json, SketchSolverSettings, SolveSketchRequest,
263};
264#[path = "solvers/assembly_solver.rs"]
265mod assembly_solver;
266pub use assembly_solver::{
267    solve_assembly, AssemblyBody, AssemblyMate, AssemblySolution, AssemblySolveOptions, BodyPose,
268    MateAlign, MateAxis, MateKind, MatePlane, MateResidualReport, SolveStrategy,
269};
270#[path = "edit/direct_edit.rs"]
271mod direct_edit;
272pub use direct_edit::{delete_face_and_heal, move_faces, resolve_face_by_point};
273#[path = "healing/sew.rs"]
274mod sew;
275pub use sew::{sew_solid, split_pinched_vertices, SewReport};
276#[path = "meshing/mesh_segment.rs"]
277mod mesh_segment;
278pub use mesh_segment::{
279    mesh_regions_to_brep, segment_mesh_faces, MeshRegion, MeshSegmentation, RegionCarrier,
280    SegmentOptions, UNASSIGNED_REGION,
281};
282
283#[derive(Clone, Copy, Debug, Default, Deserialize, Serialize)]
284pub struct Vec3 {
285    pub x: f64,
286    pub y: f64,
287    pub z: f64,
288}
289
290impl Vec3 {
291    pub fn new(x: f64, y: f64, z: f64) -> Self {
292        Self { x, y, z }
293    }
294    pub fn sub(self, rhs: Self) -> Self {
295        Self::new(self.x - rhs.x, self.y - rhs.y, self.z - rhs.z)
296    }
297    pub fn add(self, rhs: Self) -> Self {
298        Self::new(self.x + rhs.x, self.y + rhs.y, self.z + rhs.z)
299    }
300    pub fn scale(self, factor: f64) -> Self {
301        Self::new(self.x * factor, self.y * factor, self.z * factor)
302    }
303    pub fn cross(self, rhs: Self) -> Self {
304        Self::new(
305            self.y * rhs.z - self.z * rhs.y,
306            self.z * rhs.x - self.x * rhs.z,
307            self.x * rhs.y - self.y * rhs.x,
308        )
309    }
310    pub fn dot(self, rhs: Self) -> f64 {
311        self.x * rhs.x + self.y * rhs.y + self.z * rhs.z
312    }
313    pub fn length(self) -> f64 {
314        self.dot(self).sqrt()
315    }
316    pub fn length_squared(self) -> f64 {
317        self.dot(self)
318    }
319    pub fn normalized(self) -> Result<Self, String> {
320        let length = self.length();
321        if length <= 1e-12 {
322            return Err("Vec3.normalized: zero-length vector".into());
323        }
324        Ok(self.scale(1.0 / length))
325    }
326    pub fn perpendicular(self) -> Result<Self, String> {
327        let x = self.x.abs();
328        let y = self.y.abs();
329        let z = self.z.abs();
330        let axis = if x <= y && x <= z {
331            Self::new(1.0, 0.0, 0.0)
332        } else if y <= z {
333            Self::new(0.0, 1.0, 0.0)
334        } else {
335            Self::new(0.0, 0.0, 1.0)
336        };
337        self.cross(axis).normalized()
338    }
339}
340
341#[derive(Clone, Debug, Default, Deserialize, Serialize)]
342pub struct Mesh {
343    pub positions: Vec<f64>,
344    pub normals: Vec<f64>,
345    pub indices: Vec<u32>,
346    pub face_ids: Vec<u32>,
347}
348
349impl Mesh {
350    fn push_vertex(&mut self, point: Vec3, normal: Vec3) -> u32 {
351        self.positions.extend([point.x, point.y, point.z]);
352        self.normals.extend([normal.x, normal.y, normal.z]);
353        (self.positions.len() / 3 - 1) as u32
354    }
355
356    fn quad(&mut self, points: [Vec3; 4], normal: Vec3, face_id: u32) {
357        let base = self.push_vertex(points[0], normal);
358        for point in points.iter().skip(1) {
359            self.push_vertex(*point, normal);
360        }
361        self.indices
362            .extend([base, base + 1, base + 2, base, base + 2, base + 3]);
363        self.face_ids.extend([face_id, face_id]);
364    }
365
366    pub fn signed_volume(&self) -> f64 {
367        self.indices
368            .chunks_exact(3)
369            .map(|tri| {
370                let point = |index: u32| {
371                    let i = index as usize * 3;
372                    Vec3::new(
373                        self.positions[i],
374                        self.positions[i + 1],
375                        self.positions[i + 2],
376                    )
377                };
378                point(tri[0]).dot(point(tri[1]).cross(point(tri[2]))) / 6.0
379            })
380            .sum()
381    }
382
383    pub fn validate(&self) -> Result<(), String> {
384        if self.positions.len() % 3 != 0 || self.normals.len() != self.positions.len() {
385            return Err("invalid vertex or normal buffer".into());
386        }
387        self.validate_geometry()
388    }
389
390    /// Geometry-only validation for consumers that use positions + indices but
391    /// NOT normals (e.g. `signed_volume`, whose tetrahedron sum never reads a
392    /// normal). Mesh-only solids such as sheet-metal bodies supply metrics
393    /// meshes with an empty normal buffer, which is legitimate for volume.
394    pub fn validate_geometry(&self) -> Result<(), String> {
395        if self.positions.len() % 3 != 0 {
396            return Err("invalid vertex buffer".into());
397        }
398        if self.indices.len() % 3 != 0 {
399            return Err("index buffer is not triangular".into());
400        }
401        let count = self.positions.len() / 3;
402        if self.indices.iter().any(|index| *index as usize >= count) {
403            return Err("index outside vertex buffer".into());
404        }
405        if !self.positions.iter().all(|value| value.is_finite()) {
406            return Err("non-finite vertex".into());
407        }
408        Ok(())
409    }
410}
411
412pub fn make_box(size_x: f64, size_y: f64, size_z: f64) -> Mesh {
413    let x = size_x.abs();
414    let y = size_y.abs();
415    let z = size_z.abs();
416    let p = [
417        Vec3::new(0.0, 0.0, 0.0),
418        Vec3::new(x, 0.0, 0.0),
419        Vec3::new(x, y, 0.0),
420        Vec3::new(0.0, y, 0.0),
421        Vec3::new(0.0, 0.0, z),
422        Vec3::new(x, 0.0, z),
423        Vec3::new(x, y, z),
424        Vec3::new(0.0, y, z),
425    ];
426    let mut mesh = Mesh::default();
427    mesh.quad([p[0], p[3], p[2], p[1]], Vec3::new(0.0, 0.0, -1.0), 0);
428    mesh.quad([p[4], p[5], p[6], p[7]], Vec3::new(0.0, 0.0, 1.0), 1);
429    mesh.quad([p[0], p[1], p[5], p[4]], Vec3::new(0.0, -1.0, 0.0), 2);
430    mesh.quad([p[3], p[7], p[6], p[2]], Vec3::new(0.0, 1.0, 0.0), 3);
431    mesh.quad([p[0], p[4], p[7], p[3]], Vec3::new(-1.0, 0.0, 0.0), 4);
432    mesh.quad([p[1], p[2], p[6], p[5]], Vec3::new(1.0, 0.0, 0.0), 5);
433    mesh
434}
435
436pub fn make_cylinder(radius: f64, height: f64, segments: usize) -> Mesh {
437    let radius = radius.abs();
438    let height = height.abs();
439    let segments = segments.max(3);
440    let mut mesh = Mesh::default();
441    for i in 0..segments {
442        let a = std::f64::consts::TAU * i as f64 / segments as f64;
443        let b = std::f64::consts::TAU * (i + 1) as f64 / segments as f64;
444        let (sa, ca) = a.sin_cos();
445        let (sb, cb) = b.sin_cos();
446        let pa = Vec3::new(radius * ca, 0.0, -radius * sa);
447        let pb = Vec3::new(radius * cb, 0.0, -radius * sb);
448        let ta = Vec3::new(pa.x, height, pa.z);
449        let tb = Vec3::new(pb.x, height, pb.z);
450        mesh.quad(
451            [pa, pb, tb, ta],
452            Vec3::new((ca + cb) * 0.5, 0.0, -(sa + sb) * 0.5),
453            0,
454        );
455        let bottom = mesh.push_vertex(Vec3::default(), Vec3::new(0.0, -1.0, 0.0));
456        let bi = mesh.push_vertex(pb, Vec3::new(0.0, -1.0, 0.0));
457        let bj = mesh.push_vertex(pa, Vec3::new(0.0, -1.0, 0.0));
458        mesh.indices.extend([bottom, bi, bj]);
459        mesh.face_ids.push(1);
460        let top = mesh.push_vertex(Vec3::new(0.0, height, 0.0), Vec3::new(0.0, 1.0, 0.0));
461        let ti = mesh.push_vertex(ta, Vec3::new(0.0, 1.0, 0.0));
462        let tj = mesh.push_vertex(tb, Vec3::new(0.0, 1.0, 0.0));
463        mesh.indices.extend([top, ti, tj]);
464        mesh.face_ids.push(2);
465    }
466    mesh
467}
468
469
470mod abi;
471pub use abi::*;
472
473#[cfg(test)]
474mod tests;