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::{
7    curve_model_scale, model_scale, report_scale_migration, solid_model_scale, solid_scale,
8    offset_construction_band, vertex_tolerance_from_edges, KernelTolerances,
9    MeasuredTolerance, OFFSET_CONSTRUCTION_FLOOR, OFFSET_CONSTRUCTION_REL,
10    VERTEX_MATCH_FLOOR,
11};
12#[path = "props/diagnostics.rs"]
13mod diagnostics;
14pub use diagnostics::{
15    DiagnosticEvent, DiagnosticSeverity, KernelDiagnostics, KernelOutcome, KernelStage,
16};
17#[path = "geometry/curve.rs"]
18mod curve;
19pub use curve::{
20    make_arc, make_circle, make_hyperbola, make_line, make_parabola, uniform_clamped_knots,
21    KnotVector, NurbsCurve, Vec4,
22};
23#[path = "blending/blend/mod.rs"]
24mod blend;
25pub use blend::{
26    blend_closed_edge, blend_edge_variable, blend_open_edge, blend_smooth_chain,
27    round_convex_corner,
28};
29#[path = "blending/fillet.rs"]
30mod fillet;
31pub use fillet::{
32    chamfer_edge, chamfer_edge_angle, chamfer_edge_asymmetric, chamfer_edges_angle,
33    chamfer_edges_asymmetric, fillet_edge, fillet_edges, fillet_edges_variable,
34    fillet_edges_variable_law, fillet_edges_variable_vertex_radii,
35};
36#[path = "blending/law.rs"]
37mod law;
38pub use law::{LawSegment, RadiusLaw};
39#[path = "geometry/fit.rs"]
40mod fit;
41pub use fit::{
42    fit_polyline, interpolate_curve, interpolate_curve_closed, interpolate_curve_local,
43    interpolate_curve_thinned, interpolate_curve_with_end_tangents, simplify_polyline,
44    solve_banded, solve_dense, PolylineFit,
45};
46#[path = "geometry/image_curve.rs"]
47mod image_curve;
48pub use image_curve::{affine_image_curve, image_curve, image_curve_pair, ImageCurve, ImageCurveTier};
49#[path = "geometry/surface.rs"]
50mod surface;
51pub use surface::{
52    carrier_preview_patch, make_cone_surface, make_cylinder_surface, make_extrusion, make_plane,
53    make_revolution, make_sphere_surface, make_torus_surface, NurbsSurface,
54};
55#[path = "geometry/analytic_surface.rs"]
56mod analytic_surface;
57pub use analytic_surface::{
58    circle_angle_to_parameter, intersect_analytic_pair, revolution_structure, AnalyticSurface,
59    RevolutionFrame, RevolutionStructure,
60};
61#[path = "brep/solid_codec.rs"]
62mod solid_codec;
63pub use solid_codec::{decode_solid, encode_solid, SolidNames, SOLID_CODEC_VERSION};
64#[path = "brep/topology.rs"]
65mod topology;
66pub use topology::{
67    make_box_brep, make_cylinder_brep, make_pyramid_brep, BrepSolid, FaceRecord, ValidationReport,
68};
69#[path = "brep/topology_arena.rs"]
70mod topology_arena;
71pub use topology_arena::{
72    ArenaCoedge, ArenaEdge, ArenaFace, ArenaLoop, ArenaShell, ArenaVertex, CoedgeId, EdgeId,
73    FaceId, LoopId, ShellId, TopologyArena, VertexId,
74};
75#[path = "brep/analytic_topology.rs"]
76mod analytic_topology;
77pub use analytic_topology::{make_cone_brep, make_sphere_brep, make_torus_brep};
78#[path = "construction/sweep_topology.rs"]
79mod sweep_topology;
80pub use sweep_topology::{
81    extrude_profile_brep, extrude_profile_brep_draft, fit_helix_curve, helix_sample_points,
82    profile_anchor, rib_from_profile, RibExtrusion,
83    sweep_profile_along_chain, sweep_profile_along_path, sweep_profile_along_path_anchored,
84    sweep_profile_helix,
85    sweep_profile_twisted, sweep_profile_twisted_anchored, ProfileAnchor, SectionPlacement,
86};
87#[path = "construction/revolve_topology.rs"]
88mod revolve_topology;
89pub use revolve_topology::{revolve_profile_brep, revolve_profile_brep_named};
90#[path = "construction/loft_topology.rs"]
91mod loft_topology;
92pub use loft_topology::{
93    loft_profile_brep, loft_profile_brep_closed, loft_profile_brep_guided,
94    loft_profile_brep_guided_frame, loft_profile_brep_tangent,
95};
96#[path = "brep/transform_topology.rs"]
97mod transform_topology;
98pub use transform_topology::{mirror_brep, transform_brep, AffineTransform};
99#[path = "edit/split.rs"]
100mod split;
101pub use split::{
102    split_solid_by_face_surface, split_solid_by_plane, split_solid_by_surface, SplitSurface,
103};
104#[path = "props/mass_properties.rs"]
105mod mass_properties;
106pub use mass_properties::{
107    curve_arc_length, edge_arc_length, face_area, face_boundary_length, face_volume_contribution,
108    parameter_space_area, solid_edge_length_total, solid_mass_properties,
109    solid_mass_properties_full, solid_signed_volume, trim_polygons, DensityMassProperties,
110    FullMassProperties, MassProperties,
111};
112#[path = "meshing/tessellation.rs"]
113mod tessellation;
114pub use tessellation::{tessellate_brep, tessellate_face, TessellationOptions};
115#[path = "meshing/watertight_tessellation/mod.rs"]
116mod watertight_tessellation;
117pub use watertight_tessellation::{
118    sample_edge_polylines, sample_edges_encoded, tessellate_brep_watertight,
119    tessellate_brep_watertight_face_stride, tessellate_brep_watertight_face_stride_with_samples,
120};
121// The Rust feature-history execution engine (migration-plan Stage 4/5 foundation).
122// Deserializes the serialized `{ type, inputParams, ... }` descriptor shape, runs
123// each feature against a live scene-map of resident handles, and returns per-feature
124// results (handles + face/edge names). See feature_pipeline/mod.rs for the contract.
125#[path = "feature_pipeline/mod.rs"]
126mod feature_pipeline;
127pub use feature_pipeline::execute_history_json;
128// Typed, in-process pipeline surface for native consumers (brep-render): run a
129// whole history and read the results without a JSON round trip.
130pub use feature_pipeline::{
131    clear_history_cache, execute_history, execute_history_observed, AddedSolid, Axis,
132    ComponentRecord, FeatureDescriptor, FeatureResult, Frame, HistoryProgress, HistoryRequest,
133    HistoryResult, ProfileLoop, ScenePoint, SketchProfile,
134};
135// The sketch loop-id write-back: stamps each closed loop's stable id onto its
136// geometries so per-loop face names survive deleting the edge an id came from.
137// The editor calls it when a sketch is committed.
138pub use feature_pipeline::assign_sketch_loop_ids;
139// Sheet-metal flat-pattern (unfold) → 2D vector export, keyed by resident handle.
140pub use feature_pipeline::{flat_pattern_dxf, flat_pattern_svg, is_sheet_metal_handle};
141// Native IMPORT3D payload: finished solids → the `io/snapshot` container with
142// IMPORT3D's naming stamped on, i.e. the `inputParams.nativeBrep` an import lane
143// bakes into a part document (`features/import3d.rs` reads it back verbatim).
144pub use feature_pipeline::{native_import_payload, native_import_payload_with_appearance};
145// The scene-metadata isolation bracket a payload producer holds across the
146// encode, so ambient records of the LIVE document never leak into a new part.
147pub use feature_pipeline::IsolatedSceneMetadata;
148// The imported-colour read of the name-keyed scene-metadata store. Thread-local
149// like the rest of it, so an off-thread runner reads it and ships the result.
150pub use feature_pipeline::scene_metadata_colors_json;
151// The assemblies parts library (unique part payloads for ACOMP instances):
152// insert / update-refresh mutations and the save-side serialization surface.
153pub use feature_pipeline::{
154    add_part_to_library, install_parts_library, missing_library_parts, parts_library_json,
155    parts_library_map, parts_library_revision, refresh_library_entry, PartsLibraryEntry,
156    PartsLibraryMap,
157};
158// Assembly constraint state + exported ABI (state/statuses/DOF/overlay reads,
159// constraint CRUD with auto-solve, the document pose/isFixed write-back fold).
160pub use feature_pipeline::assembly::{
161    assembly_add_constraint_json, assembly_apply_document_json, assembly_dof_json,
162    assembly_move_constraint_json, assembly_overlay_json, assembly_pose_updates_json,
163    assembly_remove_constraint_json, assembly_run_solve_json,
164    assembly_set_constraint_enabled_json, assembly_set_constraint_open_json,
165    assembly_state_json, assembly_statuses_json, assembly_update_constraint_json,
166    constraint_schema_catalogue,
167};
168pub use feature_pipeline::{AssemblyState, ConstraintEntry};
169// The ONE matrix → `{translate, rotateEulerDeg}` (intrinsic XYZ, degrees) pose
170// encoder, shared by the solver write-back and any out-of-crate ACOMP author.
171pub use feature_pipeline::assembly::transform_to_pose_params;
172// The feature-schema catalogue (feature definitions: name + `inputParamsSchema`),
173// so a native UI can drive schema-driven feature dialogs without the JSON export.
174pub use feature_pipeline::feature_schema_catalogue;
175// Selection-context applicability: the context bar's per-feature/-constraint
176// show/no-show predicates, plus the constraint type table they pair with.
177pub use feature_pipeline::assembly::{constraint_type, ConstraintTypeDef, CONSTRAINT_TYPES};
178pub use feature_pipeline::{feature_context_applicable, SelectionProbe};
179// The pipeline expression evaluator (the history's `expressions` + `configurator`
180// variable sheet), so a native UI (brep-render's engine-native sketcher) can
181// evaluate a LIVE dimension `valueExpr` against the same variables a committed
182// solve would (`features/sketch.rs` `pre_evaluate_expressions`). `Env::build`/
183// `eval` are pure arithmetic (no `std::time`), so this stays wasm-clean.
184pub use feature_pipeline::Env;
185
186/// One-shot: build an [`Env`] from a history's `expressions` source + its
187/// `configurator` JSON, then evaluate `source` to a scalar. The small surface the
188/// engine-native sketcher's live dimension-value edit uses (deliverable S5.0) —
189/// equivalent to `Env::build(expressions, configurator_json)?.eval(source)`.
190pub fn eval_expression(
191    expressions: &str,
192    configurator_json: &serde_json::Value,
193    source: &str,
194) -> Result<f64, String> {
195    Env::build(expressions, configurator_json).and_then(|env| env.eval(source))
196}
197
198// In the shared-memory-threaded wasm build, re-export wasm-bindgen-rayon's
199// thread-pool initializer as `initThreadPool(numThreads)`. The JS side must
200// await it once after wasm init (needs a cross-origin-isolated page —
201// COOP/COEP — for SharedArrayBuffer). With the pool live, the ordinary
202// `tessellate_watertight_buffers` parallelizes its per-face loop across threads
203// (see `tessellate_faces_stride` under the `parallel` feature).
204#[cfg(feature = "wasm-threads")]
205pub use wasm_bindgen_rayon::init_thread_pool;
206#[path = "geometry/projection.rs"]
207mod projection;
208pub use projection::{
209    project_point_to_curve, project_point_to_surface, project_point_to_surface_seeded,
210    CurveProjection, SurfaceProjection,
211};
212#[path = "intersect/curve_surface_intersection.rs"]
213mod curve_surface_intersection;
214pub use curve_surface_intersection::{intersect_curve_surface, CurveSurfaceIntersection};
215#[path = "intersect/curve_curve_intersection.rs"]
216mod curve_curve_intersection;
217pub use curve_curve_intersection::{intersect_curves, CurveCurveIntersection};
218#[path = "intersect/surface_surface_intersection.rs"]
219mod surface_surface_intersection;
220pub use surface_surface_intersection::{
221    intersect_surfaces, intersect_surfaces_supplemental, SurfaceIntersectionCurve,
222    SurfaceIntersectionOptions,
223};
224#[path = "brep/classification.rs"]
225mod classification;
226pub use classification::{
227    classify_point, parameter_point_in_face, PointClass, PointClassification, PolygonClass,
228    SolidClassifier,
229};
230#[path = "geometry/spatial.rs"]
231mod spatial;
232pub use spatial::{Aabb, Bvh};
233#[path = "brep/pair_classification.rs"]
234mod pair_classification;
235pub use pair_classification::{
236    classify_surface_pair, classify_surface_pair_cached, SurfaceClassifyData,
237    SurfacePairClassification, SurfacePairRelation,
238};
239#[path = "intersect/arrangement.rs"]
240mod arrangement;
241pub use arrangement::{
242    arrange_segments, point_in_polygon, segment_intersection, ArrangementPiece, ArrangementRegion,
243    CycleUse, Segment2, Vec2,
244};
245#[path = "geometry/pcurve.rs"]
246mod pcurve;
247pub use pcurve::{
248    build_pcurve_on_surface, build_pcurve_on_surface_marched, build_pcurve_on_surface_range,
249    build_pcurve_on_surface_range_dense,
250};
251#[path = "csg/imprint.rs"]
252mod imprint;
253pub use imprint::{
254    build_imprints, EdgeSplitRecord, FaceImprints, FaceKey, FacePcurve, ImprintOptions,
255    ImprintPieceRecord, ImprintResultRecord, ImprintVertex,
256};
257#[path = "csg/edge_split.rs"]
258mod edge_split;
259pub use edge_split::{apply_edge_splits, apply_edge_splits_with_map};
260#[path = "csg/fragment.rs"]
261mod fragment;
262pub use fragment::{
263    fragment_face, fragment_solid, FaceFragmentRecord, FragmentCoedge, FragmentEdgeSource,
264    FragmentLoop,
265};
266#[path = "csg/boolean/mod.rs"]
267mod boolean;
268pub use boolean::{
269    boolean_operation, boolean_operation_nary, boolean_operation_with_diagnostics,
270    BooleanOperation, BooleanOptions,
271};
272#[path = "csg/oracle.rs"]
273mod oracle;
274pub use oracle::{
275    boolean_residual_fusables, boolean_semantic_disagreement, boolean_semantic_disagreement_nary,
276    OracleReport, ResidualFusable, ResidualKind, SemanticDisagreement, DISAGREEMENT_THRESHOLD,
277};
278#[path = "healing/heal.rs"]
279mod heal;
280// The pointwise offset evaluator — ONE definition of "the offset of a surface
281// at a parameter", shared by `offset_surface`'s Greville sampling, the blend
282// march's tangency residual, and the push-face offset residual gates. Declared
283// before `offset` because that module is its first consumer.
284#[path = "offset/point.rs"]
285mod offset_point;
286pub use offset_point::{OffsetEvaluator, OffsetNormal, OffsetSample};
287// The LOCUS-level offset seam — ONE definition of "intersect two offset
288// analytic carriers in closed form", shared by the blend's corner closures.
289// The pointwise evaluator above answers "where is the offset at this (u, v)";
290// this answers "where do two offsets MEET", which is what the corner solves
291// need and what no pointwise evaluator can produce (audit §9.4, §11).
292// Crate-internal: no consumer outside the kernel names these.
293#[path = "offset/analytic_pair.rs"]
294mod offset_analytic_pair;
295// The shared re-trim helpers — ONE definition of "rebuild a face's carrier and
296// pcurves around a boundary that has already moved", shared by push-face,
297// face-move and the direct-edit heals. This is the RE-INTERSECTION sense of
298// trimming; offset-shell's parametric-image trimming is a different operation
299// and deliberately not here (see the module doc). Crate-internal: no consumer
300// outside the kernel names these.
301#[path = "offset/retrim.rs"]
302mod offset_retrim;
303// The RE-INTERSECTION seam — ONE definition of "intersect two carriers to
304// remake a trim boundary", with the exact analytic lane first and the general
305// marched lane behind it. `offset_retrim` above re-trims a face around a
306// boundary that has ALREADY moved; this is the step that moves it, and it is
307// what makes a neighbour re-intersection surface-type-blind the way
308// offset-shell's imprint driver already is. Crate-internal: no consumer outside
309// the kernel names these.
310#[path = "offset/reintersect.rs"]
311mod offset_reintersect;
312// The MEASURED half of the tolerance model: the deviation actually observed
313// between an offset construction and the geometry it was built to reproduce.
314// Everything in `geometry/tolerance.rs` above `MeasuredTolerance` DERIVES a
315// band from a size before the geometry exists; this measures what the
316// construction then did. Declared before `offset` because that module is its
317// first consumer (occt-offset-algorithms.md §6.1, ADOPT #2).
318#[path = "offset/measure.rs"]
319mod offset_measure;
320pub use offset_measure::{
321    measure_edge_against_pcurve_image, measure_surface_fit_against_pointwise_offset,
322    span_midpoint_error, vertex_endpoint_gap,
323};
324#[path = "offset/offset.rs"]
325mod offset;
326pub use offset::{
327    offset_face_carrier, offset_face_carrier_measured, offset_face_carrier_sided, offset_surface,
328    offset_surface_measured, offset_surface_measured_sided, CarrierDeviation, CarrierExtension,
329    MeasuredOffsetSurface, OffsetFaceCarrier, OffsetSurfaceLane,
330};
331#[path = "offset/thicken.rs"]
332mod thicken;
333pub use thicken::{thicken_face_sheet, thicken_trimmed_sheet};
334#[path = "healing/coalesce.rs"]
335mod coalesce;
336pub use coalesce::{concatenate_exact_curve_pieces, merge_curve_continuation_edges};
337#[path = "healing/face_merge.rs"]
338mod face_merge;
339// Retained for potential mesh-import repair; no exact operation may fall
340// back to it (offset shell must produce real surfaces or fail loudly).
341#[allow(dead_code)]
342#[path = "healing/faceted_repair.rs"]
343mod faceted_repair;
344pub use face_merge::{merge_same_surface_faces, merge_same_surface_faces_excluding};
345pub use faceted_repair::mesh_to_faceted_brep;
346#[path = "offset/offset_shell.rs"]
347mod offset_shell;
348pub use offset_shell::{
349    offset_shell, offset_shell_with_diagnostics, OffsetFaceRole, OffsetShellFaceImageRecord,
350    OffsetShellResultRecord,
351};
352#[path = "io/step.rs"]
353mod step;
354pub use step::{
355    audit_step_manifold, audit_step_pcurves, export_step, export_step_report, StepExportReport,
356};
357// Imported appearance (colour) — the carrier between an importer that READ a
358// colour and the pipeline that stamps it as name-keyed scene metadata. Not a
359// storage layer: see the module doc for the `{"color": "#RRGGBB"}` convention.
360#[path = "io/appearance.rs"]
361mod appearance;
362pub use appearance::{BodyAppearance, ImportedColor, COLOR_METADATA_KEY};
363#[path = "io/step_import/mod.rs"]
364mod step_import;
365pub use step_import::{
366    import_step, import_step_report, import_step_with_appearance, read_step_assembly, StepAssembly,
367    StepOccurrence, StepProduct,
368};
369#[path = "io/iges/mod.rs"]
370mod iges;
371pub use iges::{export_iges, import_iges};
372#[path = "io/mesh_io.rs"]
373mod mesh_io;
374pub use mesh_io::{
375    read_binary_stl, read_obj, write_binary_stl, write_obj, ObjReadResult, StlReadResult,
376};
377// Native serialized exact-BREP snapshot: the assemblies parts-library fast
378// lane (instant component insert from a cached payload; fails detectably so
379// the ACOMP self-heal lane can re-execute the embedded part document).
380#[path = "io/snapshot.rs"]
381mod snapshot;
382pub use snapshot::{
383    restore_solids, snapshot_resident_solids, snapshot_solids, RestoredSnapshot, RestoredSolid,
384    SNAPSHOT_FORMAT_VERSION,
385};
386#[path = "solvers/sketch_solver.rs"]
387mod sketch_solver;
388pub use sketch_solver::{
389    solve_sketch, solve_sketch_from_json, SketchSolverSettings, SolveSketchRequest,
390};
391#[path = "solvers/assembly_solver.rs"]
392mod assembly_solver;
393pub use assembly_solver::{
394    solve_assembly, AssemblyBody, AssemblyMate, AssemblySolution, AssemblySolveOptions, BodyPose,
395    MateAlign, MateAxis, MateKind, MatePlane, MateResidualReport, SolveStrategy,
396};
397// Assembly selection resolution (build-spec §5): a namespaced selection ref
398// resolved to an analytic frame (plane/axis/sphere/circle/line/point) read from
399// the exact BREP, feeding `MateKind` inputs in component-local coordinates.
400#[path = "solvers/assembly_resolve.rs"]
401mod assembly_resolve;
402pub use assembly_resolve::{
403    is_component_reference, resolve_component_point, resolve_edge_selection,
404    resolve_face_selection, resolve_named_selection, resolve_vertex_selection,
405    split_component_namespace, ResolveError, SelectionGeometry,
406};
407#[path = "edit/direct_edit.rs"]
408mod direct_edit;
409pub use direct_edit::{
410    delete_face_and_heal, delete_faces_and_heal, move_faces, offset_freeform_face,
411    offset_revolution_face, offset_ruled_face, offset_sphere_face, offset_torus_face,
412    resolve_face_by_point,
413};
414#[path = "healing/sew.rs"]
415mod sew;
416pub use sew::{sew_solid, split_pinched_vertices, SewReport};
417#[path = "meshing/mesh_segment.rs"]
418mod mesh_segment;
419pub use mesh_segment::{
420    mesh_regions_to_brep, segment_mesh_faces, MeshRegion, MeshSegmentation, RegionCarrier,
421    SegmentOptions, UNASSIGNED_REGION,
422};
423
424#[derive(Clone, Copy, Debug, Default, Deserialize, Serialize)]
425pub struct Vec3 {
426    pub x: f64,
427    pub y: f64,
428    pub z: f64,
429}
430
431impl Vec3 {
432    pub fn new(x: f64, y: f64, z: f64) -> Self {
433        Self { x, y, z }
434    }
435    pub fn sub(self, rhs: Self) -> Self {
436        Self::new(self.x - rhs.x, self.y - rhs.y, self.z - rhs.z)
437    }
438    pub fn add(self, rhs: Self) -> Self {
439        Self::new(self.x + rhs.x, self.y + rhs.y, self.z + rhs.z)
440    }
441    pub fn scale(self, factor: f64) -> Self {
442        Self::new(self.x * factor, self.y * factor, self.z * factor)
443    }
444    pub fn cross(self, rhs: Self) -> Self {
445        Self::new(
446            self.y * rhs.z - self.z * rhs.y,
447            self.z * rhs.x - self.x * rhs.z,
448            self.x * rhs.y - self.y * rhs.x,
449        )
450    }
451    pub fn dot(self, rhs: Self) -> f64 {
452        self.x * rhs.x + self.y * rhs.y + self.z * rhs.z
453    }
454    pub fn length(self) -> f64 {
455        self.dot(self).sqrt()
456    }
457    pub fn length_squared(self) -> f64 {
458        self.dot(self)
459    }
460    pub fn normalized(self) -> Result<Self, String> {
461        let length = self.length();
462        if length <= 1e-12 {
463            return Err("Vec3.normalized: zero-length vector".into());
464        }
465        Ok(self.scale(1.0 / length))
466    }
467    pub fn perpendicular(self) -> Result<Self, String> {
468        let x = self.x.abs();
469        let y = self.y.abs();
470        let z = self.z.abs();
471        let axis = if x <= y && x <= z {
472            Self::new(1.0, 0.0, 0.0)
473        } else if y <= z {
474            Self::new(0.0, 1.0, 0.0)
475        } else {
476            Self::new(0.0, 0.0, 1.0)
477        };
478        self.cross(axis).normalized()
479    }
480}
481
482#[derive(Clone, Debug, Default, Deserialize, Serialize)]
483pub struct Mesh {
484    pub positions: Vec<f64>,
485    pub normals: Vec<f64>,
486    pub indices: Vec<u32>,
487    pub face_ids: Vec<u32>,
488}
489
490impl Mesh {
491    fn push_vertex(&mut self, point: Vec3, normal: Vec3) -> u32 {
492        self.positions.extend([point.x, point.y, point.z]);
493        self.normals.extend([normal.x, normal.y, normal.z]);
494        (self.positions.len() / 3 - 1) as u32
495    }
496
497    fn quad(&mut self, points: [Vec3; 4], normal: Vec3, face_id: u32) {
498        let base = self.push_vertex(points[0], normal);
499        for point in points.iter().skip(1) {
500            self.push_vertex(*point, normal);
501        }
502        self.indices
503            .extend([base, base + 1, base + 2, base, base + 2, base + 3]);
504        self.face_ids.extend([face_id, face_id]);
505    }
506
507    pub fn signed_volume(&self) -> f64 {
508        self.indices
509            .chunks_exact(3)
510            .map(|tri| {
511                let point = |index: u32| {
512                    let i = index as usize * 3;
513                    Vec3::new(
514                        self.positions[i],
515                        self.positions[i + 1],
516                        self.positions[i + 2],
517                    )
518                };
519                point(tri[0]).dot(point(tri[1]).cross(point(tri[2]))) / 6.0
520            })
521            .sum()
522    }
523
524    pub fn validate(&self) -> Result<(), String> {
525        if self.positions.len() % 3 != 0 || self.normals.len() != self.positions.len() {
526            return Err("invalid vertex or normal buffer".into());
527        }
528        self.validate_geometry()
529    }
530
531    /// Geometry-only validation for consumers that use positions + indices but
532    /// NOT normals (e.g. `signed_volume`, whose tetrahedron sum never reads a
533    /// normal). Mesh-only solids such as sheet-metal bodies supply metrics
534    /// meshes with an empty normal buffer, which is legitimate for volume.
535    pub fn validate_geometry(&self) -> Result<(), String> {
536        if self.positions.len() % 3 != 0 {
537            return Err("invalid vertex buffer".into());
538        }
539        if self.indices.len() % 3 != 0 {
540            return Err("index buffer is not triangular".into());
541        }
542        let count = self.positions.len() / 3;
543        if self.indices.iter().any(|index| *index as usize >= count) {
544            return Err("index outside vertex buffer".into());
545        }
546        if !self.positions.iter().all(|value| value.is_finite()) {
547            return Err("non-finite vertex".into());
548        }
549        Ok(())
550    }
551}
552
553pub fn make_box(size_x: f64, size_y: f64, size_z: f64) -> Mesh {
554    let x = size_x.abs();
555    let y = size_y.abs();
556    let z = size_z.abs();
557    let p = [
558        Vec3::new(0.0, 0.0, 0.0),
559        Vec3::new(x, 0.0, 0.0),
560        Vec3::new(x, y, 0.0),
561        Vec3::new(0.0, y, 0.0),
562        Vec3::new(0.0, 0.0, z),
563        Vec3::new(x, 0.0, z),
564        Vec3::new(x, y, z),
565        Vec3::new(0.0, y, z),
566    ];
567    let mut mesh = Mesh::default();
568    mesh.quad([p[0], p[3], p[2], p[1]], Vec3::new(0.0, 0.0, -1.0), 0);
569    mesh.quad([p[4], p[5], p[6], p[7]], Vec3::new(0.0, 0.0, 1.0), 1);
570    mesh.quad([p[0], p[1], p[5], p[4]], Vec3::new(0.0, -1.0, 0.0), 2);
571    mesh.quad([p[3], p[7], p[6], p[2]], Vec3::new(0.0, 1.0, 0.0), 3);
572    mesh.quad([p[0], p[4], p[7], p[3]], Vec3::new(-1.0, 0.0, 0.0), 4);
573    mesh.quad([p[1], p[2], p[6], p[5]], Vec3::new(1.0, 0.0, 0.0), 5);
574    mesh
575}
576
577pub fn make_cylinder(radius: f64, height: f64, segments: usize) -> Mesh {
578    let radius = radius.abs();
579    let height = height.abs();
580    let segments = segments.max(3);
581    let mut mesh = Mesh::default();
582    for i in 0..segments {
583        let a = std::f64::consts::TAU * i as f64 / segments as f64;
584        let b = std::f64::consts::TAU * (i + 1) as f64 / segments as f64;
585        let (sa, ca) = a.sin_cos();
586        let (sb, cb) = b.sin_cos();
587        let pa = Vec3::new(radius * ca, 0.0, -radius * sa);
588        let pb = Vec3::new(radius * cb, 0.0, -radius * sb);
589        let ta = Vec3::new(pa.x, height, pa.z);
590        let tb = Vec3::new(pb.x, height, pb.z);
591        mesh.quad(
592            [pa, pb, tb, ta],
593            Vec3::new((ca + cb) * 0.5, 0.0, -(sa + sb) * 0.5),
594            0,
595        );
596        let bottom = mesh.push_vertex(Vec3::default(), Vec3::new(0.0, -1.0, 0.0));
597        let bi = mesh.push_vertex(pb, Vec3::new(0.0, -1.0, 0.0));
598        let bj = mesh.push_vertex(pa, Vec3::new(0.0, -1.0, 0.0));
599        mesh.indices.extend([bottom, bi, bj]);
600        mesh.face_ids.push(1);
601        let top = mesh.push_vertex(Vec3::new(0.0, height, 0.0), Vec3::new(0.0, 1.0, 0.0));
602        let ti = mesh.push_vertex(ta, Vec3::new(0.0, 1.0, 0.0));
603        let tj = mesh.push_vertex(tb, Vec3::new(0.0, 1.0, 0.0));
604        mesh.indices.extend([top, ti, tj]);
605        mesh.face_ids.push(2);
606    }
607    mesh
608}
609
610
611mod abi;
612pub use abi::*;
613
614#[cfg(test)]
615mod tests;